快速定位MySQL死锁根因需执行SHOW ENGINE INNODB STATUS\G,重点分析LATEST DETECTED DEADLOCK区块,明确两个事务各自持有的锁(HELD LOCKS)和等待的锁(WAITING FOR THIS LOCK TO BE GRANTED),结合TABLE、INDEX、lock_mode及space id/page number/record等信息精准定位冲突行与索引。

死锁报错 Deadlock found when trying to get lock 怎么快速定位根因
MySQL 的死锁不是随机发生的,而是事务间加锁顺序冲突的必然结果。关键不是“怎么重试”,而是看清楚哪两个事务、在哪些行、按什么顺序抢了哪些锁。
执行 SHOW ENGINE INNODB STATUS\G,重点找 LATEST DETECTED DEADLOCK 区块:它会明确列出两个事务各自持有的锁(HELD LOCKS)和正在等待的锁(WAITING FOR THIS LOCK TO BE GRANTED)。注意里面的 TABLE、INDEX、lock_mode(如 X locks rec but not gap)和具体 space id/page number/record —— 这些才是真实冲突点。
- 别只看 SQL 文本,同一句
UPDATE user SET balance = ? WHERE id = ?在不同事务里可能锁住不同索引(比如一个走主键,一个走二级索引 + 回表),锁范围完全不同 - 如果看到
lock_mode X locks gap before rec,说明有间隙锁参与,大概率是 WHERE 条件用了非唯一索引或范围查询 - 事务中混用
SELECT ... FOR UPDATE和普通UPDATE时,InnoDB 可能对同一行加两次锁(一次显式,一次隐式),加剧冲突
为什么把大事务拆成小事务后,Lock wait timeout exceeded 还是频繁出现
拆事务能降低单次持有锁的时间,但解决不了“多个小事务仍按不同顺序访问相同资源”这个本质问题。锁等待多,往往意味着并发路径存在竞争热点,而非单纯事务太长。
典型场景:订单服务和库存服务都按“先查再更新”逻辑操作 product 表,但一个按 id 查,一个按 sku 查——即使各自事务只有两行 SQL,只要访问顺序不一致,就可能形成环形等待。
- 检查所有涉及同一张表的写操作路径,确保所有业务逻辑都通过**同一个唯一索引字段**定位记录(比如统一用
id,而不是有时用id、有时用order_no或user_id) - 避免在事务内做 RPC 调用或文件 IO,这些会拉长锁持有时间,且无法被 MySQL 监控到
-
autocommit=0下手动开启的事务,务必确认每个分支都有COMMIT或ROLLBACK,漏掉会导致锁一直挂着
SELECT ... FOR UPDATE 加锁范围到底由什么决定
不是由语句长得像不像,而是由执行计划 + 索引结构 + 隔离级别共同决定。同一句 SQL,在不同数据分布、不同索引覆盖下,锁的行数、是否带间隙锁,可能天差地别。
例如:SELECT * FROM order WHERE status = 'pending' FOR UPDATE —— 如果 status 没有索引,就会锁全表;如果有索引但不是唯一索引,InnoDB 会在匹配的每条记录上加记录锁,并在它们之间的间隙加间隙锁(防止幻读)。
- 用
EXPLAIN FORMAT=tree看实际走哪个索引,是否发生索引下推(ICP)、是否回表 - 在
READ COMMITTED隔离级别下,InnoDB 不加间隙锁(只锁匹配到的行),但REPEATABLE READ下默认加,这是很多死锁的隐藏来源 - 想精确控制锁范围?改用
SELECT ... FOR UPDATE OF table_name(8.0.1+)或更稳妥的方式:先用唯一索引精确查出主键,再用WHERE id = ? FOR UPDATE
如何让多个服务模块对同一张表的加锁顺序绝对一致
没有魔法配置能让 MySQL 自动排序加锁顺序。唯一可靠的方式,是在应用层强制所有写路径按**同一套规则生成访问序列**,比如按主键升序批量处理。
常见错误是:A 服务按 created_at 倒序取一批订单更新,B 服务按 id 正序取同一批订单更新——哪怕数据集完全一样,加锁顺序也相反,极易死锁。
- 所有批量更新前,先
SELECT id FROM ... ORDER BY id ASC拿到有序 ID 列表,再按此顺序逐条或分批UPDATE ... WHERE id = ? - 避免在事务中动态拼接 IN 列表(如
WHERE id IN (3,1,5)),InnoDB 对 IN 的加锁顺序不保证与列表顺序一致;改用临时表或多次单值更新 - 微服务间共享数据库时,建议用
SELECT ... FOR UPDATE SKIP LOCKED(8.0.1+)替代应用层排队,它能跳过已被锁的行,天然规避部分竞争
最麻烦的不是锁本身,而是不同模块对同一张表的访问逻辑散落在十几个服务里,没人知道谁在什么时候、以什么顺序、锁了哪些索引。上线前不梳理清楚访问路径,光调参数或加重试,只是把问题拖得更隐蔽。










