【背景】
做了 10 年高并发后端,带过 3 个 20+ 人的团队。分布式锁是我见过"被默认使用"最多的原语之一——需求评审时一句"这里需要加个锁"就过了,Redis setnx + TTL 就上了。但过去两年我逐条审视了团队里 17 个用到分布式锁的场景,最后只保留了 3 个,其余 14 个全部换掉了。
这不是"锁不好",而是"锁被当成了思维短路"。
【常见分歧:三派做法】
我看到的典型分歧有三派:
派 A「锁是最安全的」:Redis setnx + TTL,代码量最少,"加一行就完了"。但三个经典坑每一个都能造成双写或重复执行——TTL 到期锁自动释放而持锁线程还没跑完(GC 停顿 > TTL 是真实事故);Redis 主从切换导致锁失效(setnx 写到了旧 master,failover 后新 master 没有这条 key);Redlock 在时钟漂移下的正确性至今有争议(Martin Kleppmann 那篇批评你们看过吗?)。
派 B「用数据库兜底」:SELECT ... FOR UPDATE、唯一键约束、乐观锁(version 字段)。好处是和事务天然绑定,不存在"锁释放了但事务还没提交"的窗口。问题:热点行上 FOR UPDATE 竞争会显著拉高 P99(我们实测过,同一行 50 并发更新,P99 从 3ms 飙到 40ms);乐观锁在高并发下重试风暴,重试 5 次以上失败率开始指数上升。
派 C「根本不需要锁」:幂等设计 + 唯一键 + 状态机。比如订单从 PAID → SHIPPED 是单向的,并发调两次 ship 接口,第二次因为状态不对直接拒绝;支付回调带 requestId,幂等表一查就知道处理过没有。问题:需要业务建模配合,不是所有场景都能找到"天然幂等键",尤其是跨服务聚合操作。
【我的倾向与理由】
我的原则一句话:先问"这个操作并发执行两次会怎样",如果答案是"业务能容忍"或"有天然去重键",就不要上锁。
具体判断顺序(我在团队里推了两年,写进了 code review checklist):
- 操作是否天然幂等?(PUT /update、唯一键 INSERT、状态机单向转移)→ 不需要锁,重复调用无副作用
- 是否有天然去重键?(requestId、orderId + action、业务流水号)→ 用唯一键 / 幂等表,DB 层兜底
- 是否真的需要"互斥执行"?(check-then-act:先查余额再扣款)→ 这里才考虑锁,优先用数据库行锁(和事务绑定,不存在锁-事务分离的窗口)
- 如果非要用 Redis 锁:TTL 怎么设?持锁期间 Full GC 了怎么办?主从切换了怎么办?—— 这三个问题答不上来就别用,答得上来也建议加 fencing token
我最终保留的 3 个场景都是"跨服务的先占后做"(比如抢库存 → 调支付 → 确认扣减),没有单一数据库可以兜底,锁 + 超时 + 补偿确实是当前唯一能用的手段。剩下的 14 个场景,换成了唯一键(6 个)、状态机(5 个)、乐观锁(3 个)。
【想听大家在什么场景下怎么选】
抛几个具体问题,想听各位的实战经验:
- 你们团队里,分布式锁 vs 乐观锁 / 唯一键 / 状态机的"使用比例"大概是多少?有没有人做过"锁审计"——把线上所有 Redis 锁列出来,逐条问"去掉会怎样"?
- Redis 分布式锁的 TTL 你们怎么定?是"业务预估 P99 × 2"还是"拍个 30s"?有没有遇到过 TTL 到期但业务还没跑完的事故?Full GC 8 秒 + TTL 5 秒的 combo 你们见过吗?
- 有没有"一开始上了 Redis 锁,后来发现根本不需要"的 case?是怎么发现并安全去掉的?(我一般用双写过渡 + 影子对比跑一周再切)
- 对于"跨服务 check-then-act"(查余额 → 扣款),除了分布式锁 + 补偿,你们还有没有更优雅的方案?TCC?Saga?还是干脆接受"超卖"再异步退款?
我特别想听"反方"的声音——如果你认为分布式锁在你的场景里是刚需而不是伪需求,请给出具体场景和约束条件,我看看我的判断框架漏了什么。