死锁主因是事务对同一行集加锁顺序不一致,优化执行顺序最有效:先INSERT后UPDATE、多表操作统一访问顺序、WHERE条件必走索引、SELECT FOR UPDATE需索引+确定性ORDER BY。

死锁不是SQL写得慢,而是多个事务对同一组行以不同顺序加锁,形成环形等待。优化执行顺序是成本最低、见效最快的规避手段。
先 INSERT 再 UPDATE 能显著降低锁冲突
事务中先 UPDATE 后 INSERT 容易引发死锁,因为 UPDATE 会立即加行锁或间隙锁,而后续 INSERT 在唯一键检查时需在已持锁范围内扫描,锁无法释放;反过来,INSERT 先执行只加轻量的插入意向锁(Insert Intention Lock),范围窄、时间短,再 UPDATE 时锁住的行更少。
- 适用场景:订单创建+状态更新、日志记录+主表变更等含写入和条件更新的组合逻辑
- 反例:
BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 123; INSERT INTO order_logs (...) VALUES (...); COMMIT; - 正例:
BEGIN; INSERT INTO order_logs (...) VALUES (...); UPDATE accounts SET balance = balance - 100 WHERE id = 123; COMMIT; - 注意:前提是
INSERT不依赖UPDATE的结果(如不依赖UPDATE返回的last_insert_id()或影响行数)
多表操作必须统一访问顺序
只要涉及两张及以上表,所有事务必须严格按相同物理顺序访问——不是按 SQL 书写顺序,而是按业务约定的“资源优先级”顺序。MySQL 不会自动重排语句执行顺序,但并发下不同事务若交叉执行,极易触发循环等待。
- 常见错误现象:
Deadlock found when trying to get lock,且SHOW ENGINE INNODB STATUS显示两个事务分别持有对方需要的表锁 - 正确做法:全局约定如“所有订单相关事务,必须先操作
orders表,再操作order_items,最后操作inventory” - 不能靠注释或文档约定:应在 DAO 层封装成固定调用链,或用统一 Service 方法强制顺序
- 特别警惕 JOIN 更新:
UPDATE orders o JOIN order_items i ON o.id = i.order_id SET o.status = 'shipped'实际加锁顺序取决于执行计划,应拆为明确的单表语句并按序执行
避免 WHERE 条件没索引导致的隐式全表锁升级
哪怕你只改一行,如果 UPDATE 或 DELETE 的 WHERE 条件没命中索引,MySQL 会降级为全表扫描,每行加锁——这等于把行锁变成了事实上的表锁,极大抬高死锁概率。
- 验证方式:执行
EXPLAIN FORMAT=JSON,确认key字段非NULL,且rows_examined接近预期行数(而非表总行数) - 典型陷阱:
WHERE status = 'pending'(无索引)、WHERE DATE(created_at) = '2026-09-28'(函数导致索引失效)、WHERE a = 1 OR b = 2(OR 拆分后可能走全表) - 修复路径:为高频查询字段建联合索引,例如
CREATE INDEX idx_status_created ON orders(status, created_at),并确保查询条件满足最左前缀 - RR 隔离级别下更要小心:非唯一索引的范围查询会加 Gap Lock,不同事务扫描起始点不同,锁顺序天然不一致
SELECT FOR UPDATE 必须配合确定性 ORDER BY + 索引
SELECT FOR UPDATE 本身不保证加锁顺序安全。没索引的 ORDER BY id LIMIT 1 仍是全表扫描,不同事务扫描起点不同,加锁顺序就不可控——这才是死锁高发区。
- 必须同时满足三个条件:WHERE 条件命中索引、ORDER BY 字段属于同一索引、LIMIT 存在且合理(避免跳过大量行)
- 推荐写法:
SELECT * FROM orders WHERE status = 'unpaid' AND created_at ,前提是 <code>(status, created_at, id)是联合索引 - 更稳妥替代:直接用主键查找,
SELECT * FROM orders WHERE id = ? FOR UPDATE,彻底规避排序不确定性 - 禁止写法:
SELECT * FROM orders WHERE status = 'unpaid' ORDER BY id LIMIT 1 FOR UPDATE(status 无索引)、SELECT * FROM orders ORDER BY rand() LIMIT 1 FOR UPDATE(随机打乱顺序)
真正难处理的不是“怎么写”,而是“怎么让所有人一直按这个规则写”。上线前用 performance_schema.data_locks 抓取真实锁行为,比只看 SQL 更可靠;一旦发现某类事务反复出现在死锁日志里,说明它已经脱离了设计预期——这时候要查代码,而不是调参数。


















