分布式锁没报错但吞吐变差,应该先查续约还是锁粒度?
分布式锁没有明显报错,不代表锁链路健康。吞吐下降时,把持锁时间、续约抖动、锁粒度、热点 key、下游等待和重试放到同一段时间窗里一起看,才能判断到底是锁续约不稳,还是锁设计本身把并发压扁了。
分布式锁最难排的,不是锁直接报错,而是系统慢下来时看上去一切都还在工作:请求也能拿到锁,服务也没挂,错误率也不高,但吞吐就是一格一格往下掉。
这种场景里,锁往往不是失效,而是在悄悄改写并发形态。要么持锁时间被下游和事务拖长,要么锁粒度或热点 key 把本来能并行的请求串成了一条线。
真实线上里,下面几类情况都会让系统在“锁没报错”的前提下吞吐明显变差:
- 锁粒度太粗,把本来能并行的请求都串起来了
- 热点 key 集中,少数业务键把大部分请求堵在一条线上
- 业务执行时间变长,导致持锁时间整体拉长
- 自动续约抖动,虽然不一定立刻报错,但会让持锁窗口更不稳定
- 重试、补偿、轮询等待把锁链路继续放大
现场真正需要回答的是:
吞吐下降时,系统到底是被续约和持锁窗口的不稳定拖住了,还是被锁粒度、热点 key、持锁代码本身压扁了并发?
排查这类问题时,我会把持锁时间、热点 key 分布、续约抖动、等待时长和下游执行链放进同一个时间窗里,再判断主因究竟更像续约问题,还是粒度问题。
先看是不是锁链路在压吞吐
| 你现在看到的现象 | 更适合先看哪条线 | 为什么 |
|---|---|---|
| 分布式锁没明显报错,但吞吐、排队、RT 在一起变差 | 先在这里拆清是续约抖动,还是锁粒度过粗 | 不先分这一步,后面很容易把 Redis 参数和业务模型搅在一起 |
| 你还没判断这个业务到底该不该上分布式锁 | 先看《分布式锁什么时候该用,什么时候不该用》 | 先决定是不是互斥问题,再谈锁内细节 |
| 你真正头疼的是重复消费、重复扣减、重复副作用 | 先看《幂等校验明明通过了,为什么消息还是会重复消费?》 | 那更像幂等边界,不是锁续约判断 |
| 你看到的是 MQ backlog 越堆越多,团队正在争论要不要扩消费者 | 先看《消息越堆越多,为什么常常不是消费者数量不够,而是下游变慢了?》 | 那更像 backlog 先被下游拖慢,不是锁本身这条线 |
| 锁竞争并不高,但线程池、连接池、下游等待一起变差 | 先看《锁竞争不高却吞吐掉得厉害,先查线程切换还是下游等待?》 | 那更像协作放大链,而不是锁本身异常 |
这篇主要收窄到哪一段
如果眼前的现象是锁没怎么报错,但 RT、排队和吞吐一起变差,可以先沿这篇往下拆。
这里不展开“这个场景是否该上分布式锁”,也不把重复副作用、MQ backlog、线程等待链混进来,只聚焦一个诊断价值更高的问题:
系统变慢,到底是续约和持锁窗口开始不稳,还是锁粒度、热点 key、持锁代码本身已经把并发压扁了?
把这两条线分开以后,后面的判断才不会停留在“锁还能用”这种没有诊断价值的话上。
把热点、持锁时长和等待时长对齐
- 先把
lock key维度的等待最久、排队最多的请求拎出来,看热点是不是集中在少数资源上。 - 再把业务执行耗时、持锁时长、等锁时长放在同一时间窗对比。
- 如果执行时间长期贴着 TTL 跑,再去看续约线程、Redis RT 和调度抖动。
- 如果持锁时间是跟着下游、事务、查库一起变长,不要急着把锅全甩给续约。
- 最后再决定主因更像“锁续约不稳”还是“锁粒度设计把并发压扁”。
一、为什么“没报错”反而最容易误导人
因为很多人默认认为分布式锁出问题一定会长成下面这样:
- 抢锁失败率暴涨
- Redis 报错
- 解锁失败
- 明显的锁超时异常
这些当然是典型问题,但在实际系统里,更常见的往往是“温和退化”。
1. 锁能拿到,但拿到之后持有太久
例如:
- 业务逻辑比以前更重
- 事务里夹了下游调用
- 持锁期间还要查库、调服务、发消息
- 单次执行时间从 50ms 变成 500ms、1s、3s
这时锁并没有报错,但单位时间能通过的请求数已经显著下降。
2. 锁设计太粗,把原本可以并行的请求都串行化了
例如:
- 本来只该锁同一个订单
- 最后却锁成了同一类订单
- 本来只该锁同一用户
- 最后却锁成了整个活动、整个租户、整个资源组
这时不会报错,但吞吐会非常诚实地掉下去。
3. 续约没有彻底失效,但已经开始不稳定
比如:
- Redis 抖一下
- GC 抖一下
- worker 被调度延迟
- 续约线程偶发延迟
结果可能不是立刻报“锁失效”,而是:
- 持锁窗口更不可预测
- 业务不得不更保守地重试和等待
- 吞吐慢慢掉
所以“没报错”最多只能说明:
- 系统还没掉到最坏形态
不能说明:
- 锁链路健康
- 并发设计没问题
- 吞吐下降和锁无关
二、先看锁到底把什么资源串成了一条线
如果现在就遇到这种问题,第一刀不要先切“续约还是粒度”,而是先回答下面三个问题:
- 同一时刻被串起来的是哪些请求?
- 持锁时间最近有没有明显变长?
- 热点是不是集中在少数 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 明显拉长
继续往下看:
- 等待最长的锁几乎都集中在少数热点商品 key 上
- 业务持锁时间从原来的 80ms 涨到 600ms
- 原因不是 Redis 抖,而是持锁代码里新增了一次下游库存校验 RPC
- 锁粒度按商品维度,而热点商品请求量极高
- 续约机制虽然没出明显异常,但已经开始更频繁地工作
这时真正的链路就很清楚:
- 主因不是续约失败
- 而是热点 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 只是把等待时间一起拉长。
如果已经拆到这里,下一刀往哪下
读到这里,如果你已经确认“锁没报错,但吞吐确实被压住了”,我更建议按现场里最强的信号继续拆,而不是继续泛化聊锁:
- 还没想清这个场景值不值得上分布式锁:先回《分布式锁什么时候该用,什么时候不该用》,把互斥边界先定住。
- 现场已经长得像重复消费、重复副作用或 ack 重投:直接转去《幂等校验明明通过了,为什么消息还是会重复消费?》,别继续把锅扣在续约上。
- 等待型瓶颈已经往线程池、连接池、MQ backlog 扩散:就接着看《锁竞争不高却吞吐掉得厉害,先查线程切换还是下游等待?》和《消息越堆越多,为什么常常不是消费者数量不够,而是下游变慢了?》。
- 你只是卡在热点 key、锁粒度和业务模型之间拿不准:先回业务资源边界,往往比继续调 TTL 更有用。
十、我会怎么接着往下看
这类“锁能拿到,但吞吐还是往下掉”的现场,后面通常就分成两组证据:一组继续看锁到底把谁串起来了,另一组看等待是不是已经扩到了别的链路。
如果我还要继续沿锁这条线看
如果我已经看到问题往外扩
真到下一轮排查时,我会先抓这 4 件事
- 先把这篇里的顺序立住:分布式锁问题不一定报错,先分锁粒度和续约抖动。
- 如果还在纠结该不该上分布式锁,先回互斥边界,别急着调参数。
- 如果等待已经传到线程池、连接池或数据库,主线就切到等待链,不要再只盯 Redis。
- 如果热点 key 特别集中,优先回业务模型和 key 设计,通常比参数优化更见效。
十一、最后总结:先别盯“报没报错”,先盯“并发是不是被锁压扁了”
分布式锁没报错但吞吐变差,这类问题最容易误导人的地方就在这里:
- 锁表面上还能工作
- 所以大家容易以为问题不在锁
但真实线上里,更常见的是:
- 锁粒度太粗
- 热点 key 太集中
- 下游和事务让持锁时间变长
- 续约抖动让窗口更脆弱
更有用的主线是:
先看 lock key 分布和持锁时间,再看 TTL 与执行时长的距离,最后把下游等待、数据库、事务边界和续约稳定性放回同一条证据链里。
只要这个顺序立住,很多原本会被一句“没报错”掩盖的分布式锁问题,就能重新收敛到真实的吞吐瓶颈上。