死锁本质是事务间加锁顺序冲突形成的环路,与sql快慢无关,而取决于语句顺序、索引使用及是否命中索引。

死锁不是SQL写得慢,是事务抢资源的顺序冲突
MySQL死锁本质是两个或多个事务互相持有对方需要的锁,又申请对方已持有的锁,形成环路。它和SQL执行快慢无关,而和UPDATE、DELETE、SELECT ... FOR UPDATE这些语句在事务中出现的**顺序**、**涉及的索引范围**、**是否命中索引**强相关。
常见错误现象:Deadlock found when trying to get lock; try restarting transaction,但重试后又成功——这说明不是数据问题,是并发调度下的竞争模式问题。
- 必须保证所有事务以相同顺序访问多张表(比如总是先操作
orders再操作order_items) - 单表多行更新时,务必让
WHERE条件走同一索引,并按该索引顺序物理扫描(例如用id ASC排序后再更新) - 避免在事务里混合使用覆盖索引扫描和全表扫描——前者加的是记录锁,后者可能升级为间隙锁甚至临键锁,扩大锁范围
ORDER BY + LIMIT 不等于安全,没索引照样死锁
很多人以为加了ORDER BY id LIMIT 1就能控制更新顺序,但如果id字段没索引,MySQL仍可能走全表扫描,锁住大量无关记录;更糟的是,不同事务扫描起始位置不同,导致加锁顺序不一致。
使用场景:秒杀扣减库存、余额变更等需“选一行更新”的逻辑。
- 确认
WHERE和ORDER BY字段都在同一个联合索引里(例如(status, created_at, id)),且查询能命中该索引最左前缀 - 用
EXPLAIN验证是否走了索引,特别注意type不能是ALL或index(后者也可能扫全索引) - 如果业务允许,改用主键精确查找(
WHERE id = ?)替代范围+排序,彻底规避顺序不确定性
事务粒度越小越好,但别拆成“查-判-更”三步
为了减少锁持有时间,有人把“先查再判断再更新”拆成三个独立语句。这是高危操作:两次网络往返之间,数据可能已被其他事务改写,导致业务逻辑错乱,还更容易触发死锁(因为两次加锁时机分离)。
性能影响:看似减少了单次事务时间,实则增加整体锁等待概率和重试次数。
- 把校验逻辑塞进一条
UPDATE的WHERE子句里(例如UPDATE accounts SET balance = balance - 100 WHERE id = 123 AND balance >= 100) - 用
SELECT ... FOR UPDATE时,必须配合WHERE条件明确限定行,禁止SELECT * FROM t FOR UPDATE - 如果真要分步,至少把“查+更”包在一个事务里,且查的时候就加上
FOR UPDATE,不让中间有空档
READ COMMITTED 能减少间隙锁,但不解决顺序问题
把隔离级别从默认的REPEATABLE READ降到READ COMMITTED,确实会让MySQL少加很多间隙锁(Gap Lock),从而降低死锁概率。但它**完全不改变事务内SQL的执行顺序**,只要两个事务仍以不同顺序更新同一组主键,依然会死锁。
兼容性影响:某些依赖可重复读语义的业务(比如基于第一次读结果做多次判断)会出错;binlog格式必须是ROW才能安全降级。
- 仅当确认业务不需要范围查询一致性,且已通过其他方式(如唯一索引约束、应用层幂等)兜底时,才考虑调整隔离级别
- 降级后仍需检查
INFORMATION_SCHEMA.INNODB_TRX里的trx_isolation_level是否生效 - 比调隔离级别更有效的是:统一SQL顺序 + 强制索引 + 缩短事务生命周期
最容易被忽略的一点:死锁日志里显示的“waiting for”和“holding”锁,往往来自不同索引(比如一个锁主键,一个锁二级索引),这意味着你可能根本没意识到某条UPDATE会触发二级索引维护——查SHOW ENGINE INNODB STATUS时,一定盯紧INDEX名,而不是只看表名。











