快速定位死锁SQL需执行SHOW ENGINE INNODB STATUS\G,查看LATEST DETECTED DEADLOCK区块,从中提取两个事务的TRANSACTION ID、HOLDS THE LOCK(S)和WAITING FOR THIS LOCK TO BE GRANTED信息,并结合应用日志反查对应请求。

死锁报错 Deadlock found when trying to get lock 怎么快速定位是哪条 SQL 引起的
MySQL 自身不直接告诉你“第几行代码触发了死锁”,但会记录最近一次死锁的完整上下文。关键在 SHOW ENGINE INNODB STATUS\G 的输出里,找 LATEST DETECTED DEADLOCK 区块。
这个区块里会列出两个(或多个)互相等待的事务,每条都包含:TRANSACTION ID、mysql tables in use、locked tables,以及最关键的——每个事务正在执行的那条 SQL(带具体参数值)和它卡在哪个 lock_mode(如 X locks rec but not gap)。
- 别只看报错时间,要对比两个事务的
WAITING FOR THIS LOCK TO BE GRANTED和HOLDS THE LOCK(S)行,理清谁锁了什么、谁在等什么 - 如果应用用了连接池,
trx_mysql_thread_id对应的是 MySQL 内部线程 ID,需结合应用日志里的请求 trace ID 或时间戳反查是哪次 API 调用 - 注意:该命令只保留最后一次死锁信息,高频死锁时容易被覆盖,建议出问题后立刻执行,或配置
innodb_print_all_deadlocks = ON写入错误日志
为什么按不同顺序 UPDATE 同几张表就容易死锁
InnoDB 行锁是基于索引加的,不是按 SQL 书写顺序加锁,而是按数据物理/索引顺序逐行加锁。当两个事务以相反顺序访问相同资源时,就构成经典的环形等待。
比如事务 A 执行:UPDATE accounts SET balance = balance - 100 WHERE id = 1; → UPDATE accounts SET balance = balance + 100 WHERE id = 2;
事务 B 同时执行:UPDATE accounts SET balance = balance - 200 WHERE id = 2; → UPDATE accounts SET balance = balance + 200 WHERE id = 1;
只要两个事务的第二步还没完成,就可能互相持有对方下一步需要的锁。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 解决核心:所有业务路径对同一组表的 DML 操作,必须约定**固定顺序**(例如始终按
id升序处理) - 避免在事务中先
SELECT ... FOR UPDATE再判断逻辑,再UPDATE—— 这会让锁范围不可控;尽量把条件收敛到单条 UPDATE 的 WHERE 子句里 - 如果涉及多表更新,优先用
JOIN一次性完成,而不是拆成多次单表语句
SELECT ... FOR UPDATE 在非唯一索引上为什么锁住不止一行
这是最容易被忽略的坑:SELECT ... FOR UPDATE 在非唯一索引(包括普通二级索引)上执行时,InnoDB 会加 **next-key lock**(间隙锁 + 记录锁),范围可能远超你 WHERE 条件匹配的行数。
比如 name 字段没建唯一索引,执行 SELECT * FROM users WHERE name = 'alice' FOR UPDATE,InnoDB 不仅锁住所有 name = 'alice' 的记录,还会锁住 'alice' 前后相邻的索引间隙,防止其他事务插入同名新记录 —— 这直接扩大了锁粒度,提高死锁概率。
- 验证方法:用
SELECT * FROM performance_schema.data_locks查看当前事务持有的锁详情,注意LOCK_DATA和LOCK_MODE字段 - 优化方向:给查询字段加唯一索引(让锁退化为 record lock);或改用
SELECT ... LOCK IN SHARE MODE+ 应用层重试逻辑,降低冲突强度 - 特别注意:在
READ COMMITTED隔离级别下,InnoDB 会禁用间隙锁(只保留记录锁),但代价是可能引发幻读 —— 是否切换需权衡一致性要求
应用程序里 retry 死锁要不要无脑重试
不能无脑重试。MySQL 返回死锁错误时,事务已被自动回滚,应用收到异常后确实可以重试,但必须确保重试逻辑是幂等的,且重试前有明确状态检查。
- 常见错误:用户余额扣减接口收到死锁后直接重放原始请求,结果因前置状态已变(比如余额已被其他事务扣完),导致重复扣款或负余额
- 正确做法:在重试前,先
SELECT检查关键业务状态(如当前余额是否仍满足扣减条件),再决定是否继续执行;或者把整个业务逻辑封装进存储过程,用DECLARE EXIT HANDLER FOR 1213控制内部重试 - 重试次数建议限制在 2–3 次,配合指数退避(如 100ms、300ms、900ms),避免雪崩式重试压垮数据库
真正难的不是捕获死锁,而是厘清业务操作背后的数据依赖图——哪些行会被同时修改、哪些索引会导致锁扩散、哪些并发路径会交叉。这些细节藏在 SQL 执行计划和锁日志里,不在代码注释里。










