死锁根源于应用层多表操作顺序不一致,须统一按字母序或业务主次固定顺序;select ... for update 必须命中唯一索引;事务应最小化,仅包含读-算-写;禁用requires_new。

死锁不是数据库问题,是代码里访问顺序不一致导致的
MySQL 死锁绝大多数时候跟索引、隔离级别关系不大,真正根子在应用层——UPDATE 或 SELECT ... FOR UPDATE 操作的表/行顺序在不同事务里不统一。比如 A 事务先更新 user 再更新 order,B 事务反过来,就卡住了。
- 所有涉及多表写操作的事务,必须约定**固定顺序**:按表名字母序(
order→user)、按业务主次(order→order_item),选一个并写进团队规范 - 避免在事务里拼接动态 SQL,尤其不能根据参数决定先查哪张表;用
IF/CASE控制逻辑可以,但最终执行的 DML 语句顺序必须恒定 - 别依赖「先查后更」模式:用
SELECT ... FOR UPDATE查到数据再做判断,接着UPDATE—— 这中间如果其他事务也查了同一行,就容易形成环路
用 SELECT ... FOR UPDATE 前先加唯一索引条件
没走索引的 SELECT ... FOR UPDATE 会锁整张表或整个索引范围,大幅提高冲突概率。MySQL 的行锁本质是「索引记录锁」,没索引就退化成表锁。
- 确保
WHERE条件中至少有一个列命中**唯一索引或主键**,例如WHERE id = ?或WHERE order_no = ?(order_no有唯一索引) - 避免
WHERE status = ? AND created_at > ?这类组合条件,除非该组合上有覆盖索引;否则可能锁住大量无关行 - 用
EXPLAIN FORMAT=TRADITIONAL看执行计划,确认type是const或ref,且key显示用了哪个索引
事务粒度越小越好,别把 HTTP 请求生命周期当事务边界
一个 Web 请求里开事务、做校验、调第三方 API、再更新 DB,这种长事务是死锁温床。MySQL 锁持有时间越长,撞车机会越高。
- 事务只包「读-算-写」三步:查数据 → 本地计算 → 更新 DB;外部依赖(HTTP、Redis、文件)全挪到事务外
- 不要在事务里做
sleep()、print_r()、日志写磁盘等阻塞操作;调试时加日志可以,但上线前删掉或确保异步 - 批量操作别用单条
INSERT循环:改用INSERT ... VALUES (...), (...), (...)一条语句完成,减少事务内语句数和锁时间
innodb_lock_wait_timeout 不是用来兜底的,是用来暴露问题的
把 innodb_lock_wait_timeout 从默认 50 秒调成 2 秒,看起来能更快失败,但实际只是把死锁延迟变成了超时延迟,掩盖了根本问题。
- 线上应保持默认值(50 秒),配合监控:捕获
Lock wait timeout exceeded错误并告警,说明某段代码正在高频争抢资源 - 真正要改的是应用层重试逻辑:遇到
Deadlock found when trying to get lock,应立即重试(最多 1–2 次),而不是等超时 - 别在 DAO 层自动重试——重试必须在业务语义安全的前提下做,比如「扣库存」可重试,「发消息」就不能重复触发
最容易被忽略的一点:事务里调用的函数如果内部又开了新事务(比如用了 @Transactional(propagation = Propagation.REQUIRES_NEW)),等于把锁拆成了多个不可控片段,顺序和范围全乱了。代码里搜 REQUIRES_NEW 和 BEGIN,一个都不能留。











