mysql自动回滚代价最小的事务,依据是undo log条目数、已修改行数和持有锁数量,而非随机或按发起顺序选择,以实现最快恢复。

MySQL会自动回滚“代价最小”的事务,通常是持有锁数量最少、修改行数最少、事务执行时间最短的那个。
为什么不是随机选或固定选后发起的事务
InnoDB的死锁检测器(innodb_deadlock_detect)在识别出循环等待链后,会估算每个事务的“回滚代价”(rollback cost),这个代价主要基于:
-
trx_rows_locked:当前事务已持有的行锁数量 -
trx_rows_modified:已修改但未提交的行数 -
trx_undo_log_entries:undo log条目数(反映回滚工作量)
代价越小,回滚越快,系统恢复越早。所以它不是按“谁后开始”或“谁先等”来判,而是看谁“撤得最省力”。你在线上看到日志里被回滚的事务,常常是那个只更新了一行就去等另一行锁的轻量操作,而不是那个刚批量插入了200条记录的大事务。
如何从SHOW ENGINE INNODB STATUS里确认谁被回滚了
执行SHOW ENGINE INNODB STATUS\G后,在LATEST DETECTED DEADLOCK区块中:
- 被回滚的事务会明确标为
*** (1) TRANSACTION:或*** (2) TRANSACTION:,紧接着一行写ROLLING BACK - 另一方事务状态是
WAITING FOR THIS LOCK TO BE GRANTED - 注意看
undo log entries字段值——被回滚的那个通常明显更小
示例片段:
*** (1) TRANSACTION: TRANSACTION 123456789, ACTIVE 2 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 1024 n bits 72 index PRIMARY of table `db`.`t` trx id 123456789 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 123456788, ACTIVE 3 sec 2 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1 ROLLING BACK
应用层不能依赖“总是回滚对方”
很多业务代码默认写成“捕获Deadlock found when trying to get lock就重试”,这没问题;但如果你在事务里混用了存储过程、触发器或XA事务,回滚行为可能偏离预期:
- 存储过程中发生的死锁,回滚的是整个外部事务,不是仅存储过程内部
- 如果事务已显式执行过
SAVEPOINT,InnoDB仍会回滚到事务起点,不尊重保存点 - Spring
@Transactional若配置了rollbackFor = Exception.class但没捕获死锁异常并重新抛出,会导致部分语句被回滚、后续语句仍在同一事务上下文中继续执行(即“半提交”现象)
这种非原子性表现,比死锁本身更难排查。
innodb_deadlock_detect关闭后会发生什么
该参数默认为ON。如果误设为OFF(极少数高并发写场景有人尝试关闭以降低开销):
- 死锁不会被主动检测,事务将无限期等待,直到触发
innodb_lock_wait_timeout(默认50秒) - 超时后报错
Lock wait timeout exceeded,而非Deadlock found - 此时没有“自动选一个回滚”,而是所有卡住的事务都等满timeout才陆续失败
所以线上严禁关闭innodb_deadlock_detect——它不是性能瓶颈,而是死锁问题可被快速终结的前提。
真正容易被忽略的点在于:死锁回滚是InnoDB单边决定的,应用层看不到“正在被选为牺牲者”的中间态;你只能通过日志确认结果,而无法干预选择逻辑。这意味着所有关键业务路径必须把死锁当作常态错误来重试,且重试前要确保幂等或状态可校验。











