📖 本篇简介:Deadlock found when trying to get lock、Lock wait timeout exceeded是高并发业务里最常见的两类锁报错。死锁和锁等待不是一回事:死锁是两个事务互相等、MySQL 主动牺牲一个;锁等待是一个事务长时间占着锁、另一个干等超时。本篇用 10 个现场场景带你学会抓死锁日志、看懂 InnoDB 状态、定位谁堵谁、复现交叉更新与间隙锁死锁,并给出止血与治理手段。示例基于 MySQL 8.0,学完你能在面试中把死锁问题讲出"日志级"的细节。
目录
- 1 本篇导读与锁问题排查总览
- 2 场景一:业务报 Deadlock found 如何抓取死锁现场
- 3 场景二:SHOW ENGINE INNODB STATUS 死锁日志怎么读
- 4 场景三:锁等待超时 Lock wait timeout 如何定位谁堵谁
- 5 场景四:两事务交叉更新导致死锁如何复现与避免
- 6 场景五:间隙锁 Next-Key Lock 导致的插入死锁
- 7 场景六:批量更新按主键乱序引发的死锁
- 8 场景七:高并发下 UPDATE 死锁如何加索引与调整顺序
- 9 场景八:线上突发死锁风暴如何止血与降级
- 10 场景九:如何区分死锁与长事务锁等待
- 11 场景十:锁监控与死锁治理长效机制
- 12 死锁排查速查表
- 13 常见坑与注意事项
- 14 练习题与自测
1 本篇导读与锁问题排查总览
锁问题分两大类,排查思路完全不同,面试时先说清这个区分,就赢了一半:
- 死锁(Deadlock):两个或多个事务互相持有对方需要的锁,形成循环等待。MySQL InnoDB 有死锁检测(
innodb_deadlock_detect=ON),会主动回滚其中一个代价小的事务,报错给客户端。死锁是"瞬时"的,回滚后另一个事务就能继续。 - 锁等待(Lock Wait Timeout):一个事务持锁不放,另一个事务等超过
innodb_lock_wait_timeout(默认 50 秒)后超时回滚。它不是循环,是"单方向排队"。
排查锁问题的三件套:
-- 本示例基于 MySQL 8.0
-- 1. 看 InnoDB 最近一次死锁的完整记录(只保留最近一次,要持续抓得开 log)
SHOW ENGINE INNODB STATUS;
-- 2. 看当前正在等待锁的事务(性能模式 / sys schema)
SELECT * FROM sys.innodb_lock_waits;
-- 3. 把最近的死锁日志完整输出(8.0 支持把死锁日志落到 error log)
SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';
下面 10 个场景,就是围绕"抓现场 → 读日志 → 定位谁堵谁 → 复现 → 止血 → 治理"展开。
2 场景一:业务报 Deadlock found 如何抓取死锁现场
场景描述:应用日志里出现 Deadlock found when trying to get lock; try restarting transaction,但这个报错一闪而过,你登录数据库时现场已经没了。怎么抓到死锁发生时的完整现场?
考察点:考察死锁日志的持久化能力——默认 SHOW ENGINE INNODB STATUS 只保留最近一次死锁,错过就没了。
解答思路:
第一步,立刻执行 SHOW ENGINE INNODB STATUS,在 LATEST DETECTED DEADLOCK 段落里能看到最近一次死锁的双方 SQL 和持锁情况。但它只保留最近一次,如果死锁发生在几分钟前、中间又有别的死锁,就被覆盖了。
第二步,打开死锁日志持久化,让之后每次死锁都写进 error log:
-- 本示例基于 MySQL 8.0
-- 把所有死锁都打印到 error log,事后可以完整回溯
SET GLOBAL innodb_print_all_deadlocks = ON;
-- 这个参数建议长期开启,死锁量通常不大,开销可忽略
第三步,如果想在应用层捕获,业务代码要对死锁做"重试"——因为死锁被回滚的那个事务本身是无辜的,重试一次大概率成功:
# 本示例为 pymysql 连接示范(无真实服务器,仅演示重试写法)
import pymysql, time
def update_with_retry(conn, sql, params, max_retry=3):
for i in range(max_retry):
try:
with conn.cursor() as cur:
cur.execute(sql, params)
conn.commit()
return
except pymysql.err.OperationalError as e:
# 1213 是死锁错误码,业务侧必须重试
if e.args[0] == 1213:
time.sleep(0.05 * (i + 1)) # 退避后重试
continue
raise
追问:死锁为什么建议重试而不是直接报错给用户?
因为死锁是并发调度的偶发结果,不是业务逻辑错误;被牺牲的事务换个时机再跑一次就能成功,直接抛错会让用户偶发性失败,体验很差。
3 场景二:SHOW ENGINE INNODB STATUS 死锁日志怎么读
场景描述:你抓到了一段 LATEST DETECTED DEADLOCK 输出,但里面一堆 RECORD LOCKS、heap no、lock_mode X,看不懂。怎么从这段日志里读出"谁持了什么锁、谁在等什么锁"?
考察点:考察死锁日志的结构化阅读能力,这是死锁排查的硬功夫。
解答思路:
死锁日志分三段:WE ROLL BACK TRANSACTION(牺牲者)、两个事务各自的 TRANSACTION 段、以及各自持锁/等待锁的记录。关键看这几行:
*** (1) TRANSACTION: -- 事务1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED: -- 事务1 在等什么锁
*** (2) TRANSACTION: -- 事务2
*** (2) HOLDS THE LOCK(S): -- 事务2 持有哪些锁
*** (2) WAITING FOR THIS LOCK TO BE GRANTED: -- 事务2 又在等什么锁
读日志时抓三个要点:
1. 等锁的资源:RECORD LOCKS space id ... page no ... n bits ... index idx_xxx of table db.t,看清楚是哪个索引上的哪条记录;
2. 锁模式:lock_mode X(X 锁)、lock_mode X gaps(间隙锁)、lock_mode S;
3. 双方 SQL:每个事务段末尾有语句,把两条 SQL 拼起来,死锁的原因就清楚了。
-- 本示例基于 MySQL 8.0
-- 除了看文本日志,也可以查性能模式表,拿到更结构化的死锁信息
SELECT * FROM performance_schema.data_locks; -- 当前所有锁
SELECT * FROM performance_schema.data_lock_waits; -- 锁等待关系
data_locks 和 data_lock_waits 是 8.0 里替代老 information_schema.INNODB_LOCKS 的视图,能直接看到"谁等谁"的有向图。
举一段真实死锁日志的关键片段,带读一遍(已脱敏):
*** (1) TRANSACTION:
TRX 120843, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 2 row lock(s)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 112 page no 4 n bits 72 index PRIMARY of table `shop`.`orders`
trx id 120843 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRX 120844, ACTIVE 0 sec starting index read
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS ... index PRIMARY of table `shop`.`orders` lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS ... index PRIMARY ... lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (1)
读法:事务 1 在等主键索引上的一把 X 记录锁;事务 2 正握着这把锁、又在等事务 1 手里的另一把锁;InnoDB 判定事务 1 回滚代价小,于是回滚它。看到这里,把两边的 SQL 拿出来一比,交叉更新的死锁就实锤了。
追问:为什么死锁日志里有时候看到的是 gap lock(间隙锁)而不是记录锁?
因为 InnoDB 在 RR 隔离级别下用 Next-Key Lock(记录锁 + 间隙锁)防止幻读,范围查询会锁住一段间隙,两个事务锁间隙的方向相反时就会死锁。
4 场景三:锁等待超时 Lock wait timeout 如何定位谁堵谁
场景描述:业务报 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction,某个更新语句卡了 50 秒才超时。这不是死锁(没有循环),是有人占着锁不放。怎么快速定位"是谁堵住了谁"?
考察点:考察锁等待的实时定位,sys schema 锁等待视图的使用。
解答思路:
第一步,用 sys.innodb_lock_waits 一把看清等待关系:
-- 本示例基于 MySQL 8.0
-- 这条视图直接告诉你:等待的会话、被谁堵、堵了多久、持锁 SQL 是什么
SELECT * FROM sys.innodb_lock_waits\G
-- 重点列:
-- waiting_pid 等锁的连接 id
-- blocking_pid 堵人的连接 id
-- wait_elapsed_secs 已经等了多少秒
-- waiting_query 等锁的那条 SQL
-- blocking_query 持锁的那条 SQL(谁是元凶)
第二步,拿到 blocking_pid 后,去查这个持锁事务的完整信息:它是谁、从哪来、跑了多久、事务里做了什么。
-- 本示例基于 MySQL 8.0
-- 根据 blocking_pid 查这个事务的详情
SELECT trx_id, trx_state, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_age_sec, -- 事务开了多久
trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;
-- trx_age_sec 很大的那个,就是长事务元凶
第三步,如果确认是一个失控的长事务(比如应用忘了 commit、或者大 DML),可以 KILL blocking_pid 把它杀掉,释放锁。但 KILL 之前要评估:这个长事务可能回滚很久。
追问:锁等待和死锁在处理上最大的区别是什么?
死锁是 MySQL 主动牺牲一个、瞬时解决;锁等待需要你主动找到持锁的长事务并处理(KILL 或催它提交),它不会自动消失。
5 场景四:两事务交叉更新导致死锁如何复现与避免
场景描述:系统里最经典的死锁——事务 A 先更新表 1 再更新表 2,事务 B 先更新表 2 再更新表 1,高并发下偶发死锁。怎么复现?怎么从根上避免?
考察点:考察最经典的交叉更新死锁,以及"统一加锁顺序"这个核心解法。
解答思路:
这是教科书级死锁,两个会话按相反顺序访问同样两行:
-- 本示例基于 MySQL 8.0
-- 会话 A:先锁行 1,再去锁行 2
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1; -- A 拿到 id=1 的行锁
UPDATE account SET balance = balance + 100 WHERE id = 2; -- A 等 id=2 的锁
-- 会话 B:先锁行 2,再去锁行 1(与 A 顺序相反)
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 2; -- B 拿到 id=2 的行锁
UPDATE account SET balance = balance + 100 WHERE id = 1; -- B 等 id=1 的锁
-- 此时 A 等 B 的 id=2,B 等 A 的 id=1,循环等待 → 死锁,MySQL 回滚一个
根因不是"并发太高",而是多个事务加锁的顺序不一致。解法:约定全局统一的加锁顺序。比如所有事务都按 id 升序加锁,就不会出现循环等待:
-- 本示例基于 MySQL 8.0
-- 正确写法:转账前先把两个 id 排序,永远从小到大加锁
-- 比如从 id=5 转给 id=3,也要先锁 id=3 再锁 id=5
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 3; -- 先锁小 id
UPDATE account SET balance = balance + 100 WHERE id = 5; -- 再锁大 id
COMMIT;
工程上可以在业务层统一封装一个"按主键升序加锁"的转账方法,从源头消灭交叉顺序。
追问:统一加锁顺序就能完全避免死锁吗?
能避免"交叉更新"这类死锁,但避免不了间隙锁、唯一索引冲突等其他类型的死锁,所以仍要保留死锁重试兜底。
6 场景五:间隙锁 Next-Key Lock 导致的插入死锁
场景描述:业务里两个事务都是 SELECT ... WHERE id BETWEEN 10 AND 20 FOR UPDATE,然后各自 INSERT 一条记录,结果死锁了。都是范围加锁,为什么互相卡?
考察点:考察 Next-Key Lock(记录锁 + 间隙锁)在 RR 级别下的行为,这是死锁里最难懂的一类。
解答思路:
在可重复读(RR)隔离级别下,InnoDB 为了防止幻读,范围查询用的不是单纯记录锁,而是 Next-Key Lock——它会锁住记录本身,以及记录前面的间隙。两个事务都用 FOR UPDATE 锁住了 (10, 20) 这段间隙,谁都不允许对方在这个间隙里插入:
-- 本示例基于 MySQL 8.0
-- 会话 A:锁住 10~20 的间隙(Next-Key Lock)
BEGIN;
SELECT * FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE; -- 锁住这段区间
-- 会话 B:同样锁住这段间隙
BEGIN;
SELECT * FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE; -- 也想锁这段区间
-- A 想在间隙里插入 id=15:要等 B 释放间隙锁
INSERT INTO t(id) VALUES(15);
-- B 也想在间隙里插入 id=16:要等 A 释放间隙锁
INSERT INTO t(id) VALUES(16);
-- 互相等对方的间隙锁 → 死锁
解法有几种:
1. 降低隔离级别:改成 RC(读提交)级别,Next-Key Lock 退化为记录锁,间隙锁大幅减少(互联网公司很多库直接用 RC);
2. 缩小范围:把范围查询改成等值查询,等值查询走唯一索引时 Next-Key Lock 退化为记录锁;
3. 业务上避免大区间 `FOR UPDATE`:只锁真正要改的那几行。
-- 本示例基于 MySQL 8.0
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 互联网常见做法:在线把隔离级别降到 RC(需评估业务是否依赖 RR)
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
追问:RC 级别下就完全没有间隙锁了吗?
RC 下普通 SELECT 不加 Next-Key Lock,但外键约束和唯一索引检查时仍可能有间隙锁,只是范围小很多。
7 场景六:批量更新按主键乱序引发的死锁
场景描述:一条批量更新 UPDATE goods SET stock = stock - 1 WHERE id IN (100, 2, 55, 30),在高并发下频繁死锁。单条更新没事,为什么批量就死锁?
考察点:考察批量操作的加锁顺序问题——IN 列表的顺序和实际索引扫描顺序可能不一致。
解答思路:
UPDATE ... WHERE id IN (...) 在 InnoDB 里会逐个给匹配到的行加 X 锁。问题在于:加锁的顺序不一定按你写的 IN 列表顺序,而是按索引扫描顺序。两个并发的批量更新,如果它们的 IN 列表交集相同但起始顺序不同,就会以不同顺序拿锁,形成交叉死锁:
-- 本示例基于 MySQL 8.0
-- 会话 A 的批量:(100, 2, 55, 30)
UPDATE goods SET stock = stock - 1 WHERE id IN (100, 2, 55, 30);
-- 会话 B 的批量:(2, 30, 55, 100) ← 顺序与 A 相反
UPDATE goods SET stock = stock - 1 WHERE id IN (2, 30, 55, 100);
-- A 先锁 100 再锁 2,B 先锁 2 再锁 100 → 交叉死锁
解法:业务层先把 IN 列表按主键排序再执行,保证所有批量语句都按相同顺序加锁:
-- 本示例基于 MySQL 8.0
-- 正确做法:代码里先把 id 列表排序,例如 (2,30,55,100) 统一升序
UPDATE goods SET stock = stock - 1 WHERE id IN (2, 30, 55, 100);
另外,批量更新一次锁太多行本身就是风险,要控制单批大小(比如每批 100 行以内),分批提交,缩短事务持锁时间。
追问:除了排序,还有什么办法降低批量更新死锁概率?
缩小事务粒度(小批量多次提交)、降低隔离级别、以及在业务层对同一批 id 加分布式锁串行化。
8 场景七:高并发下 UPDATE 死锁如何加索引与调整顺序
场景描述:一条 UPDATE orders SET status=1 WHERE user_id=? AND status=0 在高并发下死锁,但 orders 表上 user_id 没有索引。这跟索引有什么关系?
考察点:考察"没索引导致锁升级/锁全表"的原理——UPDATE 没有索引会扫描并锁住大量行,把死锁概率放大。
解答思路:
这是一个很容易被忽略的点:UPDATE 的 WHERE 条件如果没走索引,InnoDB 会扫描全表并给扫描到的每一行都加锁(RR 级别下甚至锁间隙)。本来只该锁几行,结果锁了几万行,两个并发事务互相锁对方需要的行,死锁概率指数级上升。
-- 本示例基于 MySQL 8.0
-- 没索引时:全表扫描 + 逐行加锁,锁范围巨大
UPDATE orders SET status = 1 WHERE user_id = 123 AND status = 0;
-- 加合适的联合索引后:只锁定命中的那几行,死锁概率大幅下降
ALTER TABLE orders ADD INDEX idx_uid_status (user_id, status);
加索引后,UPDATE 只锁住 (user_id=123, status=0) 这几行,其他行完全不受影响,并发事务互不干扰。
追问:SELECT ... FOR UPDATE 没走索引也会锁全表吗?
会。不管是 UPDATE 还是 SELECT FOR UPDATE,只要 WHERE 条件不走索引,RR 级别下都会扫描并锁住大量记录,这是"无索引锁表"的经典事故。
下面这张表把"索引好坏"对锁范围的影响列清楚,面试时可以直接引用:
| WHERE 条件走没走索引 | 锁的范围 | 并发影响 |
|---|---|---|
| 走唯一索引、等值查询 | 只锁那一条记录 | 其他行完全不受影响 |
| 走普通索引、范围查询 | 锁命中记录 + 间隙 | 间隙内插入被阻塞 |
| 完全不走索引(RR) | 扫描全表逐行加 X 锁 | 等于锁全表,并发急剧恶化 |
所以"加索引"不只是为了查得快,更是为了把锁的范围缩小到精确的几行。很多同学只从性能角度理解索引,面试时能点出"索引还能缩小锁范围、降低死锁概率",会明显加分。
9 场景八:线上突发死锁风暴如何止血与降级
场景描述:某个版本上线后,死锁报错从每小时几次暴增到每秒几十次,核心交易接口大量失败。这不是零星死锁,是"死锁风暴"。现场怎么快速止血?
考察点:考察大规模锁问题的应急处置,区分"调参数"和"回滚/降级"。
解答思路:
死锁风暴的常见原因是新版本引入了新的加锁顺序或新的批量语句。止血优先级:
1. 回滚:如果死锁是新版本上线后才爆发,最快的止血是回滚上一个版本,别在线上硬调;
2. 限流降级:把触发死锁的非核心入口限流,保住主链路;
3. 临时参数:极端情况下可以临时关闭死锁检测(innodb_deadlock_detect=OFF)——但这是双刃剑,关闭后死锁不会被主动回滚,会变成锁等待直到超时,只适合应急,不能久开;
4. 缩短事务:检查是否有大事务、是否把无关操作塞进了事务。
-- 本示例基于 MySQL 8.0
-- 应急:临时关闭死锁检测(谨慎!会把死锁变成锁等待)
SET GLOBAL innodb_deadlock_detect = OFF;
-- 同时调小锁等待超时,让堵死的事务尽快释放,不要无限等
SET GLOBAL innodb_lock_wait_timeout = 5;
-- 事后必须改回 ON,否则故障会以另一种形式回来
追问:关闭死锁检测为什么能缓解风暴?代价是什么?
高并发下死锁检测本身要做全局等待图遍历,是 CPU 开销大户;关闭后省了检测开销,但代价是死锁不再被主动发现,线程会一直阻塞到锁等待超时,吞吐反而可能下降,只适合临时扛峰。
死锁风暴期间还有一个容易被忽略的观察点:SHOW ENGINE INNODB STATUS 的 SEMAPHORES 段。如果这里出现大量 RW-lock 等待、或显示 --Thread N has waited at ...,说明不仅业务在死锁,连内部 latch 都开始堆积,这时单纯调 SQL 已经救不回来,必须靠回滚版本或扩容。
10 场景九:如何区分死锁与长事务锁等待
场景描述:业务报"数据库卡住了",但你不确定是死锁还是某个长事务占着锁。怎么快速区分这两种问题,决定是等 MySQL 自动处理还是手动 KILL?
考察点:考察问题定性能力——死锁是自动解决的,长事务锁等待必须人工干预。
解答思路:
区分很简单,看现象和日志:
| 维度 | 死锁 Deadlock | 锁等待 Lock Wait |
|---|---|---|
| 报错 | 1213 错误,事务立刻回滚 | 1205 错误,等满 50 秒才超时 |
| 是否自动恢复 | 是,MySQL 检测后主动牺牲一个 | 否,持锁事务不结束就一直等 |
| 谁是元凶 | 两个事务互相等 | 一个长事务占着锁 |
| 现场在哪 | LATEST DETECTED DEADLOCK | sys.innodb_lock_waits + innodb_trx |
-- 本示例基于 MySQL 8.0
-- 查当前有没有正在等待锁的事务(锁等待的现场)
SELECT * FROM sys.innodb_lock_waits;
-- 如果查不到等待关系,但应用报 1213,那就是死锁(已被自动处理)
-- 再去看死锁日志确认最近一次死锁的双方 SQL
SHOW ENGINE INNODB STATUS;
追问:一个事务开了 30 分钟不提交,它算死锁吗?
不算,它是长事务。它会持锁导致别人锁等待,但它自己没在等任何人,所以不是死锁。处理方式是找到并 KILL 这个长事务。
11 场景十:锁监控与死锁治理长效机制
场景描述:死锁问题这次处理完了,你想知道线上还有没有别的潜在死锁,怎么建立长效的监控与治理机制?
考察点:考察从单次排障到体系化治理的能力。
解答思路:
长效机制三件事:
1. 持续采集死锁日志:innodb_print_all_deadlocks=ON,把 error log 里的死锁定期采集到日志平台,按 SQL 指纹聚合统计死锁次数;
2. 长事务监控:监控 information_schema.innodb_trx 里事务时长,超过阈值告警;
3. 规范落地:统一加锁顺序、批量操作先排序、大表 UPDATE 必须走索引、事务里不做远程调用。
# 本示例为 sqlite3 演示:模拟"死锁治理台账",记录每条死锁 SQL 的处理状态
import sqlite3
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("""CREATE TABLE deadlock (
id INTEGER PRIMARY KEY,
fingerprint TEXT, -- 死锁 SQL 指纹
freq INTEGER, -- 发生次数
owner TEXT, -- 负责团队
status TEXT)""") -- 待处理/已修复
cur.executemany("INSERT INTO deadlock(fingerprint,freq,owner,status) VALUES(?,?,?,?)",
[("UPDATE account SET balance=? WHERE id=?", 150, "支付组", "待处理"),
("UPDATE goods SET stock=? WHERE id IN(?)", 80, "交易组", "已修复")])
conn.commit()
for row in cur.execute("SELECT status, SUM(freq) FROM deadlock GROUP BY status"):
print(row) # 输出各状态累计死锁次数
conn.close()
上面脚本实跑输出为:('待处理', 150)、('已修复', 80),代表还有 150 次死锁未闭环。
追问:死锁重试放在业务层还是数据访问层统一做?
放在数据访问层(DAO/ORM 拦截器)统一做最干净,业务代码无感知,且能保证所有死锁都被一致地重试。
12 死锁排查速查表
| 你看到的报错 | 类型 | 第一反应命令 |
|---|---|---|
| 1213 Deadlock found | 死锁 | SHOW ENGINE INNODB STATUS 看 LATEST DEADLOCK |
| 1205 Lock wait timeout | 锁等待 | SELECT * FROM sys.innodb_lock_waits |
| 接口偶发超时、无报错 | 可能长事务 | SELECT * FROM information_schema.innodb_trx 看时长 |
| 死锁量突增 | 死锁风暴 | 先回滚版本,再查新 SQL 加锁顺序 |
| 隔离级别与间隙锁 | 间隙锁情况 |
|---|---|
| RR(可重复读) | 默认开启 Next-Key Lock,范围锁间隙,死锁概率高 |
| RC(读提交) | 基本退化为记录锁,间隙锁大幅减少 |
| 等值 + 唯一索引 | Next-Key Lock 退化为记录锁,无间隙 |
| 范围 + 普通索引 | 锁一段间隙,易互相插入死锁 |
13 常见坑与注意事项
1. 死锁不重试直接报错给用户:偶发死锁是并发调度问题,必须在数据层重试,否则用户偶发失败。
2. 把锁等待当死锁处理:锁等待要找长事务元凶,死锁是 MySQL 自动牺牲,处理方式别搞混。
3. UPDATE 不走索引:没索引的 UPDATE 会锁全表,把死锁概率放大,这是最隐蔽的事故源。
4. 批量 IN 列表不排序:不同事务按不同顺序加锁,必然交叉死锁。
5. KILL 长事务后不观察回滚:长事务回滚要 undo,期间锁还占着,KILL 不是立刻释放。
6. 长期关闭死锁检测:应急关了 innodb_deadlock_detect 却忘了开回来,故障会变本加厉。
7. 事务里做远程调用:RPC、发消息、查缓存塞进事务,持锁时间被网络延迟拉长,死锁概率暴涨。
14 练习题与自测
练习 1:业务报 1213 死锁,你登录时现场已过,怎么把之后的死锁完整记录下来?
参考答案:SET GLOBAL innodb_print_all_deadlocks=ON,把每次死锁写入 error log;应用层对 1213 错误码做退避重试。练习 2:sys.innodb_lock_waits 里 blocking_pid=88,你接下来怎么查这个事务为什么占着锁不放?
参考答案:用 88 去information_schema.innodb_trx查trx_started、trx_query、事务年龄,确认是否长事务,必要时KILL 88。
练习 3:两个事务交叉更新导致死锁,根因和通用解法是什么?
参考答案:根因是多事务加锁顺序不一致形成循环等待;通用解法是约定全局统一加锁顺序(如按主键升序),并保留死锁重试兜底。
练习 4:为什么 RR 级别下范围 FOR UPDATE 容易和别人死锁?怎么缓解?
参考答案:RR 用 Next-Key Lock 锁住记录前的间隙,两个事务锁重叠间隙时互相禁止对方插入;缓解办法是降到 RC、改等值查询、缩小范围。
练习 5(自测):口述一次完整的死锁排查:从收到 1213 报错到根因定位再到根治,列出你会用的至少 4 个命令或视图。
自测要点:能否说出 SHOW ENGINE INNODB STATUS / innodb_print_all_deadlocks / sys.innodb_lock_waits / innodb_trx 四个工具及其用途。
下一篇预告:第 23 章 大促高并发下的数据库方案
文章回复
0 条公开回复