所有事务必须按主键升序加锁以避免死锁,因不同顺序访问同一行是死锁主因;应用层需统一ID排序、约定多表访问顺序、慎用SELECT FOR UPDATE、批量操作用单语句分段执行,并避免事务中调用外部服务。

所有事务必须按主键升序加锁
死锁最常见原因是两个事务以不同顺序访问同一组行。InnoDB 不保证 SQL 执行顺序,但你可以强制业务层统一行为:只要所有 UPDATE、SELECT FOR UPDATE 都基于主键升序排列,就几乎能消除因顺序错乱导致的死锁。
- 错误写法:
UPDATE order SET status='paid' WHERE id IN (105, 98, 112);—— InnoDB 内部会按主键排序后加锁,但你无法控制应用层分批查询时的 ID 顺序 - 正确写法:
UPDATE order SET status='paid' WHERE id IN (98, 105, 112) ORDER BY id;(注意:MySQL 的UPDATE ... ORDER BY仅影响执行顺序,不改变加锁顺序;真正起作用的是 IN 列表本身是否已排序) - 更稳妥做法:在应用层先对 ID 数组排序,再拼入 SQL,例如 Python 中
sorted([105, 98, 112])→[98, 105, 112] - 对范围查询也适用:
SELECT * FROM user WHERE id BETWEEN 100 AND 200 FOR UPDATE比WHERE created_at > '2026-01-01'更可控,因为后者若没走索引,可能锁住大量无关行
多表操作必须约定全局表访问顺序
微服务或模块化系统里,不同服务各自写 SQL,极易出现 A 服务先更新 user 再更新 order,B 服务反着来——这就是死锁温床。解决方式不是技术选型,而是协作规范。
- 定义并文档化顺序,例如:总是
user→order→order_item→log - 在 DAO 层或 Service 入口做校验:比如用枚举或常量标识“当前事务操作阶段”,禁止在
order已更新后回写user - 避免跨服务事务嵌套调用:订单创建中调用户扣款接口,若该接口自己又开事务,就可能打破顺序约定
- 检查现有 SQL:用
SHOW ENGINE INNODB STATUS查看死锁日志,重点找HOLDS THE LOCK(S)和WAITING FOR THIS LOCK TO BE GRANTED两段,确认冲突是否来自表顺序颠倒
慎用 SELECT FOR UPDATE,优先用带条件的 UPDATE
SELECT FOR UPDATE 看似可控,实则容易引入额外锁竞争点,尤其当它和后续 UPDATE 分离时。很多死锁不是出在更新逻辑,而是读锁 + 写锁两次申请造成的窗口期。
- 反模式:
BEGIN; SELECT * FROM stock WHERE sku='A001' FOR UPDATE; -- 做库存校验后再 UPDATE;这中间可能被其他事务插队 - 推荐写法:
UPDATE stock SET qty = qty - 1 WHERE sku='A001' AND qty >= 1;成功即锁定并更新,失败说明已被抢或不足,无需重试读锁 - 如果必须用
SELECT FOR UPDATE,务必加ORDER BY id,且只用于等值查询(如WHERE id = ?),避免WHERE price BETWEEN ? AND ?触发间隙锁 - 注意隔离级别影响:在
READ-COMMITTED下间隙锁被禁用,但REPEATABLE-READ(默认)下范围查询会锁区间,这点常被忽略
批量操作别拆成循环,改用单语句 + 主键分段
把 1000 行更新写成 for 循环执行 1000 次 UPDATE,等于制造 1000 次独立加锁机会,顺序不可控、耗时拉长、死锁概率指数上升。
- 错误做法:
for id in ids: cursor.execute("UPDATE t SET x=1 WHERE id=%s", id) - 正确思路:用
WHERE id IN (...)或WHERE id BETWEEN ? AND ?,每次处理 100 行以内,并确保 IN 列表已排序 - 分段示例:
UPDATE t SET status='done' WHERE id BETWEEN 1000 AND 1099 AND status='pending';—— 利用主键连续性,避免全表扫描,锁范围明确 - 不要依赖子查询结果动态决定更新哪些行,例如
UPDATE t SET x=1 WHERE id IN (SELECT id FROM tmp WHERE flag=1),子查询执行顺序不确定,加锁路径不可预测
实际中最容易被忽略的,是“事务内调用外部服务”这个动作——它不产生 SQL 锁,但会让已有锁长时间悬停,放大所有上述问题的破坏力。锁本身不可怕,可怕的是锁着资源还在等 HTTP 响应。


















