[面试题] MySQL只操作同一条记录,也会发生死锁吗?
📖 版块 A:知识讲解版 (温故知新)
-
死锁的本质:两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。
-
单行记录死锁的可能性:
- 锁升级/锁冲突:MySQL 的行锁并非原子性一次性获取所有需要的锁,一个 SQL 语句在执行过程中可能会经历从“共享锁 (S锁)”到“排他锁 (X锁)”的提升过程。
- 并发插入冲突:在处理
INSERT ... ON DUPLICATE KEY UPDATE或者REPLACE INTO时,如果多个事务同时发现某条唯一键记录不存在并尝试插入,或者记录存在尝试更新,内部会涉及 S 锁与 X 锁的切换。 - 唯一索引校验:当多个事务并发插入相同的唯一键值时,为了检查唯一性,InnoDB 会先对该记录(或间隙)加 S 锁,如果随后需要执行插入或更新(变更为 X 锁),多个事务之间会形成环路等待。
-
典型触发场景:
- 事务 A 持有 S 锁,想转为 X 锁;此时事务 B 也持有 S 锁,也想转为 X 锁。双方都在等待对方释放 S 锁,从而触发死锁。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:MySQL只操作同一条记录,也会发生死锁吗?
💬 高手逐字稿:
是的,MySQL 即使只操作同一条记录,也是会发生死锁的。
这背后的核心原因在于,InnoDB 的行锁并不是一次性申请完毕的,一个复杂的 SQL 语句在执行过程中可能会根据需要,先后申请不同类型的锁。
最经典的一个场景就是“锁升级导致的死锁”。假设我们要执行一个 INSERT ... ON DUPLICATE KEY UPDATE 操作。当并发事务同时操作同一行唯一键记录时:
- 事务 A 进入,发现记录已存在,为了检查唯一性冲突,它会先获取该行记录的 S 锁(共享锁)。
- 事务 B 随后进入,也对同一行获取了 S 锁。此时 S 锁与 S 锁是兼容的,所以两个事务都持有该行的 S 锁。
- 接着,事务 A 准备执行更新操作,由于更新需要 X 锁(排他锁),它会尝试将持有的 S 锁升级为 X 锁。但此时它发现事务 B 还持有 S 锁,因此事务 A 进入阻塞,等待事务 B 释放 S 锁。
- 紧接着,事务 B 也准备执行更新,同样尝试将 S 锁升级为 X 锁。但它发现事务 A 也正持有 S 锁并处于等待状态。
到这一步,事务 A 在等事务 B 释放 S 锁,事务 B 也在等事务 A 释放 S 锁,典型的死锁环路就形成了。
除了这种锁升级,还有一种常见情况是并发下的唯一键冲突(Unique Key Conflict)。比如多个事务同时 INSERT 同一个已删除但未提交的唯一键值。当第一个事务回滚时,剩下的多个事务会同时获得该记录的 S 锁,随后在尝试获取 X 锁进行插入时,也会因为互相等待对方的 S 锁释放而触发死锁。
💡 高手自问自答:这里其实有一个很容易被忽略的细节:为什么 MySQL 不直接一步到位加 X 锁呢?这是因为在 InnoDB 层面,对于某些包含唯一键约束的语句,它必须先进行“读”性质的校验来确认冲突,这个过程默认加的是 S 锁或者 Next-key Lock。只有在确定可以操作后,才会转换为 X 锁。正是这种“先看后写”的非原子性锁申请,给了死锁发生的空间。在实际工程中,我们通常建议在高并发且存在唯一索引竞争的场景下,尽量通过分布式锁或者减小事务范围来规避这类单行死锁问题。