锁等待、热点行竞争把写接口拖慢时,应该先查什么?
当写接口变慢、读接口也开始被拖慢、连接池等待跟着升高时,不要只盯数据库 CPU。把“执行慢还是等待慢 -> 谁持锁太久 -> 热点行还是长事务”这几个问题依次拆开,才能更快把锁等待问题和应用层超时、排队串起来。
当你看到写接口 RT 抬高、事务接口偶发超时、连接池等待时间也开始变长时,第一反应通常会先去看数据库 CPU、慢 SQL 数量和 explain。但这类现场里,最容易被漏掉的一条主线其实是:数据库没有全面打满,不代表数据库等待链没有先把应用拖慢。
热点行竞争、锁等待、长事务和批量写操作,常常会先把请求拖进“等待型瓶颈”,然后沿着下面这条链一路放大:
热点行 / 锁等待 / 事务竞争 -> SQL 在执行前后花大量时间等待 -> 事务持有连接和锁的时间一起变长 -> 连接池归还速度下降 -> 请求线程和业务线程开始排队 -> 接口 RT 抬高、超时增多、重试继续放大
这篇文章不重复讲锁的基础概念,而是聚焦更贴近线上排障的问题:当锁等待、热点行和事务竞争已经把写接口拖慢时,应该怎么先分清方向、再找真正的持锁源头。
如果你还没分清是单接口慢、整组服务慢,还是数据库等待链已经外溢到应用层,可以先回到 接口响应慢怎么排查?后端 API 变慢与超时的定位步骤,或者先看 数据库没打满,为什么 API 和连接池已经开始变慢?。
锁等待把写接口拖慢时
下面几种现象同时出现时,这篇更适合你:
| 你现在看到的现象 | 更像什么 | 下一步 |
|---|---|---|
| 写接口、状态流转接口、扣减接口先变慢 | 热点写路径、事务竞争、锁等待 | 继续看本文 |
| 读接口也开始跟着慢,但数据库 CPU 不算高 | 锁等待已经外溢到连接池和线程排队 | 继续看本文,再联动连接池页 |
| 获取连接时间变长,活跃连接偏高 | 前面的事务把连接拿得更久了 | 看完本文后接 连接池等待时间变长时,如何判断是数据库慢还是应用拿着不放? |
| explain 看着还行,但高峰期 SQL 停留时间明显变长 | 更像等待慢,不只是执行慢 | 接 explain 看起来没问题,SQL 还是很慢,接下来该查什么? |
| 某个事务时间一直很长,提交很晚 | 长事务是上游放大器 | 接 事务执行时间过长,真正拖慢系统的往往不只是数据库 |
一、为什么锁等待问题最容易被误判成“数据库没事”
锁等待类问题最迷惑人的地方,在于它经常不长成“数据库资源全面打满”的样子。
你可能会看到这些现象:
- 数据库 CPU 中高,但没到 100%
- 磁盘 IO 也没有特别夸张
- explain 看起来还过得去
- 慢日志里不是所有 SQL 都慢得离谱
但与此同时,应用侧已经在报:
- 接口 RT 持续上升
- 获取连接变慢
- 写接口超时增多
- 线程池队列上涨
原因很简单:锁等待本质上是“等”,不是“算”。
事务拿到了连接,甚至已经跑到数据库里了,但它可能把大量时间花在:
- 等别的事务提交
- 等热点行释放锁
- 等某批批量更新结束
- 等长事务把资源释放回来
所以锁等待类问题经常会呈现一种“数据库没打满,但业务越来越慢”的矛盾感。
二、先看影响面和时间窗口,别急着翻 SQL 文本
遇到这类问题,第一步不要马上去翻某条 update 语句,先把影响范围看清。
至少先回答下面几个问题:
- 是只有写接口慢,还是读接口也被一起拖慢
- 是只有某一类业务键值慢,还是整组服务一起慢
- 是高峰期慢,还是某次发布后开始慢
- 是个别实例更严重,还是所有实例都在抖
1. 只有写接口明显变慢
更要优先怀疑:
- 热点状态流转
- 同一行反复更新
- 事务边界太大
- 同表的批量任务和在线请求撞车
2. 读接口也开始一起变慢
通常说明问题已经不只是单条更新语句慢,而是:
- 锁竞争把更多事务拖住
- 连接池开始紧张
- 线程排队把整组服务吞吐拉低
3. 只在高峰期明显变差
更像:
- 某个热点业务键在并发高时集中冲突
- 重试和补偿把竞争继续放大
- 请求数量一上来,锁等待从局部问题变成系统问题
4. 某次发布或某个任务窗口后开始变差
更要优先核对:
- 最近是否改了事务边界
- 是否新增了批量更新 / 对账 / 修复脚本
- 是否把下游调用放进了事务
- 是否把某个幂等更新改成了更高频的写路径
三、先把“执行慢”还是“等待慢”分清
锁等待、热点行和事务竞争最值钱的一步判断,就是把“慢”拆成两类。
1. 执行慢
更像是:
- SQL 本身扫描范围大
- 排序重
- 回表重
- 结果集过大
- 应用处理过重
2. 等待慢
更像是:
- SQL 本身不算很差,但在等锁
- 事务本身不算很重,但在等别的事务提交
- 连接已经借到了,但一直归还不回来
- 应用线程卡在数据库返回或事务提交上
如果本质是等待慢,你继续只盯 explain 或 CPU,很容易越查越偏。这个时候更应该把时间窗口内的锁等待、事务时长、连接持有时长和接口 RT 放到同一时间窗里看。
四、真正的慢链路,通常是这样一步步传导的
锁等待问题很少只停留在数据库内部。它更常见的传导链路是下面这样。
第 1 段:热点行开始被反复竞争
典型场景包括:
- 同一订单、库存、券状态被高并发更新
- 同一用户记录被短时间反复改写
- 计数器、余额、状态机类表行天然容易成为热点
- 多个请求都在
update ... where id = ?同一批主键
第 2 段:事务持锁时间变长
这时哪怕单条 SQL 本身不算特别复杂,只要:
- 前面的事务没及时提交
- 事务里夹了远程调用
- 批量处理没有拆小
- 某条事务被别的等待继续拖长
锁的持有时间就会被放大。
第 3 段:后续事务开始排队
后面来的请求会逐渐表现为:
- 同一条更新语句耗时飘得很厉害
- 同一接口时快时慢
- 高峰期大量请求卡在事务里
你这时看到的 SQL “慢”,很多已经不是执行慢,而是等待慢。
第 4 段:连接池和线程池一起被拖慢
事务等待时间一长,连接也会被拿得更久,于是会出现:
- 活跃连接越来越高
- 空闲连接越来越少
- 获取连接耗时变长
- 请求线程排队、业务线程池堆积
第 5 段:接口 RT 和重试继续放大问题
如果此时应用层还有:
- 超时重试
- MQ 重投
- 异步补偿
- 网关重放
原本局部的锁竞争,会继续被流量和重试放大成更大的拥塞。
五、排查时,先找等待者,再找持锁者
如果线上已经出现这类症状,我更建议按下面的顺序排。
第 1 步:先对齐接口 RT、错误率和连接池信号
先确认问题是不是已经从数据库内部传导到了应用层。
重点看:
- 哪些接口 RT 抬升最明显
- 是否集中在写路径或状态流转接口
- 获取连接耗时是否一起升高
- 连接池活跃连接和等待线程是否上涨
- 线程池队列是否也在涨
这一步的意义是确认:你处理的已经不是一条孤立 SQL,而是一条真实慢链路。
第 2 步:再看锁等待、活跃事务和长事务
这一步要尽量把下面几类信息放在一起看:
- 锁等待是否明显升高
- 活跃事务数量是否异常
- 是否出现长事务长期不提交
- 哪些表、哪些主键区间最容易冲突
如果你发现:
- 锁等待增长
- 活跃事务变多
- 某几个写接口同一时间窗口一起慢
那基本就要把排查重心放到事务竞争,而不是单纯慢 SQL 上了。
第 3 步:识别谁是“持锁者”,谁是“等待者”
这一步是很多团队最容易跳过的,但其实最关键。
你要尽量找清楚:
- 哪个事务先拿到了锁
- 它为什么拿了这么久
- 后面哪些请求在等它
- 这些等待是不是集中在某一类业务键值上
这里最常见的真实根因是:
- 某个事务里混进了外部调用
- 同一事务里做了循环更新或批量处理
- 批处理脚本一次锁住了太多热点记录
- 多个事务更新顺序不一致,彼此竞争加剧
第 4 步:回到代码边界,看事务里到底包了什么
锁等待问题最终还是要回到代码和业务语义里解释。
重点问这些问题:
- 事务里有没有 HTTP / RPC / MQ 回执等待
- 事务里有没有循环查库、循环更新
- 是否把原本可拆分的步骤全放进一个事务
- 是否把热点状态更新设计成所有请求都要争同一行
- 是否最近把幂等逻辑、乐观锁、状态机写法改重了
很多锁等待不是 DBA 视角里才看得懂的问题,而是代码边界本身就设计得太容易竞争。
第 5 步:最后再看是不是还叠加了慢 SQL 或实例资源问题
锁等待不等于数据库别的问题一定不存在。
如果同一时间窗口里还伴随:
- explain 正常但 SQL 仍然很慢
- 某些查询本身 rows 很高
- 报表或批量任务占用实例资源
- 连接池获取连接也在变慢
那说明锁等待可能只是链路里的一段,仍然要和下面几篇一起串起来看:
六、这类问题背后的常见根因,不只是“有锁竞争”这么简单
1. 热点行业务设计天然集中
最典型的是:
- 同一库存记录
- 同一优惠券 / 余额 / 配额记录
- 同一订单状态行
- 同一用户当天统计行
这类数据在高并发下天生容易变成热点,不是换个索引就能彻底解决。
2. 事务边界过大,把等待时间也包进去了
这类问题特别常见:
- 先改库,再调下游,再回库提交
- 在事务里等待外部确认
- 在事务里做大量对象计算或循环操作
最后锁拿住的时间比业务真正需要的时间长得多。
3. 批处理和在线写请求撞车
例如:
- 对账任务
- 修复脚本
- 批量状态迁移
- 夜间补偿任务
这些任务如果一次更新太大、持锁太久,很容易把线上请求一起拖慢。
4. 重试和补偿把竞争继续放大
热点写路径如果本来就紧张,再叠加:
- 应用超时重试
- 消息重复消费
- 网关或客户端重放
原本只是局部热点,最后会演变成整段链路的排队问题。
七、最容易出现的几个误判
误判 1:数据库 CPU 没打满,就说明瓶颈不在数据库
这在锁等待场景里经常是错的。
因为数据库这里的瓶颈不一定是计算瓶颈,也可能是等待瓶颈。
误判 2:只看 explain,觉得 SQL 没问题就结束排查
explain 解释不了谁在等锁,也解释不了谁把事务拖长了。
误判 3:连接池满了,就直接调大连接池
如果真正的问题是锁等待和事务竞争,扩连接只会让更多请求更快涌进同一段堵点。
误判 4:把所有责任都推给 DBA 或数据库参数
很多热点行问题的起点,其实在业务模型、事务边界和请求模式设计上。
误判 5:只盯单条慢 SQL,不看时间窗口和业务键值
热点冲突类问题往往高度依赖时间窗口、热点 key 或某类状态流转,不把这些维度拉出来,很难真正看懂。
八、FAQ:这类问题里最常被问到的几个问题
1. 锁等待一定会让数据库 CPU 很高吗?
不一定。
很多锁等待问题最明显的现象反而是:应用越来越慢,但数据库资源图没有那种“全面打爆”的感觉。
2. 为什么读接口也会被热点行竞争拖慢?
因为热点写事务一旦把连接、事务、线程排队一起拖长,读接口虽然不一定直接等同一把锁,也可能被共享连接池、共享线程池和同库等待链一起拖慢。
3. 同一条 SQL 有时候几十毫秒,有时候几秒,说明什么?
这通常就是等待型信号。
执行路径可能没怎么变,但它在某些时间窗口里卡在了锁等待或事务竞争上。
4. 锁等待和长事务是什么关系?
长事务往往是锁等待被放大的上游原因之一。事务提交越慢,锁持有时间越长,后面的事务越容易排队。
5. 这类问题第一反应应该先扩连接池还是先拆事务?
默认不要急着扩连接池。先确认连接为什么归还慢,再判断是数据库等待、事务边界过大,还是应用把连接拿着不放。大多数情况下,收短事务边界、拆热点写路径,收益比直接扩池更高。
九、锁等待坐实以后,下一步别只盯着锁本身
后面往哪边继续收,要看你现在掌握的证据更像哪一段链路:
- 还没先分清是不是数据库等待链:
数据库没打满,为什么 API 和连接池已经开始变慢? - 还没建立接口慢的总排查顺序:
接口响应慢怎么排查?后端 API 变慢与超时的定位步骤 - 更像 explain 正常但运行时还是慢:
explain 看起来没问题,SQL 还是很慢,接下来该查什么? - 更像事务边界过大、提交太晚:
事务执行时间过长,真正拖慢系统的往往不只是数据库 - 最先感知到的是连接池等待变长:
连接池等待时间变长时,如何判断是数据库慢还是应用拿着不放? - 如果 CPU、GC、数据库都不高,却还是像在等:
接口慢但 CPU、GC、数据库都不高,常见隐藏等待点有哪些?
我想强调的不是“锁等待很复杂”,而是别把它误当成一条孤立的数据库指标。真正能收口的,还是要回到谁把锁拿太久、热点为什么形成、事务为什么拖长,以及它怎么一路放大到连接池、线程池和接口 RT。把这条因果链接完整,修复动作才不会停在表面。