事故清单摊开看
老王有个习惯:每次事故复盘后,把「现象、根因、解法」压成三行记进事故本。本篇从他的本子里挑出七个与分布式锁相关的,按时间倒序排——最近的最疼。
事故一:一次升级引发的误删锁
现象:月底对账,两张卡各出现一笔「钱没扣够、咖啡照出」的流水,与系列开篇如出一辙,但这次三实例部署、扣款代码带了 Redis 锁。根因:升级那天数据库主从切换抖了 15 秒,A 实例的扣款事务被拖长,超过锁的 10 秒 TTL;锁过期被 B 抢走;A 恢复后 DEL 释放锁——删的是 B 的锁,C 顺势又进来。解法:释放锁换 Lua「校验 UUID 再删」(第 2 篇),并给临界区业务加执行时长监控,业务时长逼近 TTL 就告警——锁的参数要跟业务的实测时长对表,不能拍脑袋。
事故二:一行 leaseTime 拆了看门狗
现象:某天起,偶发的「同一订单被处理两次」,频率千分之一,查无规律。根因:新人把 lock.lock() 改成了 lock.lock(10, TimeUnit.SECONDS)「为了防止锁泄漏」——这一改,Redisson 的看门狗直接停摆(第 4 篇),业务一慢锁先过期。解法:代码评审立规矩:临界区时长不可控的场景一律无参 lock();带 leaseTime 的写法必须在注释里写明「业务实测时长上界」;CI 里对 lock 的带参调用打标记。一行参数之争,就是看门狗的生死开关。
事故三:主从切换的双扣款
现象:凌晨 Redis 哨兵切换(主库硬件故障),切换完成后的 3 分钟内,同一张卡被扣了两次款。根因:教科书级丢锁(第 7 篇)——主库写入锁记录未同步即宕机,从库升主,第二个客户端在新主上抢锁成功。解法:扣款主路径改造为「数据库乐观锁版本号 + 幂等键」(第 16 篇方案一),Redis 锁降级为防重复提交的效率锁;资金相关的新代码评审强制过一遍「丢锁了会怎样」的灵魂拷问。
事故四:GC 停顿后的幽灵发券
现象:大促发券活动,部分会员收到两张同样的券。根因:发券服务的 ZK 会话 20 秒,Full GC 停顿 25 秒,会话过期临时节点蒸发(第 11 篇),另一实例接力发券;原实例 GC 结束继续跑完——两个持有人各发一张。解法:短平快的是给发券加唯一索引(会员 ID 加活动 ID),一次不慎变成全程免疫——凡是「防重复发东西」,唯一索引永远比锁可靠;同时 JVM 调优把 Full GC 压进会话超时的三分之一。
事故五:高频抢锁拖垮注册中心
现象:秒杀压测时,服务注册与发现突然大面积失联,新实例无法注册,Dubbo 调用开始报无提供者——但秒杀服务本身没挂。根因:秒杀的抢购锁放在了全局 ZK 上(与注册中心共库),每秒数万次锁操作走过半提交,ZK 写入队列积压,会话心跳被拖死(第 11 篇坑四),注册临时节点成批蒸发。解法:秒杀改「Redis 锁加库存分段」方案,ZK 只留选主与配置仲裁;立下铁律——协调服务一身不兼二职,注册配置与高频锁必须隔离。
事故六:缓存击穿,数据库连坐
现象:某个爆款商品的缓存到期瞬间,数据库连接池瞬间打满,全站读接口跟着超时。根因:回源逻辑没加互斥(或加了自旋锁但没做单飞收敛),几千个请求同时穿透到库重建同一份缓存(Redis 系列第 5 篇的经典场景,第 6 篇单飞的正解)。解法:互斥重建改成单飞模式——第一个请求干活,其余等结果 key;配合逻辑过期永不过期策略,击穿窗口归零。
事故七:永生锁的远古传说
现象:某条历史遗留任务的锁 key 永远删不掉,新任务抢不到锁,团队没人知道这 key 是谁、什么时候加的。根因:SETNX 加 DEL 的远古写法(第 2 篇第一代坑),持有人当年 OOM 后锁永生;又因为 value 没有身份信息,运维不敢贸然删。解法:确认业务后手动清除;全员重申锁的三要素(TTL、身份、原子释放);所有锁 key 必须带「用途与负责人」的元数据约定,方便事故时的取证与善后。
七个事故串起来看,规律高度收敛:一半的锅在「锁的参数与业务时长不匹配」,三成的锅在「该用唯一索引和幂等的场景用了锁」,剩下的锅在「把锁当成了万能护栏」。下一篇收官,把十八篇的方案、七篇的事故,全部收进一张作战地图。
评论 (0)