为什么不能滥用分布式锁?
简化版
分布式锁会把并行操作串行化,并引入网络、租约、续期和故障恢复成本;粒度过大还会造成热点与级联超时。能用唯一约束、CAS、幂等、状态机或数据分区解决的问题,通常不应先上分布式锁。
详细版
锁适合保护短小、冲突真实存在且无法用存储原子操作表达的临界区。滥用常表现为给整个业务流程加一把大锁、持锁期间调用慢接口、把锁当成幂等或事务、以及为了“保险”锁住本可并行的不同资源。
评估时先明确不变量,再看数据库条件更新、唯一索引、队列按 key 串行、单写者或本地分区能否满足。如果必须用锁,就缩小 key 与临界区,设置有依据的租期和获取超时,并监控等待时间、竞争率、续期失败及持锁时长。
完整版教学
一、先问要保护的究竟是什么
“这里有并发,所以加锁”不是需求。需求应写成可验证的不变量,例如同一优惠券只能核销一次、库存不能小于零、同一设备同时只运行一个升级任务。只有明确了共享资源、冲突动作和允许的结果,才能判断需要排他锁、条件更新还是幂等去重。
心法:锁是维护不变量的一种手段,不是给并发代码贴上的安全标签。
二、分布式锁的固定成本
每次 acquire/release 至少增加网络往返,强一致锁服务还要经过复制或共识。持锁者需要租约、续期、所有者校验和故障回收;调用方要处理获取失败、超时和不确定结果。这些逻辑扩大故障面,锁服务变慢时还会把大量业务线程堵在入口。
请求 -> 获取锁 -> 业务读写 -> 释放锁
| |
网络/排队 GC/下游超时
三、串行化怎样吞掉吞吐
若临界区平均耗时 50ms,同一个 key 理论上最多约 1 / 0.05 = 20 QPS;下游偶发 2 秒超时,该 key 后面会迅速堆积。把租户级锁错误扩大为全局锁,即使 100 个租户互不影响,也会共享这 20 QPS 上限。
| 锁粒度 | 并行度 | 实现复杂度 | 典型风险 |
|---|---|---|---|
| 全局锁 | 最低 | 低 | 单热点、雪崩 |
| 租户锁 | 中 | 中 | 大租户热点 |
| 资源锁 | 高 | 中 | key 管理 |
| 无锁条件写 | 高 | 低到中 | 冲突重试 |
四、优先考虑的替代方案
创建唯一约束或幂等键;数值扣减可用带条件的原子更新;状态迁移可用版本号 CAS;同 key 事件可按分区进入队列;数据能按 owner 划分时可采用单写者。替代方案把并发裁决下沉到真正保存状态的组件,通常比“先拿外部锁、再写数据库”少一个一致性窗口。
UPDATE stock
SET available = available - 1, version = version + 1
WHERE sku_id = :id AND available > 0 AND version = :old_version;
影响 1 行表示成功,0 行表示库存不足或竞争失败;这类原子条件写无需让所有 SKU 共用锁。
五、锁不能替代事务与幂等
拿到锁后写数据库成功,但释放锁前响应丢失,客户端仍会重试;没有幂等键就可能重复执行。持锁期间先扣库存、后调用支付失败,锁也不会自动回滚前一步。锁只约束参与同一协议的并发者,绕过锁的脚本和其他服务仍能破坏数据。
因此数据库唯一约束、事务边界、状态机和补偿机制仍是最终防线。锁更多用于降低冲突或选出临时执行者,不应成为数据正确性的唯一依据。
六、确实要用时怎样收敛风险
锁 key 应精确到冲突资源,临界区只包含必须互斥的本地或存储操作,网络调用尽量移出锁外。租期要覆盖正常 P99 并对异常设上限,获取失败快速返回或有界退避,不能无限阻塞业务线程。释放时校验 owner,迟到写入需要 fencing token。
上线前压测同 key 热点和不同 key 并行两种场景。监控获取成功率、等待 P95/P99、持锁时长、超时、续期失败、队列深度,并能定位 top lock key。
七、常见误区与追问
- 误区:加锁是最稳妥的并发方案。 锁自身有租约、网络分区和旧持有者恢复等失败模式。
- 误区:锁粒度越大越安全。 大锁扩大互相影响范围,却不能修复跨系统原子性。
- 误区:拿到锁就不需要幂等。 响应丢失和消息重投仍会触发同一业务再次执行。
- 误区:锁能代替数据库事务。 它不负责回滚临界区内已经提交的部分操作。
- 追问:什么时候锁是合理的? 冲突真实、临界区短、参与方受控且替代原子操作无法表达时。
- 追问:如何发现锁被滥用? 查全局 key、长持锁、锁内 RPC、高等待比和大量超时重试。
- 追问:热点资源无法拆锁怎么办? 接受其串行本质,用排队、合并写和容量保护稳定处理。
八、加强记忆
判断分布式锁可沿“不变量—原子能力—冲突粒度—临界区—故障语义”推进:先说清要守什么,再优先使用唯一约束、CAS、幂等和单写者;只有无法表达时才引入锁。用了锁也要同时保留存储约束与补偿,并用等待、持锁和热点指标证明它没有把系统悄悄串成单线程。