分布式锁什么时候该用,什么时候不该用
分布式锁不是看到并发就该上的保险丝。真正要分清的是资源互斥、幂等控制、任务抢占还是吞吐治理,再决定它是不是那个值得长期维护的解法。
分布式锁几乎是后端工程里最容易被“默认正确”的工具之一。只要一看到“并发”“重复执行”“多节点同时处理”,很多人第一反应就是:上个分布式锁。它当然有用,但也特别容易被滥用。因为一旦把问题抽象得不够清楚,分布式锁很容易从“解决并发互斥”的工具,变成“什么都想靠它兜一下”的万能胶。
真正危险的地方在于:分布式锁不是免费能力。你每加一把锁,实际上都在引入新的约束和新的失败模式:
- 锁抢不到怎么办
- 锁过期了怎么办
- 业务执行时间超过预期怎么办
- 锁释放失败怎么办
- 服务重启、网络抖动、时钟问题会不会带来误判
所以比起问“这个场景能不能用分布式锁”,更值得先问的是:**这个问题本质上到底是资源互斥、状态幂等,还是任务协调?**不同问题,最优解并不一样。
我更想把分布式锁从“默认答案”里拽出来:它确实能解决跨节点互斥,但也会把 TTL、续约、误删、超时这些可靠性账一起带进来。所以别急着问能不能上,关键是想清这个问题到底配不配上。
这是不是分布式锁该解决的问题
| 你现在面对的问题 | 更值得先看什么 | 为什么 |
|---|---|---|
| 多节点会同时处理同一份资源,你要决定该不该上分布式锁 | 直接往下看这篇 | 重点就是把分布式锁适用边界说清楚 |
| 你真正担心的是重复请求、重复消费、结果只生效一次 | 去看《幂等校验明明通过了,为什么消息还是会重复消费?》 | 这更像幂等约束,不是入口互斥 |
| 你已经用了锁,但现在是“锁没报错却吞吐掉” | 转去看《分布式锁没报错但吞吐变差,应该先查续约还是锁粒度?》 | 这已经是运行态问题,不是要不要上锁的问题 |
| 你看到的是 backlog 越来越大,团队想先加消费者 | 去看《消息越堆越多,为什么常常不是消费者数量不够,而是下游变慢了?》 | 这更像消费吞吐判断,不是锁选型 |
| 你看到锁竞争不高,但线程池、连接池、下游一起拖吞吐 | 去看《锁竞争不高却吞吐掉得厉害,先查线程切换还是下游等待?》 | 先把等待链拆开,比继续讨论锁更有收益 |
先把范围收在“要不要上锁”
如果你现在最卡的点是:多节点会不会同时处理同一份资源,我到底该不该上分布式锁,这篇值得先看。
它不展开回答下面这些已经进入运行态的问题:
- 锁续约为什么抖
- 锁没报错但吞吐为什么掉
- backlog 为什么越积越多
- 幂等明明做了,副作用还是没收住
它先帮你把一个前置判断做清楚:
你现在面对的,到底是不是“跨节点互斥”问题?如果不是,分布式锁大概率从第一步就不是最优解。
把这个边界划清,后面才不会把幂等、限流、调度和互斥混成一团。
如果你只想做 5 分钟判断
- 先判你面对的是“互斥”“幂等”“限流”还是“协调”。
- 再看数据库、消息模型、调度系统能不能直接表达这个约束。
- 如果真要上锁,必须先说清楚
lock key、资源边界、TTL 和失败语义。 - 如果拿不到锁后的业务动作都说不清,就说明方案还没收敛。
- 只有当问题明确是跨节点互斥时,才把分布式锁当候选答案。
一、先有一个基本认识:分布式锁解决的是“跨进程、跨节点的互斥访问”
在单机程序里,多个线程抢同一份资源时,可以用:
synchronizedReentrantLock
但一旦系统变成多实例部署,问题就变了:
- 请求可能落到不同节点
- 定时任务可能被不同机器同时触发
- 多个消费者可能同时处理同一类业务
这时候单机锁已经失效,因为它只能约束当前进程里的线程,约束不了别的机器。
分布式锁要解决的,就是:
在多个进程、多个节点之间,尽量保证同一时刻只有一个执行者进入某段关键逻辑。
所以它的核心目标是 跨节点互斥,不是“万能防重”。
二、什么时候该用:确实存在“同一份资源不能被多个节点同时处理”的约束
这是分布式锁最正统的使用场景。
例如:
- 同一个订单只能有一个节点执行状态迁移
- 同一个批任务在任一时刻只允许一个实例跑
- 同一个用户账户在某类关键修改上不允许并发写冲突
- 同一个库存修正任务不能被多机同时处理
这类场景为什么适合分布式锁
因为问题本质就是:
- 多个节点都可能同时进来
- 但资源不允许并行处理
- 必须在入口处做互斥控制
如果这个互斥要求明确存在,而且很难下沉到数据库约束、消息模型或任务调度系统里,那分布式锁就是一个合理工具。
三、什么时候该用:需要多实例之间抢占“唯一执行权”
另一个很典型的场景,是任务抢占。
例如:
- 某个定时任务部署在 5 台机器上
- 但每次调度时只能有 1 台真正执行
或者:
- 某个补偿任务允许多实例待命
- 但任一时刻只能有一个 worker 拿到执行资格
为什么这类场景适合分布式锁
因为这里的核心问题不是数据一致性,而是:
- 谁来执行这次任务
- 如何防止多节点重复执行
这种“抢唯一资格”的语义,和分布式锁天然比较贴合。
四、什么时候不该用:只是想做幂等控制时,不一定先上锁
很多人一看到“重复请求”就想加分布式锁,但这不一定是最优解。
例如:
- 用户重复提交订单
- 消息重复消费
- 接口重复回调
这类问题很多时候更接近 幂等问题,而不是“互斥访问问题”。
幂等问题更适合的思路通常是
- 唯一业务键约束
- 幂等号
- 去重表
- 状态机判断
- 消息去重机制
为什么不一定先上锁
因为你的目标不是“让同一时刻只有一个请求进来”,而是:
- 即使来了多个重复请求
- 系统最终也只处理一次
这两者差别很大。
分布式锁解决的是入口互斥;幂等解决的是结果一致。很多业务上真正需要的是后者。
五、什么时候不该用:数据库本身就能天然表达约束时
有些问题看起来像并发控制,实际上数据库已经更擅长处理。
例如:
- 一个用户只能领取一次优惠券
- 某个唯一业务编号不能重复插入
- 某个状态迁移必须满足当前状态前置条件
这类场景很多时候更自然的做法是:
- 唯一索引
- 乐观锁版本号
update ... where status = ?- 条件更新
为什么这通常比分布式锁更稳
因为约束直接落在数据层:
- 成功或失败由数据库原子保证
- 不依赖额外锁服务状态
- 不容易出现“拿到锁了但数据还是错”的双重复杂性
一个很实用的原则是:
如果业务约束本来就能在数据库原子表达,优先考虑把约束放在数据库,而不是先加一层分布式锁。
六、什么时候不该用:只是想限制并发量,而不是保护同一资源
还有一类误用很常见:
- 系统太忙了
- 想限制并发
- 于是上分布式锁
但这里的问题往往不是“同一份资源不能并发处理”,而是:
- 整体流量太大
- 某类任务太多
- 下游承载能力有限
这类更适合的工具通常是:
- 限流
- 信号量
- 队列削峰
- 线程池隔离
- 熔断和降级
为什么不该直接上锁
因为分布式锁强调的是“互斥”,而不是“限流”。
如果你的目标只是控制系统整体吞吐,锁往往既不优雅,也会让语义变得奇怪。
七、什么时候不该用:锁的粒度和业务边界都说不清时
这是很重要但经常被忽视的一点。
分布式锁不是说“我加一把 lock 就好了”,你必须先回答:
- 锁的 key 到底是什么
- 锁保护的是哪一类资源
- 多大范围内互斥才合理
- 锁持有多久算正常
如果这些问题答不清
很容易出现:
- 锁粒度太粗,正常并发都被误伤
- 锁粒度太细,根本起不到保护作用
- 锁作用范围和业务语义对不上
例如:
- 你本来只是要保护“同一个订单”
- 却加成了“整个订单模块只有一个请求能进”
这种设计在低并发时可能看不出来,到了线上就会直接把系统吞吐压死。
所以一旦锁 key 设计模糊,先别急着上锁,先把业务互斥边界想清楚。
八、为什么分布式锁经常让问题从“业务并发”变成“系统可靠性”问题
很多人低估分布式锁,是因为只看到了“抢锁成功就执行”这一面,没看到后面的失败模式。
例如你一旦用了 Redis 分布式锁,就得继续考虑:
- 锁超时时间怎么定
- 业务执行时间超过 TTL 怎么办
- 锁续期失败怎么办
- 客户端拿到锁后服务挂了怎么办
- 锁释放时如何避免误删别人持有的新锁
这意味着什么
意味着分布式锁不是单纯一个 API,而是一整套可靠性设计题。
所以如果某个问题本来能用:
- 数据库约束
- 幂等控制
- 消息串行消费
- 任务调度平台
更自然地解决,那通常比硬上分布式锁更稳。
九、一个更实用的判断方法:先问这 4 个问题
如果你在犹豫要不要上分布式锁,可以先问自己这 4 个问题。
1. 这是“互斥问题”还是“幂等问题”
- 如果是同一资源不能同时处理,更像互斥
- 如果是重复请求结果要一致,更像幂等
2. 这个约束能不能直接在数据库表达
- 唯一索引
- 条件更新
- 乐观锁
如果能,通常优先考虑数据层原子约束。
3. 是否真的存在跨节点竞争
如果只是单机内部并发,用本地锁可能就够了;别把单机问题硬升级成分布式问题。
4. 锁失败后的业务语义是什么
- 返回稍后重试
- 排队等待
- 直接丢弃
- 降级处理
如果连拿不到锁后的业务语义都没想清楚,往往说明锁方案本身也还没成熟。
十、几个典型场景,分别更适合什么
场景 1:定时任务多实例部署,但只允许一个实例执行
这类通常适合分布式锁。
场景 2:用户重复点击“提交订单”
更适合幂等号、唯一业务键、订单状态约束,不一定先上锁。
场景 3:扣库存时避免超卖
优先考虑数据库原子更新、乐观锁或库存扣减模型。分布式锁有时能用,但通常不是首选。
场景 4:同一消息可能重复消费
优先考虑消息幂等和消费去重,不一定先上分布式锁。
场景 5:某个大任务在多 worker 中只能有一个执行者
这类通常比较适合分布式锁或任务协调机制。
十一、几个非常常见的误区
1. 看到“重复”就先想到分布式锁
重复不一定是互斥问题,很多时候更像幂等问题。
2. 觉得加了锁,业务就一定正确
锁只是限制并发入口,不替你自动保证业务结果绝对正确。
3. 锁粒度拍脑袋定
粒度太粗伤吞吐,粒度太细伤正确性。
4. 不考虑锁超时和业务执行时间
这在线上是高频事故来源。
5. 把分布式锁当成架构万能胶
很多场景更适合数据库、消息队列、调度平台或状态机。
十二、最后留一份够用的判断 checklist
以后再碰到“要不要上分布式锁”,可以沿着这条线过一遍:
第 1 步:先判定问题类型
- 跨节点互斥
- 幂等控制
- 限流削峰
- 任务协调
第 2 步:看有没有更自然的原子约束手段
- 唯一索引
- 乐观锁
- 条件更新
- 消息串行化
- 调度平台
第 3 步:如果真要上锁,先定义清楚
- 锁 key 是什么
- 锁保护的资源边界是什么
- TTL 多长
- 续期怎么做
- 锁失败后业务怎么处理
第 4 步:最后再评估复杂度值不值得
- 引入锁后是否真的比别的方案更简单
- 是否把问题从业务并发转成了系统可靠性问题
FAQ:分布式锁选型里最常被追问的几个问题
1. 同一个接口会被重复请求,就一定要上分布式锁吗?
不一定。
如果你的目标是“结果只生效一次”,通常更该先看幂等键、唯一约束、状态机,而不是先上锁。
2. 单机本地锁已经不够用了,是不是就该上分布式锁?
也不一定。
还要先确认是不是确实存在跨节点竞争,以及这个约束能不能直接落在数据库、消息串行化或调度平台上。
3. 扣库存、扣余额这类高并发写场景,分布式锁是不是最稳?
通常不能直接下这个结论。
很多时候数据库原子更新、乐观锁、条件更新会更自然;分布式锁可能能用,但不一定是第一选择。
4. 如果最后决定用分布式锁,最容易漏掉什么?
最容易漏掉的是失败语义:
- 抢不到锁怎么办
- TTL 贴边怎么办
- 锁释放失败怎么办
- 业务执行超过预期怎么办
这些如果没定义清楚,锁方案上线后很容易变成新的事故入口。
十三、最后总结:分布式锁该用在“跨节点互斥”最明确的地方,而不是所有并发问题上
分布式锁当然是有价值的工程工具,但它最合适的使用位置其实很明确:
- 多节点
- 同一资源
- 明确互斥
- 很难用更简单的数据约束或任务模型替代
如果一个问题本质上是幂等、限流、去重、状态约束或任务调度,那更自然的解法往往并不是分布式锁。
所以更值得记住的,不是“分布式锁怎么写”,而是这一句:
先判断你要解决的是互斥、幂等还是协调;只有当问题真正是跨节点互斥时,分布式锁才通常是那个顺手又合理的工具。
只要这条边界想清楚了,分布式锁就不会再沦为“什么都锁一下”的万能胶,而会回到它真正擅长的位置上。
所属专题
- 并发控制、幂等与一致性设计
如果现场还没缩到同一类问题
- 现场其实还是单机并发或本地临界区,我会先回到 synchronized 和 ReentrantLock 有什么区别?项目里该怎么选。
- 真正卡在结果一致性、事务边界或删缓存后的时序,就别再围着锁打转了,直接看 缓存一致性问题为什么总在删缓存、改数据库之后出事? 和 Spring 事务为什么会失效?常见场景汇总。
如果锁只是表象,后面已经拖成排队和超时
- 锁持有过长已经传导成排队、超时或接口抖动时,我通常会把 线程池打满以后,应该先查队列、拒绝策略还是慢任务?、数据库连接池打满时,根因通常不是连接数太小 和 接口响应慢怎么排查?后端性能问题定位步骤 放在一起对。
读到这里,通常就剩下面这几条岔路
如果你已经确认问题不只是“要不要上锁”,而是现场卡在更具体的症状上,我一般会这样接着排:
- 核心矛盾是重复消费、重复副作用、ack 重投:去《幂等校验明明通过了,为什么消息还是会重复消费?》
- 核心矛盾是锁已经上了,但吞吐、热点 key、持锁时间开始变差:去《分布式锁没报错但吞吐变差,应该先查续约还是锁粒度?》
- 核心矛盾是 backlog 压不下去,团队在争论要不要扩消费者:去《消息越堆越多,为什么常常不是消费者数量不够,而是下游变慢了?》
- 核心矛盾是线程、连接、下游一起把吞吐拖差,但锁竞争并不高:去《锁竞争不高却吞吐掉得厉害,先查线程切换还是下游等待?》