Appearance
数据库死锁和锁等待超时案例怎么复盘
数据库锁冲突类问题很容易把团队带进一种状态:
- 大家都知道“发生了死锁”
- 也知道“有锁等待超时”
- 但真正问到是哪两条 SQL、哪两个事务、为什么按这个顺序执行时,就说不清了
这类问题最怕的误区是:
- 只会调大超时
- 只会加重试
- 只会说“数据库有点慢”
这些动作顶多缓一阵,不能解决真正的并发冲突结构。
先说结论
- 复盘死锁和锁等待,关键不是只知道“谁报错了”,而是重建两个或多个事务的执行顺序
- 第一目标是先分清这是死锁、普通锁等待,还是慢 SQL 导致的长事务占锁
- 第二目标是找到持锁点、等待点和访问顺序是否一致
- 很多锁问题本质不是数据库参数,而是事务边界过大、索引不命中、更新顺序不一致
一个典型故障现场
一个很典型的场景是订单系统:
- 应用日志里开始出现 deadlock
- 一部分请求直接失败
- 另一部分请求长时间卡住,最后 lock wait timeout
- MySQL CPU 并不高,但接口 RT 已经明显抖动
这时如果只看“数据库响应慢”,很容易漏掉真正关键信息:
- 事务 A 先锁了表/索引上的哪一部分
- 事务 B 又锁了哪一部分
- 后面它们是怎样互相等待的
- 为什么这次才冲突,以前没有
第一阶段:先止血,避免冲突放大
1. 先确认失败范围和业务影响
要先分清:
- 是单个接口冲突
- 还是一组核心交易流程都在冲突
- 是偶发尖刺
- 还是持续复现
2. 必要时先降并发或切掉批量任务
很多锁冲突不是纯线上流量触发,而是:
- 定时任务批量更新
- 补偿任务扫表
- 大事务回写
- 运营后台批量操作
如果这些动作正在放大冲突,先停掉或降速,往往比继续盯数据库监控更有效。
3. 谨慎使用应用侧重试
死锁场景下,有限重试有时是合理的;但如果锁等待本来就严重,放大重试只会把数据库压得更乱。
第二阶段:先分清是死锁还是锁等待超时
1. 死锁
特点通常是:
- 两个或多个事务形成循环等待
- 数据库会主动回滚其中一个事务
- 应用收到 deadlock 报错
2. 锁等待超时
特点通常是:
- 一个事务一直等另一个事务释放锁
- 没形成循环
- 但等太久后超时失败
3. 长事务导致的“看起来像死锁”
有些问题并没有真正形成死锁,只是某个事务长时间持锁,后面一串请求都在排队。
先把这三类分清,后面的分析才不会跑偏。
第三阶段:复盘时最该还原的四件事
1. 哪几个事务参与了冲突
不是只看“哪个接口报错”,而是要看冲突链路里每个事务分别在做什么。
2. 它们按什么顺序访问数据
死锁最经典的根因之一就是访问顺序不一致:
- 事务 A 先改订单,再改库存
- 事务 B 先改库存,再改订单
并发一高,就很容易互相卡住。
3. SQL 有没有命中合适索引
没有命中索引时,锁范围往往会比你以为的大得多。
这也是很多“明明只改一行,为什么锁了一片”的根因。
4. 事务边界是不是太大
有些事务里混了:
- 远程调用
- 复杂计算
- 多次查询和更新
- 甚至用户态等待
结果锁持有时间被大幅拉长。
常见根因画像
1. 更新顺序不一致
这是死锁最典型的结构性根因。
2. 大事务或长事务
事务时间一长,锁冲突概率和影响范围都会明显放大。
3. 索引缺失或条件不精确
让更新、删除、扫描时锁住更多记录或更大范围。
4. 批量任务和在线请求抢同一批数据
非常常见,也最容易在高峰时突然爆。
5. 业务逻辑把“查 + 改 + 再查 + 再调外部接口”都塞进一个事务
这类设计在并发低时可能没问题,但一上量就容易放大锁问题。
一个更实用的复盘顺序
遇到这类问题,我通常按这个顺序复盘:
- 先确认报错类型,是 deadlock 还是 lock wait timeout。
- 再抓当时参与冲突的 SQL 和事务链路。
- 还原它们的访问顺序和持锁顺序。
- 看 SQL 是否走了正确索引,锁范围是否被意外放大。
- 看事务边界里有没有不必要的长逻辑。
- 最后再决定是调整 SQL、拆事务、统一更新顺序,还是改批量任务策略。
这样复盘出来的结论更容易真正指导后续改造,而不是只停在“数据库偶发冲突”。
止血后的治理方向
1. 统一核心资源访问顺序
尤其是订单、库存、账户、余额这类热点资源。
2. 缩小事务边界
能移出事务的逻辑尽量移出,别把远程调用和非关键步骤放进事务里。
3. 确保更新条件命中索引
避免扩大锁范围。
4. 批量任务错峰和限速
别让补偿、归档、批量变更和在线核心交易长期抢同一批资源。
总结
数据库死锁和锁等待超时真正要复盘的,不只是“谁报错了”,而是哪些事务在什么顺序下抢了哪些资源。
把“事务顺序、SQL 索引、锁范围、事务边界”这几条线理顺之后,死锁和锁等待问题通常都会从偶发故障,变成能被持续治理的结构性问题。