MySQL 锁等待与死锁进阶:怎么看等待、怎么降冲突(工程化套路)

目标:你能把"锁等待/死锁"从概念背诵,提升到线上可落地:

  • 如何快速判断是锁导致的慢
  • 如何拿到等待链与死锁日志
  • 如何通过索引与事务改造降低锁冲突

1. 先建立直觉:慢不一定是 SQL 慢,可能是"在等锁"

典型现象:

  • QPS 下降,RT 飙升
  • DB CPU 不高,但连接数变多
  • 慢 SQL 里耗时很大,但执行计划并不差

这种很可能是:

  • 锁等待(被别的事务占着)

1.1 一个可复现的最小例子:两条 update 就能制造死锁

准备表:

sql 复制代码
create table t_account (
  id bigint primary key,
  balance int not null
);

并发执行两段事务(注意更新顺序相反):

事务 T1:

sql 复制代码
begin;
update t_account set balance = balance - 1 where id = 1;
update t_account set balance = balance + 1 where id = 2;
commit;

事务 T2:

sql 复制代码
begin;
update t_account set balance = balance - 1 where id = 2;
update t_account set balance = balance + 1 where id = 1;
commit;

现象:

  • 其中一个事务会报 deadlock 并回滚
  • 另一个事务继续提交

直觉:

  • T1 先锁住 id=1,再等待 id=2
  • T2 先锁住 id=2,再等待 id=1
  • 形成循环等待

2. 锁等待从哪里看:三类证据

2.1 InnoDB 状态

  • SHOW ENGINE INNODB STATUS
    • 能看到 LATEST DETECTED DEADLOCK
    • 能看到部分锁等待信息

2.1.1 你真正需要从 deadlock 日志里读出 3 件事

  1. 哪两个事务在互相等待(事务 id/线程 id)
  2. 各自持有什么锁、在等什么锁(锁类型与索引)
  3. 哪条 SQL 导致的(定位到业务入口)

2.2 performance_schema(更体系化)

  • 能查到等待事件、锁对象、等待时长
  • 适合线上持续观测

2.3 慢日志与链路

  • 慢 SQL 不一定是"执行慢",可能是"等锁慢"
  • 需要把"等待时间"从总耗时中拆出来(APM 或数据库指标)

3. 死锁的本质:循环等待 + InnoDB 主动检测并回滚一方

死锁满足:

  • A 持有锁 1 等锁 2
  • B 持有锁 2 等锁 1

InnoDB 会检测到环,并选择回滚"代价较小"的事务(通常是修改行少的)。

4. 常见死锁场景(高频且可改造)

4.1 不同顺序更新同一组资源

事务 1:先更新 A 再更新 B

事务 2:先更新 B 再更新 A

解决:

  • 统一资源访问顺序(按 id 排序)
对照组
  • 错:顺序不一致(T1 更新 1->2,T2 更新 2->1)
  • 对:所有地方都按 id 从小到大更新(1->2),或按某个业务维度固定顺序

4.2 范围更新导致 Next-Key Lock 覆盖面大

  • update t set ... where idx_col between 10 and 20

在 RR 下可能加 next-key 锁,导致范围内插入/更新受阻。

解决:

  • 让条件更精确(尽量命中唯一键/主键)
  • 拆小批次,减少锁持有时间
对照组
  • 错:一次 update/delete 覆盖大范围,事务持续时间长
  • 对:按主键分批(例如每批 200/500),每批单独事务提交

4.3 二级索引更新 + 回表锁

  • 更新会锁索引记录 + 对应主键记录

如果条件走错索引或扫描范围大,会锁很多行,冲突飙升。

解决:

  • 用合适索引缩小扫描范围
  • 避免在热点表做"大范围 update/delete"
对照组
  • 错:where 条件导致扫描行数大,更新锁住大量索引记录与主键记录
  • 对:补齐合适索引,让 where 精确命中,减少锁范围与回表

5. 降锁冲突的工程方法(优先级从高到低)

5.1 缩短事务时间(最有效)

  • 把 RPC/外部调用移出事务
  • 事务内只做必要的 DB 操作
  • 避免事务里做复杂计算/循环

5.2 缩小锁范围(靠索引与写法)

  • where 尽量用主键/唯一键
  • 让扫描行数最小
  • 避免函数/类型转换导致索引失效

5.3 拆批处理

  • 大批量更新改成分批(每批 200/500)
  • 每批单独事务

5.4 统一加锁顺序

  • 多行更新时按主键排序
  • 多表更新时固定表顺序

5.5 降级/重试

  • 对幂等操作可做死锁重试(带退避)
  • 对不可重试操作要快速失败并告警

6. 线上排查步骤(可直接照做)

  1. 确认现象:RT 高、连接数高、CPU 不高
  2. 看慢 SQL:是否集中在 update/delete
  3. 抓 InnoDB status:是否有死锁日志
  4. 查锁等待:谁在持锁、谁在等待、等待多久
  5. 回到 SQL:
    • 是否扫描过多行(EXPLAIN rows)
    • 是否事务太长
    • 是否访问顺序不一致

6.1 更流程化的线上排查 checklist(从现象到根因)

  1. 先判断"慢"是不是锁等待
    • 典型特征:CPU 不高、连接数上升、慢 SQL 耗时大但执行计划不差
  2. 固定证据
    • 慢日志拿到 SQL + 参数
    • 同时抓 SHOW ENGINE INNODB STATUS(看是否有 deadlock/等待片段)
  3. 找到"谁在持锁、谁在等待"
    • 如果能用 performance_schema,就用它定位等待链与锁对象
  4. 回到 SQL 做三类归因
    • 事务太长:把 RPC/循环/计算移出事务
    • 锁范围太大:用索引把 where 精确化、减少扫描行
    • 顺序不一致:统一按主键排序更新、固定多表更新顺序
  5. 决定临时止血动作(低峰再根治)
    • 降级/限流/拆批
    • 幂等更新可加重试(带退避)

7. 面试背诵稿(60 秒)

线上遇到 SQL 慢我会先区分是执行慢还是等锁慢:如果 CPU 不高但连接堆积、RT 飙升,很可能是锁等待。我会通过 SHOW ENGINE INNODB STATUS 看死锁日志,并结合 performance_schema 查看等待链与持锁事务。

降冲突的核心是三点:缩短事务时间(把 RPC/计算移出事务)、缩小锁范围(用合适索引让 where 精确命中、减少扫描行数)、以及统一加锁顺序避免交叉等待。对于不可避免的死锁可做幂等重试和退避,但根因还是要从事务边界、索引和批量操作策略上治理。

相关推荐
心有—林夕2 小时前
MySQL 误操作恢复完全指南
android·数据库·mysql
夕除2 小时前
Mysql--15
java·数据库·mysql
野生技术架构师2 小时前
掌握SQL窗口函数,轻松处理复杂数据分析
数据库·sql·数据分析
会飞的大可2 小时前
NoSQL:从原理到实践的全景指南
数据库·nosql
刘~浪地球3 小时前
Redis 从入门到精通(四):字符串操作详解
数据库·redis·缓存
荒川之神3 小时前
MySQL 商品拉链表 完整最终版(配备了全套存储过程)
数据库·mysql
admin and root3 小时前
从资产收集FUZZ接口到SQL注入案例
网络·数据库·sql·安全·web安全·渗透测试·log4j
我真会写代码3 小时前
MySQL关键词全面总结(含用法+避坑指南)
数据库·mysql·索引
rainy雨3 小时前
精益数据分析系统功能拆解:如何用精益数据分析解决指标虚高难题与初创期验证场景
大数据·数据库·人工智能·信息可视化·数据挖掘·数据分析·精益工程