Java

分布式锁什么时候该用,什么时候不该用

分布式锁不是看到并发就该上的保险丝。真正要分清的是资源互斥、幂等控制、任务抢占还是吞吐治理,再决定它是不是那个值得长期维护的解法。

  • 分布式锁
  • Redis
  • Java
  • 后端架构
13 分钟阅读

分布式锁几乎是后端工程里最容易被“默认正确”的工具之一。只要一看到“并发”“重复执行”“多节点同时处理”,很多人第一反应就是:上个分布式锁。它当然有用,但也特别容易被滥用。因为一旦把问题抽象得不够清楚,分布式锁很容易从“解决并发互斥”的工具,变成“什么都想靠它兜一下”的万能胶。

真正危险的地方在于:分布式锁不是免费能力。你每加一把锁,实际上都在引入新的约束和新的失败模式:

  • 锁抢不到怎么办
  • 锁过期了怎么办
  • 业务执行时间超过预期怎么办
  • 锁释放失败怎么办
  • 服务重启、网络抖动、时钟问题会不会带来误判

所以比起问“这个场景能不能用分布式锁”,更值得先问的是:**这个问题本质上到底是资源互斥、状态幂等,还是任务协调?**不同问题,最优解并不一样。

我更想把分布式锁从“默认答案”里拽出来:它确实能解决跨节点互斥,但也会把 TTL、续约、误删、超时这些可靠性账一起带进来。所以别急着问能不能上,关键是想清这个问题到底配不配上。

这是不是分布式锁该解决的问题

你现在面对的问题更值得先看什么为什么
多节点会同时处理同一份资源,你要决定该不该上分布式锁直接往下看这篇重点就是把分布式锁适用边界说清楚
你真正担心的是重复请求、重复消费、结果只生效一次去看《幂等校验明明通过了,为什么消息还是会重复消费?这更像幂等约束,不是入口互斥
你已经用了锁,但现在是“锁没报错却吞吐掉”转去看《分布式锁没报错但吞吐变差,应该先查续约还是锁粒度?这已经是运行态问题,不是要不要上锁的问题
你看到的是 backlog 越来越大,团队想先加消费者去看《消息越堆越多,为什么常常不是消费者数量不够,而是下游变慢了?这更像消费吞吐判断,不是锁选型
你看到锁竞争不高,但线程池、连接池、下游一起拖吞吐去看《锁竞争不高却吞吐掉得厉害,先查线程切换还是下游等待?先把等待链拆开,比继续讨论锁更有收益

先把范围收在“要不要上锁”

如果你现在最卡的点是:多节点会不会同时处理同一份资源,我到底该不该上分布式锁,这篇值得先看。

它不展开回答下面这些已经进入运行态的问题:

  • 锁续约为什么抖
  • 锁没报错但吞吐为什么掉
  • backlog 为什么越积越多
  • 幂等明明做了,副作用还是没收住

它先帮你把一个前置判断做清楚:

你现在面对的,到底是不是“跨节点互斥”问题?如果不是,分布式锁大概率从第一步就不是最优解。

把这个边界划清,后面才不会把幂等、限流、调度和互斥混成一团。

如果你只想做 5 分钟判断

  1. 先判你面对的是“互斥”“幂等”“限流”还是“协调”。
  2. 再看数据库、消息模型、调度系统能不能直接表达这个约束。
  3. 如果真要上锁,必须先说清楚 lock key、资源边界、TTL 和失败语义。
  4. 如果拿不到锁后的业务动作都说不清,就说明方案还没收敛。
  5. 只有当问题明确是跨节点互斥时,才把分布式锁当候选答案。

一、先有一个基本认识:分布式锁解决的是“跨进程、跨节点的互斥访问”

在单机程序里,多个线程抢同一份资源时,可以用:

  • synchronized
  • ReentrantLock

但一旦系统变成多实例部署,问题就变了:

  • 请求可能落到不同节点
  • 定时任务可能被不同机器同时触发
  • 多个消费者可能同时处理同一类业务

这时候单机锁已经失效,因为它只能约束当前进程里的线程,约束不了别的机器。

分布式锁要解决的,就是:

在多个进程、多个节点之间,尽量保证同一时刻只有一个执行者进入某段关键逻辑。

所以它的核心目标是 跨节点互斥,不是“万能防重”。

二、什么时候该用:确实存在“同一份资源不能被多个节点同时处理”的约束

这是分布式锁最正统的使用场景。

例如:

  • 同一个订单只能有一个节点执行状态迁移
  • 同一个批任务在任一时刻只允许一个实例跑
  • 同一个用户账户在某类关键修改上不允许并发写冲突
  • 同一个库存修正任务不能被多机同时处理

这类场景为什么适合分布式锁

因为问题本质就是:

  • 多个节点都可能同时进来
  • 但资源不允许并行处理
  • 必须在入口处做互斥控制

如果这个互斥要求明确存在,而且很难下沉到数据库约束、消息模型或任务调度系统里,那分布式锁就是一个合理工具。

三、什么时候该用:需要多实例之间抢占“唯一执行权”

另一个很典型的场景,是任务抢占。

例如:

  • 某个定时任务部署在 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 贴边怎么办
  • 锁释放失败怎么办
  • 业务执行超过预期怎么办

这些如果没定义清楚,锁方案上线后很容易变成新的事故入口。

十三、最后总结:分布式锁该用在“跨节点互斥”最明确的地方,而不是所有并发问题上

分布式锁当然是有价值的工程工具,但它最合适的使用位置其实很明确:

  • 多节点
  • 同一资源
  • 明确互斥
  • 很难用更简单的数据约束或任务模型替代

如果一个问题本质上是幂等、限流、去重、状态约束或任务调度,那更自然的解法往往并不是分布式锁。

所以更值得记住的,不是“分布式锁怎么写”,而是这一句:

先判断你要解决的是互斥、幂等还是协调;只有当问题真正是跨节点互斥时,分布式锁才通常是那个顺手又合理的工具。

只要这条边界想清楚了,分布式锁就不会再沦为“什么都锁一下”的万能胶,而会回到它真正擅长的位置上。

所属专题

  • 并发控制、幂等与一致性设计

如果现场还没缩到同一类问题

如果锁只是表象,后面已经拖成排队和超时

读到这里,通常就剩下面这几条岔路

如果你已经确认问题不只是“要不要上锁”,而是现场卡在更具体的症状上,我一般会这样接着排: