Java

分布式锁没报错但吞吐变差,应该先查续约还是锁粒度?

分布式锁没有明显报错,不代表锁链路健康。吞吐下降时,把持锁时间、续约抖动、锁粒度、热点 key、下游等待和重试放到同一段时间窗里一起看,才能判断到底是锁续约不稳,还是锁设计本身把并发压扁了。

  • Java
  • 分布式锁
  • Redis
  • 并发
  • 性能排查
17 分钟阅读

分布式锁最难排的,不是锁直接报错,而是系统慢下来时看上去一切都还在工作:请求也能拿到锁,服务也没挂,错误率也不高,但吞吐就是一格一格往下掉。

这种场景里,锁往往不是失效,而是在悄悄改写并发形态。要么持锁时间被下游和事务拖长,要么锁粒度或热点 key 把本来能并行的请求串成了一条线。

真实线上里,下面几类情况都会让系统在“锁没报错”的前提下吞吐明显变差:

  • 锁粒度太粗,把本来能并行的请求都串起来了
  • 热点 key 集中,少数业务键把大部分请求堵在一条线上
  • 业务执行时间变长,导致持锁时间整体拉长
  • 自动续约抖动,虽然不一定立刻报错,但会让持锁窗口更不稳定
  • 重试、补偿、轮询等待把锁链路继续放大

现场真正需要回答的是:

吞吐下降时,系统到底是被续约和持锁窗口的不稳定拖住了,还是被锁粒度、热点 key、持锁代码本身压扁了并发?

排查这类问题时,我会把持锁时间、热点 key 分布、续约抖动、等待时长和下游执行链放进同一个时间窗里,再判断主因究竟更像续约问题,还是粒度问题。

先看是不是锁链路在压吞吐

你现在看到的现象更适合先看哪条线为什么
分布式锁没明显报错,但吞吐、排队、RT 在一起变差先在这里拆清是续约抖动,还是锁粒度过粗不先分这一步,后面很容易把 Redis 参数和业务模型搅在一起
你还没判断这个业务到底该不该上分布式锁先看《分布式锁什么时候该用,什么时候不该用先决定是不是互斥问题,再谈锁内细节
你真正头疼的是重复消费、重复扣减、重复副作用先看《幂等校验明明通过了,为什么消息还是会重复消费?那更像幂等边界,不是锁续约判断
你看到的是 MQ backlog 越堆越多,团队正在争论要不要扩消费者先看《消息越堆越多,为什么常常不是消费者数量不够,而是下游变慢了?那更像 backlog 先被下游拖慢,不是锁本身这条线
锁竞争并不高,但线程池、连接池、下游等待一起变差先看《锁竞争不高却吞吐掉得厉害,先查线程切换还是下游等待?那更像协作放大链,而不是锁本身异常

这篇主要收窄到哪一段

如果眼前的现象是锁没怎么报错,但 RT、排队和吞吐一起变差,可以先沿这篇往下拆。

这里不展开“这个场景是否该上分布式锁”,也不把重复副作用、MQ backlog、线程等待链混进来,只聚焦一个诊断价值更高的问题:

系统变慢,到底是续约和持锁窗口开始不稳,还是锁粒度、热点 key、持锁代码本身已经把并发压扁了?

把这两条线分开以后,后面的判断才不会停留在“锁还能用”这种没有诊断价值的话上。

把热点、持锁时长和等待时长对齐

  1. 先把 lock key 维度的等待最久、排队最多的请求拎出来,看热点是不是集中在少数资源上。
  2. 再把业务执行耗时、持锁时长、等锁时长放在同一时间窗对比。
  3. 如果执行时间长期贴着 TTL 跑,再去看续约线程、Redis RT 和调度抖动。
  4. 如果持锁时间是跟着下游、事务、查库一起变长,不要急着把锅全甩给续约。
  5. 最后再决定主因更像“锁续约不稳”还是“锁粒度设计把并发压扁”。

一、为什么“没报错”反而最容易误导人

因为很多人默认认为分布式锁出问题一定会长成下面这样:

  • 抢锁失败率暴涨
  • Redis 报错
  • 解锁失败
  • 明显的锁超时异常

这些当然是典型问题,但在实际系统里,更常见的往往是“温和退化”。

1. 锁能拿到,但拿到之后持有太久

例如:

  • 业务逻辑比以前更重
  • 事务里夹了下游调用
  • 持锁期间还要查库、调服务、发消息
  • 单次执行时间从 50ms 变成 500ms、1s、3s

这时锁并没有报错,但单位时间能通过的请求数已经显著下降。

2. 锁设计太粗,把原本可以并行的请求都串行化了

例如:

  • 本来只该锁同一个订单
  • 最后却锁成了同一类订单
  • 本来只该锁同一用户
  • 最后却锁成了整个活动、整个租户、整个资源组

这时不会报错,但吞吐会非常诚实地掉下去。

3. 续约没有彻底失效,但已经开始不稳定

比如:

  • Redis 抖一下
  • GC 抖一下
  • worker 被调度延迟
  • 续约线程偶发延迟

结果可能不是立刻报“锁失效”,而是:

  • 持锁窗口更不可预测
  • 业务不得不更保守地重试和等待
  • 吞吐慢慢掉

所以“没报错”最多只能说明:

  • 系统还没掉到最坏形态

不能说明:

  • 锁链路健康
  • 并发设计没问题
  • 吞吐下降和锁无关

二、先看锁到底把什么资源串成了一条线

如果现在就遇到这种问题,第一刀不要先切“续约还是粒度”,而是先回答下面三个问题:

  1. 同一时刻被串起来的是哪些请求?
  2. 持锁时间最近有没有明显变长?
  3. 热点是不是集中在少数 lock key 上?

为什么这一步最关键

因为它能先把问题粗分成两类:

更像锁粒度问题时

通常会看到:

  • 某类 lock key 流量高度集中
  • 大量请求都在等同一把或同一组锁
  • 锁等待比较稳定,但系统吞吐上不去
  • 业务成功率不一定低,只是越来越慢

更像续约 / 持锁不稳问题时

通常会看到:

  • 单次任务耗时和持锁时间波动很大
  • 同一个 key 有时很快,有时很慢
  • 续约延迟、超时边缘、重复执行、补偿重试开始增多
  • 某些窗口内锁表现明显抖动

也就是说,先看锁把谁串起来了,比先看 Redis 有没有报错更值钱。

三、更像锁粒度问题时,常见会有什么信号

这类问题在业务量上来后非常常见,而且往往比续约问题更高频。

1. 热点 key 非常集中

最典型的画像是:

  • 80% 的锁等待都集中在少数几个业务 key 上
  • 某个租户、活动、账户、商品、订单组明显比别的更慢
  • 不是系统整体都卡,而是热点局部把总体吞吐拖低

2. 单次业务执行本身没明显异常,但并发度上不去

例如:

  • 每次持锁执行耗时 100ms 左右
  • 看起来不算离谱
  • 但同一个 key 上每秒只能过 10 次
  • 请求一多就自然排起来

这类问题最本质的矛盾不是“执行出错”,而是:

  • 你的并发需求和锁粒度允许的并行度不匹配

3. 锁等待很稳定,但吞吐稳定偏低

如果你看到:

  • RT 变长
  • 等锁耗时可见
  • 但异常率不一定高
  • 续约也没明显抖

那更像锁粒度和 key 设计问题,而不是续约故障。

4. 业务上本来可以并行的资源被过度归并

例如:

  • 本来按订单 ID 锁就够了,却按用户 ID 锁
  • 本来按库存单元锁就够了,却按商品 ID 锁
  • 本来按任务分片锁就够了,却按整个任务类型锁

这类设计在低流量时往往没感觉,一到热点就会直接把吞吐压死。

四、更像续约问题时,常见会有什么信号

另一类问题是锁粒度本身不一定太粗,但持锁窗口变得不稳定。

1. 业务耗时接近或频繁越过 TTL 边缘

这是最典型的高危信号。

例如:

  • 锁 TTL 设 5s
  • 业务平时 500ms
  • 但高峰偶发会跑到 4s、5s、7s

这时只要续约稍有抖动,锁链路就会变得非常脆弱。

2. 续约线程、调度线程或 Redis 链路有偶发抖动

常见诱因包括:

  • GC 停顿
  • 线程池被占满
  • Redis RT 波动
  • 网络偶发抖动
  • 单独负责续约的线程没有及时执行

这类问题最麻烦的地方在于:

  • 它可能不天天爆
  • 也不一定每次都报明确错误
  • 但会让持锁窗口和执行行为变得很不稳定

3. 偶发重复执行、补偿增多或“明明成功却又重试”

这是很重要的间接证据。

因为很多续约抖动问题,外部第一眼看到的不是“续约失败”,而是:

  • 有些任务像是重进了一次
  • 有些任务开始多做补偿
  • 某些日志里出现重复抢占或重复提交痕迹

4. 持锁时间分布比业务本身耗时更离散

如果你把:

  • 业务执行耗时
  • 持锁总时长
  • 续约次数

放在一起看,发现持锁时间离散程度明显大于业务执行时间,那就要警惕续约抖动或锁释放不稳定。

五、真正更该看的,不是“锁成不成功”,而是“持锁时间结构”

分布式锁问题里最值钱的一组指标,通常不是简单的成功失败,而是:

  • 获取锁耗时
  • 持锁时长
  • 等锁时长
  • 同一 lock key 的并发等待数
  • 业务执行耗时和持锁耗时的差值
  • TTL 与真实执行时长的距离

为什么这些比错误率更重要

因为它们能直接回答:

  • 锁是不是把本来可并发的资源串起来了
  • 锁是不是被拿得越来越久
  • 持锁时间变长到底来自业务执行、下游等待,还是续约保守策略

而只看“锁有没有报错”,是回答不了这些问题的。

六、一条更实用的排查顺序

如果线上现在就碰到“分布式锁没报错但吞吐变差”,我更建议按下面顺序走。

第 1 步:先看 lock key 分布

重点看:

  • 等待最久的是哪些 key
  • 流量最集中的是否也是最慢的 key
  • 是否少数热点 key 吞掉了绝大多数等待时间

如果 key 极度集中,先优先怀疑粒度和热点设计。

第 2 步:对比业务执行时间和持锁时间

这一步很关键。

  • 如果两者一起变长,更像执行链变重或下游等待变多
  • 如果持锁时间明显比业务耗时更离散,更像续约、释放或等待策略问题

第 3 步:看 TTL 与真实执行时间是否过近

如果执行时间经常贴着 TTL 跑,续约问题迟早会出现,即使现在还没大量报错。

第 4 步:把下游和数据库一起拉进来

因为很多锁问题根本不是锁本身先坏,而是:

  • 下游变慢
  • 数据库等待变长
  • 事务变长
  • 导致持锁时间整体变长

这时锁只是把下游慢放大成了吞吐下降。

第 5 步:最后再看续约线程和 Redis 链路稳定性

重点查:

  • Redis RT 是否抖动
  • 续约执行线程是否被阻塞或饥饿
  • GC、线程池、调度延迟是否影响续约

如果前面几步还没解释清,再往这里收敛。

七、一个典型例子:为什么锁没报错,吞吐还是从 1200/s 掉到 300/s

假设某个扣减库存链路用了 Redis 分布式锁,现象是:

  • 错误日志很少
  • 抢锁成功率看着也不差
  • 但吞吐从 1200/s 掉到 300/s
  • 高峰期 RT 明显拉长

继续往下看:

  1. 等待最长的锁几乎都集中在少数热点商品 key 上
  2. 业务持锁时间从原来的 80ms 涨到 600ms
  3. 原因不是 Redis 抖,而是持锁代码里新增了一次下游库存校验 RPC
  4. 锁粒度按商品维度,而热点商品请求量极高
  5. 续约机制虽然没出明显异常,但已经开始更频繁地工作

这时真正的链路就很清楚:

  • 主因不是续约失败
  • 而是热点 key + 锁粒度偏粗 + 下游变慢
  • 三者叠加把并发压扁了

这个例子很能说明:

吞吐下降时,续约和粒度不是二选一凭感觉猜,而是要先看 key 分布和持锁时间结构。

八、关键误判:这类问题最容易在哪些地方走偏

误判 1:没有报错,就排除分布式锁问题

错。

很多锁问题首先表现为吞吐下降和排队变长,而不是异常爆炸。

误判 2:一看到锁等待,就默认先查续约

高频主因其实经常是锁粒度和热点 key 设计,而不是续约本身。

误判 3:只看锁成功率,不看持锁时间

成功率高不代表吞吐健康。只要持锁时间变长,系统照样会慢。

误判 4:只看 Redis,不看业务执行链

很多锁问题最后根因都不在 Redis,而在:

  • 下游慢
  • 数据库慢
  • 事务边界过大
  • 持锁代码做了太多事

误判 5:扩机器、加 worker,却不改 key 粒度

如果锁粒度本身就太粗,扩 worker 很多时候只会让更多请求更快排到同一把锁前面。

九、FAQ:值班时通常会继续追问的 4 个点

1. 锁没报错,但吞吐变差,还值得优先看锁吗?

值得。

尤其当你已经看到:

  • 等待时间上升
  • 热点 key 集中
  • 持锁时间变长

这时候锁就算没直接报错,也很可能已经把并发压扁了。

2. 怎么快速判断更像续约问题还是粒度问题?

有两件事最省时间:

  • lock key 是否高度集中
  • 持锁时间分布是否异常离散

前者更像粒度 / 热点,后者更像续约 / 持锁不稳。

3. 续约没失败,是不是就说明 TTL 没问题?

不是。

如果业务耗时经常贴着 TTL 跑,说明系统已经在脆弱边缘,续约只是暂时把风险托住。

4. 这类问题要不要先调大 TTL?

别一上来就调。

如果锁粒度本身太粗,TTL 再大只会让串行窗口更长;如果下游变慢,调大 TTL 只是把等待时间一起拉长。

如果已经拆到这里,下一刀往哪下

读到这里,如果你已经确认“锁没报错,但吞吐确实被压住了”,我更建议按现场里最强的信号继续拆,而不是继续泛化聊锁:

十、我会怎么接着往下看

这类“锁能拿到,但吞吐还是往下掉”的现场,后面通常就分成两组证据:一组继续看锁到底把谁串起来了,另一组看等待是不是已经扩到了别的链路。

如果我还要继续沿锁这条线看

如果我已经看到问题往外扩

真到下一轮排查时,我会先抓这 4 件事

  1. 先把这篇里的顺序立住:分布式锁问题不一定报错,先分锁粒度和续约抖动。
  2. 如果还在纠结该不该上分布式锁,先回互斥边界,别急着调参数。
  3. 如果等待已经传到线程池、连接池或数据库,主线就切到等待链,不要再只盯 Redis。
  4. 如果热点 key 特别集中,优先回业务模型和 key 设计,通常比参数优化更见效。

十一、最后总结:先别盯“报没报错”,先盯“并发是不是被锁压扁了”

分布式锁没报错但吞吐变差,这类问题最容易误导人的地方就在这里:

  • 锁表面上还能工作
  • 所以大家容易以为问题不在锁

但真实线上里,更常见的是:

  • 锁粒度太粗
  • 热点 key 太集中
  • 下游和事务让持锁时间变长
  • 续约抖动让窗口更脆弱

更有用的主线是:

先看 lock key 分布和持锁时间,再看 TTL 与执行时长的距离,最后把下游等待、数据库、事务边界和续约稳定性放回同一条证据链里。

只要这个顺序立住,很多原本会被一句“没报错”掩盖的分布式锁问题,就能重新收敛到真实的吞吐瓶颈上。