MySQL InnoDB select for update 和 insert 之间出现死锁

标签 mysql innodb deadlock database-deadlocks

背景:

MySQL 5.7.18中 我有一个名为“test”的表,定义如下:

| test  | CREATE TABLE `test` (
  `id` varchar(255) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `test_name_x01` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |

它只有 1 行:

id | name
2  | eva

现在我启动 2 个事务,均处于 REPEATABLE-READ 隔离级别,并执行如下命令:

  1. T1:开始;
  2. T2:开始;
  3. T2 : select * from test where name='eva' 进行更新;
  4. T1 : select * from test where name='eva' 进行更新; (现在 T1 成立)
  5. T2:插入测试值(1,'eva'); (死锁,T1回滚)

InnoDB 日志:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2017-05-06 20:24:28 0x126df8000
*** (1) TRANSACTION:
TRANSACTION 112142, ACTIVE 15 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 139, OS thread handle 4950491136, query id 997169 localhost root Sending data
select * from test where name='eva' for update
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 5 n bits 72 index test_name_x01 of table `promotion`.`test` trx id 112142 lock_mode X waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 3; hex 657661; asc eva;;
 1: len 1; hex 32; asc 2;;

*** (2) TRANSACTION:
TRANSACTION 112141, ACTIVE 19 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
MySQL thread id 138, OS thread handle 4947148800, query id 997170 localhost root update
insert into test values (1, 'eva')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 26 page no 5 n bits 72 index test_name_x01 of table `promotion`.`test` trx id 112141 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 3; hex 657661; asc eva;;
 1: len 1; hex 32; asc 2;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 5 n bits 72 index test_name_x01 of table `promotion`.`test` trx id 112141 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 3; hex 657661; asc eva;;
 1: len 1; hex 32; asc 2;;

*** WE ROLL BACK TRANSACTION (1)

我的想法

根据我的研究,以下是我认为可能导致死锁的原因。但需要专业人士确认。

1. T1 : begin;
2. T2 : begin;
3. T2 : select * from test where name='eva' for update; 

第 3 步需要:IX 锁,索引“name”上的下一键锁(负无穷大,“eva”]

4. T1 : select * from test where name='eva' for update;

第 4 步需要:IX 锁,索引“name”上的下一个键锁(负无穷大,“eva”) IX锁与IX兼容,因此T1无需T2释放即可获取它。 next-key lock 实际上包含 2 个部分:记录 X + 间隙,由于冲突的锁可以被不同的事务持有在间隙上,所以 T1 也持有间隙锁而不等待 T2。 所以 T1 只是等待T2 释放记录锁(在二级索引 name='eva' 和聚集索引 id='1' 上)以继续。

5. T2 : insert into test values (1, 'eva'); (dead lock, T1 is rolled back)

第5步:这里插入需要插入意图锁,这与间隙锁不兼容。因此T2正在等待T1释放其间隙锁。但同时T1正在等待T2释放记录X锁。

================================================== =========================

更新,发现了更多我上面的解释无法解释的有趣事实。

此外,对于第 5 步,在尝试使用不同的 ID 值后,我得到以下观察结果:

如果表预加载了具有不同 ID 且名称相同“eva”的多行,只有在步骤 5 中尝试插入的 ID 值小于所有现有行的最小 ID 时,才会重现死锁。

例如预加载表

id | name
2  | eva
4  | eva

对于上面的步骤 5,

insert (0, 'eva') => deadlock 
insert (1, 'eva') => deadlock
insert (3, 'eva') => NO deadlock
insert (5, 'eva') => NO deadlock

最佳答案

请勿将数字存储到不带引号的 VARCHAR 中。这会挫败任何使用索引的尝试。

更多

一般来说,...

当可以通过索引识别单行时,只有该行将被“锁定”。单行锁定通常通过“延迟”一个事务直到另一个事务释放其锁来避免死锁。

当必须扫描整个表时,所有可能需要锁定。这可能不允许“延迟”,并陷入僵局。

无论如何,...

通过准备好重放整个事务来计划死锁。 有些死锁是不可避免的。

(很抱歉含糊其词;这里有太多可能的变化。修复引号或索引;应该避免大多数死锁。)

关于MySQL InnoDB select for update 和 insert 之间出现死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43827740/

相关文章:

mysql外键未违反

Java生产者消费者模型总是死锁

mysql - 什么时候忽略innodb_lock_wait_timeout?

c++ - asio::async_write 在大容量流上同步非常困难

php - Mysql中的"Equity"系统

php - 在哪里可以找到 MySQLi 中所有可能的错误

python - 如何通过 MySQLdb Python 将值插入表中

MYSQL LEFT JOIN 和多个 ORDER BY

mysql - 将表类型更改为 InnoDB

MySQL MyISAM关系图