怎么看最近一次死锁的详细信息
MySQL 每次检测到死锁,都会在
SHOW ENGINE INNODB STATUS的输出中保留**最后一次死锁的完整现场**,这是排查最核心的入口。别依赖错误日志——它通常只记“Deadlock found when trying to get lock”,而真正关键的 SQL、索引、锁类型、哪个事务被回滚,全在
SHOW ENGINE INNODB STATUS里。 执行
show engine innodb status\G(加
\G是为了格式化可读) 重点找
*** (1) TRANSACTION:和
*** (2) TRANSACTION:块:看各自执行的 SQL、持有的锁(
HOLDS THE LOCK(S))、等待的锁(
WAITING FOR THIS LOCK TO BE GRANTED) 注意锁类型缩写:
X是排他锁,
S是共享锁,
GAP是间隙锁,
INSERT_INTENTION是插入意向锁 如果看到
index idx_user_id上两个事务互等对方某条记录的
X锁,基本就是加锁顺序不一致导致的
为什么加了索引还是死锁?查锁是否真的命中索引
很多同学加了索引就以为万事大吉,但死锁依然频发——根本原因是 SQL **没走预期索引**,退化成全表扫描或范围扫描,锁住大量无关行。InnoDB 的行锁是建立在索引上的,没走索引 = 锁表级行为。
用EXPLAIN看执行计划:确认
type是
ref/
eq_ref(精确匹配),而不是
range或更差的
ALL特别警惕
WHERE条件含函数、隐式类型转换(如
user_id = '123'而字段是
INT)、
OR多条件拼接——这些都可能导致索引失效 RR 隔离级别下,哪怕走了索引,范围查询(如
id > 100 AND id )也会触发 <code>Next-Key Lock(行锁 + 间隙锁),扩大锁范围;而 RC 级别虽禁用大部分间隙锁,但唯一键冲突检查时仍会用到
怎么快速定位正在堵着的事务和锁
死锁发生后,可能已有事务卡在锁等待中,不及时处理会影响后续请求。别只盯着“最后一次死锁”,要实时看当前谁在等、谁在占。
查活跃事务:SELECT * FROM information_schema.INNODB_TRX,重点关注
TRX_STATE(是否
LOCK WAIT)、
TRX_STARTED(是否长事务)、
TRX_QUERY(正在执行的 SQL) 查锁等待关系:
SELECT * FROM information_schema.INNODB_LOCK_WAITS,它直接关联了阻塞者(
BLOCKING_TRX_ID)和被阻塞者(
REQUESTING_TRX_ID) MySQL 8.0+ 可用
performance_schema.data_locks查具体锁在哪张表、哪个索引、哪一行(需提前开启相关 instrument) 慎用
KILL:只 kill 等待方(
TRX_STATE = 'LOCK WAIT'),不要 kill 持有锁的事务,否则可能引发二次死锁或数据不一致
代码层怎么避免重蹈覆辙
DBA 能帮你查清原因,但根治得靠业务逻辑改造。死锁不是数据库问题,是并发访问模式暴露了设计缺陷。
所有涉及多行更新的场景,强制按主键/唯一键**升序排序后统一加锁**:比如分配资金给多个借款人,先SELECT id FROM borrower WHERE ... ORDER BY id FOR UPDATE,再逐条更新,确保所有事务锁行顺序一致 拆分大事务:避免在事务内做 HTTP 调用、文件读写、sleep 等耗时操作;把“查 → 算 → 改”拆成“查 → 算 → 提交 → 改”,让锁持有时间尽可能短 减少显式锁:能用
UPDATE ... WHERE id = ? AND version = ?乐观锁解决的,就别用
SELECT ... FOR UPDATE;尤其在高并发计数、库存扣减等场景 ORM 尤其要注意:MyBatis 的
<foreach></foreach>生成 IN 查询时,若传入 ID 列表无序,不同事务锁顺序天然不一致——务必先排序再传参
最容易被忽略的一点:死锁日志里那个被回滚的事务,往往不是“错”的那个,而是 InnoDB 挑的“代价最小”的牺牲者。你看到的错误堆栈,只是结果,不是病因——必须回到
SHOW ENGINE INNODB STATUS去还原两个事务的真实执行路径。
