死锁发生是因为加锁顺序形成循环等待,而非仅因更新同一行;事务A锁id=1后等id=2,事务B锁id=2后等id=1,InnoDB检测到闭环即回滚其一。

UPDATE不同行为什么还会死锁
因为死锁不看“行是否相同”,而看“加锁顺序是否形成循环等待”。两个事务各自先锁住一行,再反向去锁对方已持有的那一行,InnoDB检测到这种互相等待的闭环,就会选一个事务回滚。
典型表现就是:UPDATE语句执行后没提交,另一个事务紧接着执行另一条UPDATE,然后双方都试图获取对方已占的锁。此时不是“谁先抢到谁赢”,而是MySQL主动介入,报出Deadlock found when trying to get lock。
- 事务A执行
UPDATE accounts SET balance = balance - 100 WHERE id = 1→ 持有id=1的行锁 - 事务B执行
UPDATE accounts SET balance = balance - 200 WHERE id = 2→ 持有id=2的行锁 - 事务A再执行
UPDATE accounts SET balance = balance + 100 WHERE id = 2→ 等待id=2的锁 - 事务B再执行
UPDATE accounts SET balance = balance + 200 WHERE id = 1→ 等待id=1的锁
四步走完,循环等待就成立了。注意:这个过程不需要任何索引缺失或间隙锁参与,纯行锁+错序访问就能触发。
可重复读下间隙锁让“看似不同”的行也互相阻塞
当UPDATE的WHERE条件没匹配到任何记录时,InnoDB在可重复读(RR)隔离级别下会加**间隙锁(Gap Lock)**,而不是跳过。间隙锁锁住的是“值之间的空隙”,不是某一行——所以两个事务操作id = 25和id = 26这种不存在的主键,实际锁住的是同一段间隙(比如(20, 30)),后续INSERT又需要插入意向锁,与间隙锁互斥,于是形成等待链。
关键点在于:你写的SQL是“更新id=25”,但MySQL看到的是“在(20, 30)这个间隙里找id=25”,而这个间隙已经被另一个事务用UPDATE ... WHERE id = 26锁住了。
- 表中有
id = 20和id = 30两条记录,中间无其他数据 - 事务A执行
UPDATE t_student SET name = 'a' WHERE id = 25→ 匹配0行,加间隙锁(20, 30) - 事务B执行
UPDATE t_student SET name = 'b' WHERE id = 26→ 同样加间隙锁(20, 30),但需等待事务A释放 - 若事务A接着执行
INSERT INTO t_student (id, name) VALUES (25, 'a'),就需要插入意向锁,又和事务B的间隙锁冲突
这时即使id值不同、SQL文本不同,底层锁资源仍是同一段区间,死锁自然发生。
辅助索引 + 主键操作混用容易隐式锁升级
当一条SQL通过辅助索引(如KEY idx_c1(c1))定位数据,另一条通过主键(PRIMARY)操作同一张表时,两把锁可能落在不同索引上,但最终指向同一行物理记录。InnoDB为保证一致性,会在辅助索引记录和对应主键记录上都加锁。如果两个事务交叉使用这两种路径,就容易出现“一个锁主键、一个锁辅助索引”的互等局面。
例如:
- 事务A执行
SELECT * FROM dl WHERE c1 = 5 FOR UPDATE→ 在idx_c1上加X锁,并隐式在对应主键记录上也加X锁 - 事务B执行
DELETE FROM dl WHERE id = 6→ 直接在主键索引上加X锁 - 若这两条语句恰好命中同一行(比如c1=5那行的id正好是6),事务A已锁主键,事务B又想锁主键,而事务B也已持有主键锁,事务A又想通过辅助索引锁它——锁结构交叉,死锁日志里就会同时出现
index idx_c1和PRIMARY的锁等待
这种场景在业务中很隐蔽:开发只关注SQL逻辑,很少意识到WHERE c1 = ?和WHERE id = ?在锁层面可能相互干扰。
最容易被忽略的点:事务未显式结束
所有上述死锁的前提,是事务长期处于活跃状态。只要任一事务执行完UPDATE后没立刻COMMIT或ROLLBACK,它持有的锁就一直挂着。后面来的任何操作,只要涉及相同资源,就可能被拖进死锁链。
线上最常见的情况是:应用层开启事务后,业务逻辑卡在RPC调用、缓存读写或异常分支里,忘记commit;或者用了连接池但没正确归还连接,导致事务“悬停”在数据库侧长达几十秒。这时候哪怕只是两条简单的UPDATE,也能因时间差酿成死锁。
查当前长事务命令:SELECT * FROM information_schema.innodb_trx WHERE TIME_TO_SEC(NOW()) - TIME_TO_SEC(trx_started) > 30,重点关注trx_state = 'LOCK WAIT'和trx_started时间戳偏老的记录。


















