← 返回题目列表

为什么不能滥用分布式锁?

高频 中等 第 7 / 26 题 更新于 2026/10/01
分布式锁并发控制乐观锁吞吐
学习 AI 实战项目

简化版

分布式锁会把并行操作串行化,并引入网络、租约、续期和故障恢复成本;粒度过大还会造成热点与级联超时。能用唯一约束、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、幂等和单写者;只有无法表达时才引入锁。用了锁也要同时保留存储约束与补偿,并用等待、持锁和热点指标证明它没有把系统悄悄串成单线程。