自研分布式锁服务:从Redis到MySQL降级的完整实践
写这篇文章的背景为什么最终自研了一套分布式锁服务做过几年后端的人大概率都踩过分布式锁的坑。一开始业务简单一个定时任务、一个库存扣减用 Redis 的 SETNX 就能糊弄过去。等业务发展到多个服务实例同时跑、数据一致性要求变高的时候单机 SETNX 那套马上就不够用了。我之前在某业务团队负责一个订单状态流转系统初期就是单 Redis 主从架构配置中心里放一把大锁哪个节点抢到锁谁就跑任务。后来出现了一个印象很深的事故主从切换的瞬间从节点还没同步到锁的 Key结果两个节点同时认为自己是持锁方同一笔订单被两个任务各处理了一次账单金额对不上排查了大半天。问题修复后我意识到分布式锁这件事看着简单真要做到生产可用涉及存储选型、原子操作、续约保活、降级补偿、可观测性等一整套设计。与其继续在各个业务里各自为政写锁不如沉淀一个独立的分布式锁服务。文章所讲的这套分布式锁服务从设计到上线大概一个半月支持 Redis 主模式、哨兵模式、Redis Cluster并且提供失效降级到 MySQL 的能力。核心设计原则是主路径必须够快降级路径必须不能烂所有锁都要可追溯。如果你是正在做微服务改造、或者已经在用 SETNX 但心里没底的同学这篇内容应该能帮你避开不少坑。1. 自研锁服务之前我们到底在纠结什么很多团队听到自研分布式锁第一反应是Redis SETNX 不是已经能用了吗为什么要花一个半月来做这件事这个问题的背后其实是几个很关键的取舍。1.1 为什么不用现成的开源锁组件当时我们对比过 Redisson、ZooKeeper 实现、数据库乐观锁等方案。Redisson 的锁确实做得不错但它有个隐性问题它要求业务方把锁的调用嵌入到业务代码里锁的 Key 命名、过期时间、重试策略都需要业务自己维护。我们内部有十多个业务线每个团队写出来的锁用法五花八命有的把过期时间设成 1 小时有的忘记释放锁靠天然过期兜底出了问题根本没法统一排查。ZooKeeper 的锁走的是临时顺序节点 watch 机制可靠性高但性能上限比较低。我当时做过一个粗测单集群环境下 ZooKeeper 的写并发能到每秒几千就已经不错了但我们的高并发场景里锁的获取请求峰值能达到每秒两万以上。而且很多业务方并不愿意为了拿一把锁多引入一套 ZooKeeper 依赖运维成本也是实打实的。所以最终决定自研不是说开源方案不够好而是我们需要一把统一入口、统一规范、能降级、能观测的锁。自研的核心收益在于锁的获取和释放逻辑收敛在一处业务方只需要引入一个轻量 SDK至于锁存哪、怎么续约、怎么降级全部由服务端负责。1.2 锁服务需要满足的核心条件动工前我们把需求拆成了几条硬性标准互斥性任意时刻同一个资源只能被一个客户端持有锁。防死锁持有锁的客户端崩溃或网络异常后锁必须在有限时间后自动释放。可重入同一个客户端在已经持锁的情况下允许再次获取同一把锁。高性能正常路径下获取锁的延迟 P99 要低于 5 毫秒。可降级Redis 不可用时系统不能直接雪崩要有替代方案。可观测每次加锁、解锁都要有日志和指标出了问题能查。这六条看着简单真落地时每条都有一堆细节。比如防死锁依赖过期时间但过期时间设短了业务执行时间稍微长一点就误伤设长了万一持锁方崩了其他人要等很久。标准答案是用看门狗续约但看门狗本身又是一个新增的故障点怎么设计才能既可靠又不会过度消耗资源我在后面的续约部分会细讲。另外我们在设计之初就定了一个原则宁可让锁偶尔失效也不能让加锁动作拖垮业务主链路。这句话决定了后面所有的降级策略。2. 锁服务的整体架构与 API 设计先说结论分布式锁服务不是一个独立进程就能搞定的东西它是服务端 SDK 存储层三件套的组合。服务端提供锁的申请和释放接口SDK 负责在业务进程内做本地缓存和临界区控制存储层支持 Redis 和 MySQL 两种后端。2.1 对外暴露的 API 长相API 设计上我们没有让业务直接传 Redis Key而是要求业务方先注册锁资源。锁资源的定义包括资源名称、默认过期时间、最大续约次数、是否允许降级。注册完成后后续加锁只需要传资源 ID 业务唯一请求 ID。// 伪代码示意 ResourceLock(name order:settle, expireMs 30000, downgrade true) public Result settleOrder(Long orderId) { LockToken token lockClient.tryLock(order:settle, orderId.toString(), 3, TimeUnit.SECONDS); if (token null) { // 拿不到锁走快速失败或排队逻辑 } try { // 业务逻辑 } finally { lockClient.unlock(token); } }这里有两个细节值得说一下。第一个是 tryLock 的 waitTime 参数。我们内部对锁竞争分了两类场景一类是必须立即拿锁否则就干别的事比如读缓存回源另一类是拿不到锁就死等一会儿比如订单退款状态流转。waitTime 0 表示非阻塞快速失败waitTime 0 表示阻塞等待。阻塞等待不能在服务端做服务端只负责存储锁状态真正的阻塞要靠客户端本地用阻塞队列模拟。这个后面会讲。第二个是返回值 LockToken。它里面封装了锁的 ID、持有者标识、过期时间点、续约次数上限等元信息。所有信息通过 JSON 序列化存进 Redisvalue 就是 LockToken 的 JSON 串。这样解锁的时候服务端能校验是不是同一个持有者避免误删别人持有的锁。2.2 服务端与 SDK 的职责边界服务端是一个无状态集群提供 REST 接口内部操作用 Lua 脚本实现原子性。SDK 在业务进程里维护了一个锁资源的长连接池每个锁资源对应一个信号量。获取锁的流程大致是这样的业务调用 SDK 的 tryLock。SDK 先去本地缓存里看这个资源是否有锁且已过期如果有且未过期说明锁被其他持有者占着走等待或快速失败。如果本地判断可获取SDK 发请求到这个锁服务集群。锁服务集群用 Lua 脚本在 Redis 里写锁写入成功则返回 LockToken。SDK 收到 LockToken 后在本地开启看门狗协程定期续约。SDK 做本地缓存的意义在于同一个资源在单位时间内的加锁请求量可能很大比如秒杀场景下的库存预占如果每个请求都穿透到 Redis会白白增加一次网络往返。我们允许 SDK 在锁已存在时直接返回失败只有当锁不存在或已过期时才真正请求锁服务这样大幅减少了服务端的压力。2.3 锁的存储模型Redis 模式下的 Key 设计为lock:{resourceId}:{businessId}value 为 LockToken JSON。过期时间取了默认值和业务指定值的较小者上限为 60 秒防止业务方乱来。MySQL 降级模式下的表结构比较简单CREATE TABLE lock_record ( id bigint(20) NOT NULL AUTO_INCREMENT, resource_id varchar(64) NOT NULL, business_id varchar(128) NOT NULL, holder_id varchar(64) NOT NULL, expire_at datetime NOT NULL, create_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_resource_business (resource_id, business_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;唯一键uk_resource_business保证了互斥性插入成功的那个事务获得锁。MySQL 方案是降级用的不追求极致性能但保证在 Redis 挂掉时业务还能用一把慢锁跑起来。3. Redis 模式的核心实现加锁、解锁与续约的细节Redis 是主路径整个锁服务的性能上限全靠这一层撑起。实现上没有用什么高深的技术核心就是 Lua 脚本加几个容易踩坑的细节。3.1 加锁的 Lua 脚本与防误删设计加锁的核心逻辑很简单SETNX EXPIRE 的原子操作。Redis 官方推荐用 SET key value NX EX seconds 一条命令搞定但我们还多了一层持有者校验同一把锁同一个持有者可以重入次数加一。-- KEYS[1] lockKey -- ARGV[1] LockToken JSON -- ARGV[2] expireMilliseconds if redis.call(exists, KEYS[1]) 0 then redis.call(set, KEYS[1], ARGV[1], EX, ARGV[2]) return 1 end local current redis.call(get, KEYS[1]) if current ~ false and cjson.decode(current)[holderId] cjson.decode(ARGV[1])[holderId] then local newToken cjson.decode(current) newToken[reentrant] newToken[reentrant] 1 redis.call(set, KEYS[1], cjson.encode(newToken), EX, ARGV[2]) return 2 end return 0这个脚本的亮点在于重入逻辑。很多团队的重入是通过本地 ThreadLocal 实现的但本地重入只对同一个 JVM 进程内多线程有效如果同一个客户端跨两台机器调用本地状态就不共享了必须把重入次数写进 Redis。我们把重入计数值放在 LockToken 的 reentrant 字段里每次加锁判断 holderId 相同就次数加一。防误删同样重要。解锁时如果直接 DEL很可能把另一个客户端刚获取到的锁给删掉这是 Redis 分布式锁最常见的误操作。正确做法是解锁前先比对 holderId一致才删除。这个操作也必须用 Lua 保证原子性-- KEYS[1] lockKey -- ARGV[1] LockToken JSON local current redis.call(get, KEYS[1]) if current false then return 0 end local currentObj cjson.decode(current) local requestObj cjson.decode(ARGV[1]) if currentObj[holderId] ~ requestObj[holderId] then return -1 end if currentObj[reentrant] 1 then currentObj[reentrant] currentObj[reentrant] - 1 redis.call(set, KEYS[1], cjson.encode(currentObj), EX, currentObj[expireAt] - currentObj[renewCount] * 1000) return 2 end redis.call(del, KEYS[1]) return 1有个很隐蔽的坑cjson 在 Lua 里处理空值时会把 null 字段自动删除而持有者信息一旦缺字段后续解锁就会失败。我们的经验是 LockToken 序列化时所有字段必须给默认值不能用可空对象否则线上问题极难排查。3.2 看门狗续约续约怎么才能不成为新的故障点考虑一个场景业务代码执行时间超过了锁的过期时间比如一个统计任务跑了 90 秒锁只设了 30 秒过期。第 30 秒时锁被 Redis 自动删除另一个节点拿到了锁两边同时写同一份数据数据就坏了。解决方案有两种让业务方自己评估执行时间把过期时间设得非常大。简单粗暴但一旦持锁方崩溃锁要等很久才能被释放影响可用性。用看门狗定期续约锁快过期时自动延长业务执行多久就续多久。我们选了看门狗方案。但看门狗的实现比想象中复杂续约操作必须判断锁的持有者是不是自己否则会帮别人续约。续约频率也要控制默认锁过期时间 30 秒每 10 秒续约一次也就是过期时间的三分之一。这个频率不能太快否则 Redis 会被续约请求打爆也不能太慢否则锁可能已经过期但还没续上。续约的 Lua 脚本更简单if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(expire, KEYS[1], ARGV[2]) end return 0这里有个值得注意的设计续约只判断 value 是否等于当前 LockToken 的 JSON 字符串不再单独比对 holderId。原因是我们要求 LockToken 里有一条唯一 token 字段每次加锁生成的 token 都不同。这样即使重入场景下同一业务多次加锁token 也是新的不会误续。看门狗本身的启停需要特别小心。SDK 在收到 LockToken 后启动一个守护协程业务在 finally 块里调用 unlock 后SDK 先通知守护协程停掉再进行解锁操作。这个顺序不能反——如果先解锁再停看门狗续约线程可能在锁已经释放后还续上一次把 Redis 里那把已经释放的锁又给续活了后续别人永远加不上锁。我们这里吃过一次亏修复方式是解锁和停止续约用了同一个进程内的互斥锁保证操作先后顺序。3.3 阻塞获取与本地排队前面提到 tryLock 支持 waitTime 0 的阻塞等待。这个功能不能做成服务端阻塞线程等待因为锁服务的线程会被全部占满阻塞请求一多整个集群就瘫痪了。我们采用的办法是在 SDK 里为每个锁资源维护一个信号量。线程 A 获取锁失败后不会直接返回而是进入信号量等待队列。锁释放后脱锁成功的线程会释放一个信号量许可唤醒等待队列里的下一个线程。这个机制相当于把排队从服务端搬到了客户端服务端的 Redis 永远只处理无阻塞的加锁请求。这里的问题在于公平性。如果不做处理信号量唤醒的线程可能不是最早开始等待的那个导致饥饿。我们用的是公平信号量内部维护了一个 FIFO 队列每次唤醒队首线程保证先来先得。实测下这个排队方式对大多数业务都够用毕竟分布式锁的等待时间通常都在秒级以内。信号量队列还有一个好处锁被释放后唤醒的线程不用再等网络 RTT直接本地就拿到了虚拟锁。所谓虚拟锁是指 SDK 在信号量唤醒后直接在本地生成一个续约状态然后异步向锁服务补充一次加锁请求。这种设计把关键路径上的网络时间从加锁阶段转移到了后台异步阶段体验上会顺滑很多。4. 降级链路Redis 不可用时系统不能直接雪崩业务系统选型锁服务最怕的就是锁服务本身成为单点。我们把降级设计了三个层级Redis 正常 - Redis 不可用降级 MySQL - MySQL 也不可用降级本地内存锁。每层降级的触发条件和服务保障都不一样。4.1 Redis 到 MySQL 的降级流程与切换时机锁服务会定时给 Redis 发心跳探测连续三次失败大概 15 秒就触发降级。降级不是瞬间把所有流量切到 MySQL而是先在服务端标记降级中新来的加锁请求直接走 MySQL已有 Redis 持锁的请求继续走 Redis 续约等它们自然过期。这个平滑切换很重要。如果一刀切立即降级正在 Redis 持锁执行业务的那些节点会发现续约失败锁在临界点丢失数据一致性就保不住了。我们的做法是服务端标记降级后Redis 锁的续约请求继续放行但不再接受新的 Redis 加锁请求。新加锁请求全部走 MySQL 插入语句成功即得锁。等旧 Redis 锁全部过期最多等一个最大过期时间 60 秒Redis 存储层停止所有读写进入纯 MySQL 模式。MySQL 也失败时SDK 本地内存锁兜底同时触发告警。MySQL 模式的加锁实现public boolean tryLockByMysql(LockResource resource, String businessId, String holderId, long expireAt) { try { lockRecordMapper.insert(new LockRecord(resource.getName(), businessId, holderId, new Date(expireAt))); return true; } catch (DuplicateKeyException e) { // 说明已有人持锁执行清理过期锁的逻辑 int cleaned lockRecordMapper.deleteExpired(resource.getName(), businessId, new Date()); if (cleaned 0) { // 重试一次 return tryLockByMysql(resource, businessId, holderId, expireAt); } return false; } }这里有个很关键的细节MySQL 模式下如果检测到锁记录已存在但同时已过期先删除过期记录再重试插入。删除操作要加条件expire_at now否则会把别人还没用完的锁给删了。我们用唯一键冲突来判断是否持锁而不是先 select 再 insert避免并发检查时出现两个线程都查到没有记录然后都 insert 成功的情况——虽然唯一键会挡住后者但靠报错来判断总归是更保险的做法。4.2 MySQL 模式下的锁记录清理与异常兜底MySQL 模式跑久了之后锁记录会越积越多尤其是那些业务方忘了释放锁的情况。所以必须有补偿机制。我们后台跑了两个任务过期清理任务每分钟扫描一次lock_record表删除所有expire_at now的记录。这个任务在高并发下要小心批量删除锁表导致的行锁竞争所以我们没有一次删全表而是每次都limit 500配合主键范围删除避免长时间持锁影响正常加锁操作。持有者心跳校验任务每 10 秒扫描一次检查锁的持有者是否还活着。这个校验是通过 Redis 里每个客户端注册的临时节点一个固定 Key做的如果 Key 过期了说明 SDK 所在机器可能挂了锁服务主动清掉 MySQL 里的锁记录。MySQL 模式还有一个天然问题加锁性能比 Redis 慢了一个数量级。我们用读写分离缓解了一部分读压力但写锁本质上还是单点串行超过每秒三千次加锁就会开始出现等待。所以降级模式我们定位是保证可用、不保证高性能业务方在降级期间会感知到锁获取变慢这是可以接受的毕竟 Redis 挂了系统没挂。4.3 最终兜底进程内锁按照资源维度隔离如果 MySQL 也挂了SDK 会在进程内退化为本地锁。实现非常简单用ConcurrentHashMapString, ReentrantLock按资源维度维护一把本地锁同一进程内同一个资源的所有线程互斥。这个兜底只对单机场景有效多机场景下是没有互斥效果的所以我们只在所有远程存储都不可用这个极端条件下启用且同时打告警。这里有个设计原则降级结构是逐级失去一致性的Redis 模式提供最强互斥MySQL 模式提供中等互斥本地锁几乎不提供跨机互斥。我们允许多级降级一致性逐级放宽但在告警上做了强提醒——一旦进入本地锁模式页面会红屏告警值班同学必须第一时间介入处理存储故障因为这种状态下系统已经不具备跨节点互斥能力只能靠快速恢复存储来止损时间越长数据风险越大。5. 上线压测暴露的几个真问题这篇文章如果不写踩坑部分价值至少少了一半。这里有四个问题都是压测阶段真实遇到并解决的希望对你有参考价值。5.1 主从切换导致锁丢失压测时我们专门做了主从切换演练。结果和预想的一样Redis 主节点宕机后从节点晋升为主节点但锁的 Key 还没来得及同步新的主节点上查不到这把锁另一个客户端就能成功加锁两个客户端同时持锁。解决思路有两个一是开启 Redis 的 WAIT 命令写锁后等待至少一个从节点确认收到数据再返回成功。这能降低锁丢失概率但会显著增加加锁延迟。二是在锁服务层面做兜底——加锁时把 LockToken 异步写一份到 MySQL作为锁事实表Redis 查询不到时回查 MySQL 确认。两种我们都做了生产环境实际开启的是第一种WAIT 同步到至少一个从节点压测中加锁 P99 从 3 毫秒涨到了 12 毫秒但换来了鲜明的安全性提升。5.2 时钟跳跃引发的续约异常看门狗续约依赖系统时钟计算过期时间。如果服务器时钟被 NTP 拨快锁可能提前过期拨慢锁可能延迟过期。压测中某台机器时钟跳了 2 秒导致一批锁在业务执行中途消失触发了大量数据冲突告警。修复方式是LockToken 里同时记录客户端本地时间戳和服务端时间戳续约时以服务端时间为准客户端本地时间只用于生成等待信号。后续我们把依赖时钟的代码全部收敛到了锁服务端SDK 侧不直接依赖本地时间做任何决定。5.3 锁的内存占用与锁膨胀业务一多锁资源成千上万每个锁 Key 在 Redis 里都是一个字符串长时间积累后内存占用非常可观。压测时我们发现某个压测环境 Redis 内存涨了 3GB全是历史遗留的锁 Key。解决方式所有锁 Key 强制设置过期时间即使业务方没显式指定服务端也会给一个默认的 30 秒 TTL同时定期扫描资源列表超过 7 天没有请求的锁资源自动下线并清理数据。分布式锁的核心原则是短生命周期一个活跃业务时刻同时存在的锁数量不应该超过绑定资源的数量级如果超过大概率是业务方把锁用错了场景。5.4 大事务持锁时间过长引发的连锁反应压测中遇到过最棘手的问题一个订单流程里业务方在持锁状态下执行了远程 RPC 调用、数据库批量写、消息队列发送等操作整个持锁时间超过 10 秒。这样一来所有依赖同一把锁的操作全部排队锁服务的等待队列瞬间拉满反而把正常请求拖垮了。这个问题本质上不是锁服务能解决的它属于业务设计问题。我们在 SDK 侧做了两件事一是在加锁时允许业务传入预估持锁时间超过预估时间 3 倍后触发告警日志二是提供tryLockWithLease方法业务必须显式声明持锁时间上限超过上限哪怕业务没执行完锁也会被强制释放并抛出锁续约异常。虽然这个机制会导致个别慢业务执行失败但总比整个系统因为锁被长期占用而雪崩要好。6. 从单把锁到锁服务还差几个面向未来的设计到这里分布式锁服务的基本功能已经完整了但真正让这套系统能持续跑在生产环境的往往是这些边缘功能。6.1 锁的可观测性设计每次加锁、解锁、续约、降级都会上报指标指标维度包括资源名称、业务方应用名、加锁是否成功、耗时、解锁是否成功、当前持锁数、排队数、等待时长。有了这些数据我们才能定位哪个业务方不管控锁使用、哪个资源锁竞争最激烈、降级是否频繁。排查线上问题时我强烈建议把锁的 Token 打印到业务日志和链路追踪系统里。锁的 Token 里包含 holderId 和业务请求 ID当业务发现数据异常时直接按 Token 搜日志马上能定位到是哪台机器、哪个请求在操作这把锁以及锁是何时释放的。6.2 锁与分布式事务、幂等的边界分布式锁只解决互斥问题不解决最终一致和幂等问题。很多团队误以为有了锁分布式事务就安全了这是很危险的认知偏差。我们内部给业务方培训时反复强调锁保证的是同一时刻只有一个节点在执行这段代码但代码执行完后数据落库、消息发送是否成功锁管不了。如果业务需要严格幂等必须把幂等键设计到业务表里数据库唯一键兜底而不是依赖锁的互斥性。分布式事务要么走可靠消息要么走本地消息表锁只是其中的一个前置条件不能替代事务框架。6.3 未来演进方向继续演进的话有几个方向我觉得值得做接入多租户和权限管理不同团队之间锁资源隔离避免 A 团队误删 B 团队的锁。自适应过期时间。当前是固定过期时间加固定频率续约未来可以结合业务历史执行时长动态调整续约间隔让续约更精准。把锁的决策从客户端 SDK 本地缓存升级到边缘节点加速在业务入口处做一层锁状态缓存进一步减少跨机房延迟。不过这些都是远期规划了。对大多数团队而言先把手头的分布式锁做到可用、可观测、可降级已经能解决 90% 的问题。真正的价值不在于用了什么高级技术而在于把一条看似简单的链路做扎实让每一把锁都清清楚楚不会成为生产事故里最让人难眠的那个疑点。

相关新闻

外贸独立站和国内网站的核心差异,从服务器到支付一文说清

外贸独立站和国内网站的核心差异,从服务器到支付一文说清

做了这些年外贸独立站,最常被问的一句话就是:“我在国内好好一个网站,把文字翻译成英文,是不是就能直接卖给老外了?”每次听到这个问题我都想叹气。外贸网站和国内网站看起来都是网站,但背后的用户习惯、平…

2026/10/12 2:00:05 阅读更多 →
data-id数据指纹与游标式翻页:解决爬虫分页偏移

data-id数据指纹与游标式翻页:解决爬虫分页偏移

1. 为什么说data-id是爬虫设计的“金线”先聊一个很实际的场景。你接到一个采集需求,目标是一个带列表页的站点,列表里每条数据长得差不多,名称、缩略图、发布时间都能抓到,但真正能唯一定位一条记录的标识,往往不在肉…

2026/10/12 2:00:05 阅读更多 →
2025年下半年网络规划设计师综合知识真题及答案解析

2025年下半年网络规划设计师综合知识真题及答案解析

2025年下半年网络规划设计师综合知识真题及答案解析 第1题 1983年,ARPANet完成技术转型,对此后的 Internet发展全关重要,这一转型是指( ) A.电路交换转向分组交换 B.商业化转型 C.民用化转型 D.从 NCP 协议转向 TCP/IP 答案:D 解析:ARPANet 最初使用 NCP (Netw…

2026/10/12 2:00:05 阅读更多 →

最新新闻

百万级缺陷样本开源:工业视觉的「地基」被补上了

百万级缺陷样本开源:工业视觉的「地基」被补上了

1.工业质检的两道坎 ▍坎一:数据各管各的现成的工业缺陷数据集,几乎都窝在单一行当里。VisA、3CAD 盯着 3C 电子,PKU-GoodsAD 盯着包装,Real-IAD、MulSen-AD 盯着材料。覆盖面稍宽些的 VISION、MVTec AD、MMAD,又卡在…

2026/10/12 5:42:21 阅读更多 →
多模态 RAG 实战(二):一张图片,为什么我给它建立了两条向量索引?

多模态 RAG 实战(二):一张图片,为什么我给它建立了两条向量索引?

上一篇文章聊了一个多模态 RAG 中遇到的问题:图片明明被检索到,最终回答却可能没有正确展示。这次想把视角往前移,聊聊图片是如何被检索到的。在项目中,我通过 MinerU 解析 PDF,将图片保存到 MinIO。但图片存下来只是第…

2026/10/12 5:42:21 阅读更多 →
研究中合成数据使用闸门:失败含义与当天最小实验

研究中合成数据使用闸门:失败含义与当天最小实验

图:披露、种子复现、来源标记、漂移声明与结论边界五闸门,加合规打包。 千笔-AIWritePaper https://www.aiwritepaper.com 合成与 AI 生成数据能补样本、护隐私、做情景,但也会冒充实测、污染下一代训练集。欧盟出版署 data.europa academy …

2026/10/12 5:42:21 阅读更多 →
写时复制、读写分离:一文吃透CopyOnWriteArrayList原理与实战

写时复制、读写分离:一文吃透CopyOnWriteArrayList原理与实战

如果你在并发环境里用过ArrayList,大概率被ConcurrentModificationException教育过:一边遍历一边改数据,轻则抛异常,重则读到脏数据。很多人第一反应是加锁,加synchronized关键字或者直接套一层Collections.synchroniz…

2026/10/12 5:42:21 阅读更多 →
从ReAct到多Agent协作:AI Agent工程化落地关键实践

从ReAct到多Agent协作:AI Agent工程化落地关键实践

1. 从一套“能干活”的提示词说起这几年 AI Agent 的概念红得发紫,但很多团队拿着大模型 API 折腾了个把月,最后发现所谓的 Agent 就是个“会聊天的脚本”。真正把 Agent 推向生产环境,绕不开两个关键词——ReAct和多 Agent 协作。我参与过几…

2026/10/12 5:42:21 阅读更多 →
【AI应用】用TaoToken统一Key把AI生成的HTML一键转成可编辑PPTX

【AI应用】用TaoToken统一Key把AI生成的HTML一键转成可编辑PPTX

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 5:41:20 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →