mysql 在REPEATEABLE-READ隔离级别下如果两个线程同时对相同条件记录用SELECT…ROR UPDATE加排他锁在没有符合该记录情况下两个线程都会加锁成功。程序发现记录尚不存在就试图插入一条新记录如果两个线程都这么做就会出现死锁。这种情况下将隔离级别改成READ COMMITTED就可以避免问题。解释以上发生死锁的原因及解决死锁的原理1 死锁的原因InnoDB间隙锁Gap Lock的一个核心特性间隙锁之间是不互斥的允许多个事务在同一间隙上持有锁。你描述的完整流程会形成一个“双方各自持有了对方需要的间隙锁然后又试图获取对方的间隙锁”的经典死锁循环。我们分步拆解一下。 问题根源RR隔离级别下的间隙锁与死锁在REPEATABLE-READRR隔离级别下为了彻底防止幻读InnoDB引入了间隙锁机制。当SELECT ... FOR UPDATE查询一条不存在的记录时它不会空手而归而是会锁定这条记录“本应存在”的区间。第一步查询并加间隙锁事务T1执行SELECT ... WHERE id 5 FOR UPDATE假设id5的记录不存在。T1会在id5所属的间隙例如(3, 8)区间上加上间隙锁Gap Lock。事务T2执行相同的查询。因为间隙锁可以共存它不会阻塞T2T2也会成功地在同一个间隙(3, 8)上加上间隙锁。第二步尝试插入并死锁现在T1尝试INSERT INTO ... VALUES (5)。在插入前它需要申请一个与间隙锁互斥的插入意向锁Insert Intention Lock。由于间隙(3,8)上已有T2的间隙锁T1的插入意向锁请求会被阻塞T1进入等待T2的状态。同样T2尝试插入时其插入意向锁也会被T1的间隙锁阻塞T2进入等待T1的状态。至此死锁Deadlock形成系统将回滚其中一个事务以解除僵局。这个经典死锁场景在MySQL官方Bug库中也被多次记录和讨论。2 为什么READ-COMMITTED(RC) 能解决问题将隔离级别降为READ COMMITTED能从根本上避免此问题因为它完全改变了加锁策略禁用间隙锁在RC隔离级别下InnoDB不使用间隙锁来防止幻读除了极少数涉及唯一索引检查的特殊情况。仅锁定存在的行SELECT ... FOR UPDATE只会对实际存在并符合条件的记录加上行锁Record Lock。对于不存在的记录它不会锁定任何间隙。因此在你描述的场景中T1和T2的SELECT ... FOR UPDATE都不会获得任何锁更不会产生间隙锁的冲突死锁的循环链条就从源头上被打破了。 总结死锁如何形成下表清晰对比了两种隔离级别下的行为差异隔离级别SELECT … FOR UPDATE (记录不存在时)能否避免死锁原因REPEATABLE READ (RR)加间隙锁(Gap Lock)否间隙锁不互斥允许多个事务同时持有但它们都与插入意向锁互斥从而形成等待环路导致死锁。READ COMMITTED (RC)不加锁是仅对存在的行加记录锁从根本上杜绝了针对不存在记录的间隙锁冲突。对于这种SELECT ... FOR UPDATE后再INSERT的业务模式通常称为get_or_create一个更推荐的解决方案是直接使用INSERT ... ON DUPLICATE KEY UPDATE语句这通常能在一条SQL内原子化地完成操作既保证了并发安全也无需在应用层编写复杂的加锁逻辑。另外由于间隙锁是针对索引记录的如果你查询的列上没有建立索引SELECT ... FOR UPDATE可能会锁定更大的范围甚至退化成表锁因此为查询列建立合适的索引也十分重要。你遇到的这个问题在并发编程中确实很典型要复现它需要一个完美的时机。如果想亲自体验一下我也可以为你提供具体的代码示例和验证步骤帮你更直观地理解这个过程