原因是,如果表中不存在记录,它看起来像 SELECT ... FOR UPDATE 在共享模式下(或类似的东西 - 它没有记录在任何地方)对于不存在的记录之后的索引记录REPEATABLE READ 隔离模式已启用。
让我们尝试一个简单的表t 的示例,该表在REPEATABLE READ 隔离模式下为空。
t1> SELECT * FROM t WHERE id = 1 FOR UPDATE;
no rows found, next-key lock acquired in shared mode
t2> SELECT * FROM t WHERE id = 1 FOR UPDATE;
no rows found, next-key lock acquired in shared mode
t1> INSERT INTO t (id) VALUES (1);
transaction t1 is blocked by t2
t2> INSERT INTO t (id) VALUES (1);
transaction t2 is blocked by t1 - deadlock
即使第二个 SELECT 和 INSERT 将使用id=2,也会发生死锁,因为它也落入了相同的间隙,由SELECT ... FOR UPDATE 锁定,在t1 中执行。如果桌子是空的,这个间隙是无穷大。如果表不为空,则插入不同记录的死锁概率较小,但仍然很大(这取决于表中有多少间隙以及插入到表末尾的频率 - 最大间隙)。
发生这种情况是因为来自t1 和t2 的SELECT ... FOR UPDATE 在记录不存在时不会相互阻止。对于现有记录,它会在t1 中的记录上获取X(独占)锁,因此t2 将被阻止,直到t1 被提交或回滚。但是如果记录不存在 - 它会在间隙上获取 S(共享)next-key 锁(我不确定它是否真的是 S 锁(它没有在任何地方记录),但是 MySQL 还允许如何在同一个间隙上同时获取 2 个锁?)。这就是这里出现死锁的主要原因——t1 和t2 都试图在间隙上获得 IX(插入意图)锁,然后在插入的记录上获得 X 锁,但都因为锁而相互等待,被SELECT ... FOR UPDATE收购。
使用READ COMMITED事务隔离级别时不存在此问题。如果没有找到记录并且使用了READ COMMITED 隔离级别,SELECT ... FOR UPDATE 不会持有任何锁。所以第一个INSERT 会成功。第二个INSERT 将被第一个INSERT 获取的独占锁阻塞,在提交t1 之后,第二个INSERT 将抛出Duplicate entry '1' for key 'PRIMARY'。
您现在可以认为这种情况并不比僵局更好。只是另一个错误。但现在想象第二个INSERT 尝试插入一条带有id=2 的记录。在这种情况下,t1 不会阻止它,两个事务都会成功。这对于某些应用程序来说是一个很大的不同。