MySQL 使用具有新值的索引发生死锁

MySQL Deadlock using an index with a new value

Table:

create table properties
(
  id              int auto_increment primary key,
  other_id        int          null
);

create index index_properties_on_other_id
  on properties (other_id);

TX 1:

start transaction;
SET @last_id = 1;
delete from `properties` WHERE `properties`.`other_id` = @last_id;
INSERT INTO `properties` (`other_id`) VALUES (@last_id);
commit

发送 2:

start transaction;
SET @last_id = 2;
delete from `properties` WHERE `properties`.`other_id` = @last_id;
INSERT INTO `properties` (`other_id`) VALUES (@last_id);
commit

假设 table 在 运行 交易之前是空的。

我的应用程序有 2 个用例。有时 last_id 已经被另一行使用,因此它会被优先索引;但有时它会由先前的插入查询在同一事务中生成,在这种情况下我会遇到死锁。

我需要 运行 两个事务直到删除语句之后。当我 运行 在 tx1 上插入时,它等待获得锁,然后我 运行 在 tx2 上插入,tx2 出现死锁并回滚。

mysql            | LATEST DETECTED DEADLOCK
mysql            | ------------------------
mysql            | 2019-06-03 21:01:05 0x7f0ba4052700
mysql            | *** (1) TRANSACTION:
mysql            | TRANSACTION 320051, ACTIVE 12 sec inserting
mysql            | mysql tables in use 1, locked 1
mysql            | LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
mysql            | MySQL thread id 286, OS thread handle 139687839577856, query id 17804 172.18.0.1 root update
mysql            | INSERT INTO `properties` (`other_id`) VALUES (@last_id)
mysql            | *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
mysql            | RECORD LOCKS space id 1524 page no 4 n bits 72 index index_properties_on_other_id of table `properties` trx id 320051 lock_mode X insert intention waiting
mysql            | Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
mysql            |  0: len 8; hex 73757072656d756d; asc supremum;;
mysql            | 
mysql            | *** (2) TRANSACTION:
mysql            | TRANSACTION 320052, ACTIVE 8 sec inserting
mysql            | mysql tables in use 1, locked 1
mysql            | 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
mysql            | MySQL thread id 287, OS thread handle 139687973168896, query id 17814 172.18.0.1 root update
mysql            | INSERT INTO `properties` (`other_id`) VALUES (@last_id)
mysql            | *** (2) HOLDS THE LOCK(S):
mysql            | RECORD LOCKS space id 1524 page no 4 n bits 72 index index_properties_on_other_id of table `properties` trx id 320052 lock_mode X
mysql            | Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
mysql            |  0: len 8; hex 73757072656d756d; asc supremum;;
mysql            | 
mysql            | *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
mysql            | RECORD LOCKS space id 1524 page no 4 n bits 72 index index_properties_on_other_id of table `properties` trx id 320052 lock_mode X insert intention waiting
mysql            | Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
mysql            |  0: len 8; hex 73757072656d756d; asc supremum;;
mysql            | 
mysql            | *** WE ROLL BACK TRANSACTION (2)

删除语句后的锁状态:

mysql            | ---TRANSACTION 320066, ACTIVE 90 sec
mysql            | 2 lock struct(s), heap size 1136, 1 row lock(s)
mysql            | MySQL thread id 287, OS thread handle 139687973168896, query id 18076 172.18.0.1 root
mysql            | TABLE LOCK table `properties` trx id 320066 lock mode IX
mysql            | RECORD LOCKS space id 1524 page no 4 n bits 72 index index_properties_on_other_id of table `properties` trx id 320066 lock_mode X
mysql            | Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
mysql            |  0: len 8; hex 73757072656d756d; asc supremum;;
mysql            | 
mysql            | ---TRANSACTION 320065, ACTIVE 95 sec
mysql            | 2 lock struct(s), heap size 1136, 1 row lock(s)
mysql            | MySQL thread id 286, OS thread handle 139687839577856, query id 18039 172.18.0.1 root
mysql            | TABLE LOCK table `properties` trx id 320065 lock mode IX
mysql            | RECORD LOCKS space id 1524 page no 4 n bits 72 index index_properties_on_other_id of table ``properties` trx id 320065 lock_mode X
mysql            | Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
mysql            |  0: len 8; hex 73757072656d756d; asc supremum;;

所以两个交易是 deleting/inserting 不同的 other_id,我没想到他们会陷入僵局。我想知道为什么会这样。

MySQL 不会锁定不存在的东西,例如锁定您没有删除的行。它也不存储您尝试删除具有特定值“1”的行。相反,它所做的是标记 space 如果“1”本来应该在那里,并用 gap lock 锁定它,它具有以下特征:

Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.

在一个空的 table 中,1 本来应该在的地方是 "anywhere in the table"(或者从开始到死锁中提到的 "supremum" 的任何地方)——因此由 delete 锁定。 2 也是如此。根据定义,这些锁不会相互冲突。

但是 insert 可以。第一个 insert 将不得不等待第二个事务为其删除发出的间隙锁。如果第二个事务现在也尝试 insert 进入间隙,这将需要解除第一个事务的间隙锁,但这不会发生,因为第一个事务已经在等待第二个间隙锁被解除。所以你陷入了僵局。

一旦填满 table,这种情况就会减少,因为间隙锁不再需要跨越整个 table。如果你例如你的 table 中已经有 other_id 1 和 3,delete/inserting 值 2 和 4 不会相互死锁。

一般来说,空 table 是罕见的,你不能也不应该从这种特殊情况推断出任何正常行为。您基本上必须接受边缘情况:

Gap locks are part of the tradeoff between performance and concurrency

所以在一般的用例中,你只需要准备好偶尔会发生死锁(然后重复事务)。如果您的预期用例是您有一个基本为空的 table,或者主要是在值的末尾添加,或者经常将 2 个值添加到同一间隙中,您可能需要不同的解决方案(并且应该询问关于如何在此特定用例中进行的问题)。你可以例如能够使用唯一索引(不需要间隙锁),recode/hash 你的值随机地放在索引中,或者你让所有事务锁定你知道存在的东西,所以他们互相等待。