MySQL 并发 INSERT 和 SELECT 引起的死锁
MySQL deadlock caused by concurrent INSERT and SELECT
- MySQL版本:5.6
- 存储引擎:InnoDB
当两个任务尝试 select
然后 insert
相同的 table 时发生死锁。该过程如下所示:
Task_1 Task_2
------ ------
Phase 1 | SELECT SELECT
Phase 2 | INSERT INSERT
SELECT count(id) from mytbl where name = 'someValue' and timestampdiff(hour, ts, now()) < 1;
INSERT mytbl (id, name, ts) values ('newId', 'anotherValue', now());
死锁日志如下(删减部分内容):
------------------------
LATEST DETECTED DEADLOCK
------------------------
151225 8:22:17
*** (1) TRANSACTION:
TRANSACTION 0 746402, ACTIVE 0 sec, process no 4690, OS thread id 140411390486272 inserting
mysql tables in use 1, locked 1
LOCK WAIT 1172 lock struct(s), heap size 112624, 32914 row lock(s)
MySQL thread id 3909, query id 31751474 10.20.36.38 mydb update
INSERT INTO mytbl -- truncated
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 5044 n bits 88 index `PRIMARY` of table `MYDB`.`mytbl` trx id 0 746402 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 0 746449, ACTIVE 0 sec, process no 4690, OS thread id 140411389953792 inserting, thread declared inside InnoDB 500
mysql tables in use 1, locked 1
1172 lock struct(s), heap size 112624, 32914 row lock(s)
MySQL thread id 3906, query id 31751477 10.20.36.38 mydb update
INSERT INTO mytbl -- truncated
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 5044 n bits 88 index `PRIMARY` of table `MYDB`.`MYTBL` trx id 0 746449 lock mode S
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 5044 n bits 88 index `PRIMARY` of table `MYDB`.`MYTBL` trx id 0 746449 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
问题
- 根据 MySQL 手册,简单的
SELECT
语句使用不需要 S 锁 的快照读取。 INSERT
语句要求在要插入的单行上使用 X 锁。那为什么Task_2
持有S锁导致死锁?
编辑
SHOW CREATE TABLE
结果如下:
| task_content | CREATE TABLE `mytbl` (
`id` bigint(20) NOT NULL,
`ts` timestamp NULL DEFAULT NULL,
`name` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
如果您当前的隔离级别是 repeatable read
或更高,为了能够在事务中为 select count(id) ...
重复相同的结果 MySQL 必须锁定整个主键(或WHERE
条件使用的另一个键的一部分)。然后通过插入新值来修改键。但是并发事务修改了密钥的状态,这已经被看到了。两者都可以从密钥的相同状态开始,然后等到另一个完成而没有任何更改,这样它就会应用自己的更改。
where name = 'someValue' and timestampdiff(hour, ts, now()) < 1;
那是相当低效的。让我们清理它以加快速度,减少出现死锁的可能性。
timestampdiff(hour, ts, now()) < 1
隐藏任何带有 ts
的索引;让我们将其重写为
ts < NOW() - INTERVAL 1 HOUR
您的内容以意想不到的方式被截断了;我的说 "ts older than 1 hour ago",我怀疑你想要。
现在我们可以索引ts
,效果很好。但是,让我们通过使用 "composite" 索引更进一步:
INDEX(name, ts)
这将有效地使用 WHERE
子句的两个部分来定位行。
你说COUNT(id)
——这意味着你需要在id
中避免NULLs
。也许这不是问题,您可以简单地说 COUNT(*)
.
这些应该会使 SELECT
更快。现在让我们弄清楚为什么 SELECT
和 INSERT
有什么关系。他们在同一笔交易中吗?或者你是否关闭了自动提交,但忘了说 COMMIT
?请向我们展示整个交易,加上 SHOW CREATE TABLE
.
文章 here 对锁和隔离级别进行了详尽的解释。
感谢@newtover 提供有关隔离级别的线索。我对这篇文章的总结和我自己的问题的答案如下:
InnoDB 中的默认隔离级别是 RepeatableRead,它将锁定索引(不锁定数据 table)直到 交易结束。
在我的情况下,唯一的索引是 PRIMARY
,这在我的 SELECT
查询中没有用(可以通过 explain select...
验证)。结果,PRIMARY
索引中的 所有 项都被锁定。当 TXN_2
在某个条目上等待 X 锁 时,该条目被 [=15= 保留的 S 锁 锁定].类似地,TXN_1
在另一个条目上等待 X 锁,但该条目也被自己保留的 S 锁 锁定。发生 "one S two X" 死锁。
相比之下,在 name
列上创建索引 name
后,索引 name
将在 SELECT
语句中使用(可以通过explain select ...
),因此将在索引 name
而不是 PRIMARY
上发出锁。更重要的是,SELECT
语句只会对等于 someValue
的条目发出 S 锁 ,而不是索引 name
的所有条目。此外,INSERT
需要的IX锁和X锁会在索引PRIMARY
上发出。 S锁与IX锁、X锁冲突解决
列 name
上的索引不仅加快了查询速度,更重要的是防止锁定索引的所有条目。
在 BEGIN 和 END 事务中写下您的每个查询。我希望它不会发生。
更多:here
查询的书面部分似乎是正确的,绝对不是问题的根源。我猜你的任务正在执行交错,并且在开始时,每个任务都会启动一个事务。你没说怎么执行这些任务,执行每一个任务的时候主键值是多少?
您可能希望将主键字段更改为 AUTO_INCREMENT 或确保您的任务使用的主键确实是唯一的。
如果它没有帮助,另一种(但不建议)解决方案是通过互斥文本保护上层的过程调用代码。
- MySQL版本:5.6
- 存储引擎:InnoDB
当两个任务尝试 select
然后 insert
相同的 table 时发生死锁。该过程如下所示:
Task_1 Task_2
------ ------
Phase 1 | SELECT SELECT
Phase 2 | INSERT INSERT
SELECT count(id) from mytbl where name = 'someValue' and timestampdiff(hour, ts, now()) < 1;
INSERT mytbl (id, name, ts) values ('newId', 'anotherValue', now());
死锁日志如下(删减部分内容):
------------------------
LATEST DETECTED DEADLOCK
------------------------
151225 8:22:17
*** (1) TRANSACTION:
TRANSACTION 0 746402, ACTIVE 0 sec, process no 4690, OS thread id 140411390486272 inserting
mysql tables in use 1, locked 1
LOCK WAIT 1172 lock struct(s), heap size 112624, 32914 row lock(s)
MySQL thread id 3909, query id 31751474 10.20.36.38 mydb update
INSERT INTO mytbl -- truncated
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 5044 n bits 88 index `PRIMARY` of table `MYDB`.`mytbl` trx id 0 746402 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 0 746449, ACTIVE 0 sec, process no 4690, OS thread id 140411389953792 inserting, thread declared inside InnoDB 500
mysql tables in use 1, locked 1
1172 lock struct(s), heap size 112624, 32914 row lock(s)
MySQL thread id 3906, query id 31751477 10.20.36.38 mydb update
INSERT INTO mytbl -- truncated
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 5044 n bits 88 index `PRIMARY` of table `MYDB`.`MYTBL` trx id 0 746449 lock mode S
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 5044 n bits 88 index `PRIMARY` of table `MYDB`.`MYTBL` trx id 0 746449 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
问题
- 根据 MySQL 手册,简单的
SELECT
语句使用不需要 S 锁 的快照读取。INSERT
语句要求在要插入的单行上使用 X 锁。那为什么Task_2
持有S锁导致死锁?
编辑
SHOW CREATE TABLE
结果如下:
| task_content | CREATE TABLE `mytbl` (
`id` bigint(20) NOT NULL,
`ts` timestamp NULL DEFAULT NULL,
`name` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
如果您当前的隔离级别是 repeatable read
或更高,为了能够在事务中为 select count(id) ...
重复相同的结果 MySQL 必须锁定整个主键(或WHERE
条件使用的另一个键的一部分)。然后通过插入新值来修改键。但是并发事务修改了密钥的状态,这已经被看到了。两者都可以从密钥的相同状态开始,然后等到另一个完成而没有任何更改,这样它就会应用自己的更改。
where name = 'someValue' and timestampdiff(hour, ts, now()) < 1;
那是相当低效的。让我们清理它以加快速度,减少出现死锁的可能性。
timestampdiff(hour, ts, now()) < 1
隐藏任何带有 ts
的索引;让我们将其重写为
ts < NOW() - INTERVAL 1 HOUR
您的内容以意想不到的方式被截断了;我的说 "ts older than 1 hour ago",我怀疑你想要。
现在我们可以索引ts
,效果很好。但是,让我们通过使用 "composite" 索引更进一步:
INDEX(name, ts)
这将有效地使用 WHERE
子句的两个部分来定位行。
你说COUNT(id)
——这意味着你需要在id
中避免NULLs
。也许这不是问题,您可以简单地说 COUNT(*)
.
这些应该会使 SELECT
更快。现在让我们弄清楚为什么 SELECT
和 INSERT
有什么关系。他们在同一笔交易中吗?或者你是否关闭了自动提交,但忘了说 COMMIT
?请向我们展示整个交易,加上 SHOW CREATE TABLE
.
文章 here 对锁和隔离级别进行了详尽的解释。
感谢@newtover 提供有关隔离级别的线索。我对这篇文章的总结和我自己的问题的答案如下:
InnoDB 中的默认隔离级别是 RepeatableRead,它将锁定索引(不锁定数据 table)直到 交易结束。
在我的情况下,唯一的索引是 PRIMARY
,这在我的 SELECT
查询中没有用(可以通过 explain select...
验证)。结果,PRIMARY
索引中的 所有 项都被锁定。当 TXN_2
在某个条目上等待 X 锁 时,该条目被 [=15= 保留的 S 锁 锁定].类似地,TXN_1
在另一个条目上等待 X 锁,但该条目也被自己保留的 S 锁 锁定。发生 "one S two X" 死锁。
相比之下,在 name
列上创建索引 name
后,索引 name
将在 SELECT
语句中使用(可以通过explain select ...
),因此将在索引 name
而不是 PRIMARY
上发出锁。更重要的是,SELECT
语句只会对等于 someValue
的条目发出 S 锁 ,而不是索引 name
的所有条目。此外,INSERT
需要的IX锁和X锁会在索引PRIMARY
上发出。 S锁与IX锁、X锁冲突解决
列 name
上的索引不仅加快了查询速度,更重要的是防止锁定索引的所有条目。
在 BEGIN 和 END 事务中写下您的每个查询。我希望它不会发生。
更多:here
查询的书面部分似乎是正确的,绝对不是问题的根源。我猜你的任务正在执行交错,并且在开始时,每个任务都会启动一个事务。你没说怎么执行这些任务,执行每一个任务的时候主键值是多少?
您可能希望将主键字段更改为 AUTO_INCREMENT 或确保您的任务使用的主键确实是唯一的。
如果它没有帮助,另一种(但不建议)解决方案是通过互斥文本保护上层的过程调用代码。