目标:你能把"锁等待/死锁"从概念背诵,提升到线上可落地:
- 如何快速判断是锁导致的慢
- 如何拿到等待链与死锁日志
- 如何通过索引与事务改造降低锁冲突
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 件事
- 哪两个事务在互相等待(事务 id/线程 id)
- 各自持有什么锁、在等什么锁(锁类型与索引)
- 哪条 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. 线上排查步骤(可直接照做)
- 确认现象:RT 高、连接数高、CPU 不高
- 看慢 SQL:是否集中在 update/delete
- 抓 InnoDB status:是否有死锁日志
- 查锁等待:谁在持锁、谁在等待、等待多久
- 回到 SQL:
- 是否扫描过多行(EXPLAIN rows)
- 是否事务太长
- 是否访问顺序不一致
6.1 更流程化的线上排查 checklist(从现象到根因)
- 先判断"慢"是不是锁等待
- 典型特征:CPU 不高、连接数上升、慢 SQL 耗时大但执行计划不差
- 固定证据
- 慢日志拿到 SQL + 参数
- 同时抓
SHOW ENGINE INNODB STATUS(看是否有 deadlock/等待片段)
- 找到"谁在持锁、谁在等待"
- 如果能用 performance_schema,就用它定位等待链与锁对象
- 回到 SQL 做三类归因
- 事务太长:把 RPC/循环/计算移出事务
- 锁范围太大:用索引把 where 精确化、减少扫描行
- 顺序不一致:统一按主键排序更新、固定多表更新顺序
- 决定临时止血动作(低峰再根治)
- 降级/限流/拆批
- 幂等更新可加重试(带退避)
7. 面试背诵稿(60 秒)
线上遇到 SQL 慢我会先区分是执行慢还是等锁慢:如果 CPU 不高但连接堆积、RT 飙升,很可能是锁等待。我会通过 SHOW ENGINE INNODB STATUS 看死锁日志,并结合 performance_schema 查看等待链与持锁事务。
降冲突的核心是三点:缩短事务时间(把 RPC/计算移出事务)、缩小锁范围(用合适索引让 where 精确命中、减少扫描行数)、以及统一加锁顺序避免交叉等待。对于不可避免的死锁可做幂等重试和退避,但根因还是要从事务边界、索引和批量操作策略上治理。