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

ach*_*n55 1 mysql database deadlock innodb

桌子:

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

create index index_properties_on_other_id
  on properties (other_id);
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

发射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
Run Code Online (Sandbox Code Playgroud)

假设在运行事务之前表是空的。

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

我需要运行这两个事务,直到删除语句之后。当我在 tx1 上运行 insert 时,它会等待获取锁,然后我在 tx2 上运行 insert,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)
Run Code Online (Sandbox Code Playgroud)

删除语句后锁的状态:

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;;
Run Code Online (Sandbox Code Playgroud)

所以两个事务正在删除/插入不同的other_ids,我没想到它们会陷入死锁。我想了解为什么会发生这种情况。

Sol*_*are 5

MySQL 不会锁定不存在的内容,例如锁定您未删除的行。它也不会存储您尝试删除具有特定值“1”的行。相反,它所做的是标记“1”应该存在的空间(如果它本来就在那里),并用间隙锁锁定它,它具有以下特征:

\n\n
\n

InnoDB中的间隙锁是\xe2\x80\x9cpurely抑制\xe2\x80\x9d,这意味着它们的唯一目的是防止其他事务插入间隙。间隙锁可以共存。一个事务获取的间隙锁不会阻止另一事务在同一间隙上获取间隙锁。共享间隙锁和独占间隙锁之间没有区别。它们彼此不冲突,并且执行相同的功能。

\n
\n\n

在空表中, 1 所在的位置是“表中的任何位置”(或从开始到死锁中提到的“最高值”的任何位置) - 因此它被delete. 2 也是如此。并且根据定义,这些锁不会相互冲突。

\n\n

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

\n\n

一旦填满表,这种情况就会减少,因为间隙锁不再需要跨越整个表。例如,如果您other_id的表中已经有 1 和 3,则删除/插入值 2 和 4 不会彼此死锁。

\n\n

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

\n\n
\n

间隙锁是性能和并发性之间权衡的一部分

\n
\n\n

因此,在一般用例中,您只需要做好偶尔可能发生死锁的准备(然后重复事务)。如果您预期的用例是您有一个基本上为空的表,或者主要在值的末尾添加,或者经常在同一个间隙中添加 2 个值,那么您可能需要不同的解决方案(并且应该询问如何继续此特定用例)。例如,您可以使用唯一索引(不需要间隙锁),重新编码/散列您的值以随机位于索引中,或者让所有事务锁定您知道存在的某些内容,以便它们互相等待。

\n