死锁本质是事务加锁顺序不一致形成的环路,与sql快慢无关;需统一多表访问顺序、确保order by走索引、单表更新按主键物理序加锁、避免混合扫描模式,并将校验压入update的where子句。

死锁不是SQL跑得慢,而是多个事务抢资源时加锁顺序不一致,形成了环路。只要所有事务对同一组数据按相同物理顺序加锁,90%以上的死锁就能直接避免。
为什么ORDER BY id LIMIT 1不一定安全
很多人以为加了ORDER BY id LIMIT 1就能控制更新顺序,但若id没索引,MySQL仍可能走全表扫描——不同事务扫描起始位置不同,加锁顺序就不可控。更危险的是,全表扫描会触发间隙锁或临键锁,锁住大量无关记录。
- 用
EXPLAIN确认type字段:不能是ALL(全表扫描)或index(全索引扫描) - 确保
WHERE和ORDER BY字段共用同一个联合索引,且查询命中最左前缀(例如索引是(status, created_at, id),查询必须带status = ?) - 如果业务逻辑允许,直接改用
WHERE id = ?精确查找,彻底规避排序带来的不确定性
多表操作必须统一访问顺序
两个事务分别先更新orders再更新order_items,和先更新order_items再更新orders,就是死锁的温床。InnoDB不会帮你协调顺序,它只认你写的SQL执行流。
- 所有业务代码、存储过程、ORM配置,必须强制约定:涉及
users、orders、payments三张表时,一律按此顺序操作 - 不要依赖“看起来差不多”的逻辑——比如一个服务用
JOIN一次性更新,另一个拆成两条UPDATE,执行计划不同,加锁路径就可能错位 - 检查ORM框架(如MyBatis、GORM)是否隐式改变了
WHERE条件顺序,导致生成的SQL加锁顺序不一致
单表多行更新要按索引物理顺序加锁
InnoDB行锁基于索引,但锁的申请顺序取决于查询实际扫描的行序。如果你用WHERE status = 'pending' ORDER BY created_at DESC,而created_at没索引,MySQL大概率按聚簇索引(主键)顺序扫描,但方向是逆序的——这就和另一个事务按ASC扫描形成反向加锁,极易死锁。
- 批量更新前,先用
SELECT id FROM t WHERE ... ORDER BY id ASC FOR UPDATE显式锁定,并确保该查询走索引 - 避免在事务里混合使用覆盖索引扫描(加记录锁)和全表扫描(可能升级为间隙锁)
- 用
SELECT ... FOR UPDATE时,禁止SELECT * FROM t FOR UPDATE这种无条件语句,它会锁整张表的聚簇索引
事务里别拆“查-判-更”,更别塞非DB操作
把“查余额→判断够不够→扣款”写成三个独立语句,看着清晰,实则高危:两次网络往返之间,数据可能已被改写;而且第一次查没加锁,第二次更新才加X锁,中间空档就是并发冲突窗口。
- 把校验逻辑全部压进
UPDATE的WHERE子句,例如:UPDATE accounts SET balance = balance - 100 WHERE id = 123 AND balance >= 100 - 如果必须分步,至少把“查+更”包在一个事务里,且查的时候就用
SELECT ... FOR UPDATE,锁住目标行不放 - 严禁在事务中调用RPC、发短信、写日志、做复杂计算——这些操作不产生锁,却拖长锁持有时间,把“快拿快放”变成“拿着不放”
真正难的不是写对一条SQL,而是在整个系统里让所有路径都收敛到同一套加锁契约。哪怕一个定时任务、一个补偿脚本、一个灰度接口,只要碰了那几张核心表,就必须遵守顺序规则——否则,它就是那个打破全局一致性的例外,也是下一次死锁的起点。











