由并发INSERT和SELECT引起的MySQL死锁

时间:2015-12-24 06:49:40

标签: mysql deadlock

  • MySQL版本:5.6
  • 存储引擎:InnoDB

当两个任务尝试select然后insert同一个表时,发生了死锁。程序如下:

          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)

问题

  1. 根据MySQL手册,简单SELECT语句使用快照读取,不需要 S lock INSERT语句要求插入单行上的 X lock 。那么为什么Task_2持有 S锁并导致死锁?
  2. 修改

    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 |
    

5 个答案:

答案 0 :(得分:7)

文章here给出了关于锁和隔离级别的详尽解释。

感谢@newtover提供有关隔离级别的线索。我对该文章的总结和我自己的问题的答案如下:

InnoDB中的默认隔离级别是可重复读取,它将锁定索引(不锁定数据表),直到事务结束

在我的情况下,唯一的索引是PRIMARY,这在我的SELECT查询中无效(可由explain select...验证)。因此,PRIMARY索引中的所有条目都被锁定。当TXN_2在某个条目上等待 X lock 时,该条目被TXN_1保留的 S lock 锁定。同样,TXN_1在另一个条目上等待 X lock ,但该条目也被 S lock 锁定。发生"one S two X"死锁。

相比之下,在name列上创建索引name后,name语句中将使用索引SELECT(可由{{验证} 1}}),因此锁将在索引explain select ...而不是name上发布。更重要的是,PRIMARY语句只会在条目等于SELECT而不是索引someValue的所有条目上发出 S lock 。此外,name所需的 IX lock X lock 将在索引INSERT上发布。 S lock IX lock X lock 之间的冲突将得到解决。

PRIMARY上的索引不仅加快了查询速度,更重要的是阻止了锁定索引的所有条目。

答案 1 :(得分:6)

如果当前的隔离级别为repeatable read或更强,为了能够在事务中为select count(id) ...重复相同的结果,MySQL必须锁定整个主键(或者使用另一个键的一部分)按WHERE条件)。然后通过插入新值来修改密钥。但并发事务会修改密钥的状态,这已经被看到了。两者都可以从密钥的相同状态开始,然后等到另一个完成而没有任何更改,以便它将应用自己的更改。

答案 2 :(得分:6)

  

其中name ='someValue'和timestampdiff(hour,ts,now())&lt; 1;

这是相当低效的。让我们清理一下,以加快速度,降低死锁的可能性。

timestampdiff(hour, ts, now()) < 1隐藏ts的任何索引;让我们把它重写为

ts < NOW() - INTERVAL 1 HOUR

你的意外截断;我说“比1小时前还要大”,我怀疑你想要它。

现在我们可以将ts指向良好的效果。但是让我们通过使用“复合”索引进一步实现它:

INDEX(name, ts)

这将有效地使用WHERE子句的两个部分来定位行。

您说COUNT(id) - 这意味着您需要避免NULLs中的id。也许这不是一个问题,你可以简单地说COUNT(*)

那些应该使SELECT更快。现在让我们弄清楚为什么SELECTINSERT彼此有任何关系。他们在同一笔交易中吗?或者您是否已关闭自动提交,但忘记说COMMIT?请向我们展示整个交易,以及SHOW CREATE TABLE

答案 3 :(得分:1)

BEGIN END 交易中写下您的每个查询。我希望不会发生。

更多:here

答案 4 :(得分:0)

查询的书面部分似乎是正确的&amp;绝对不是你问题的根源。我猜你的任务正在执行交错,在开始时,每个任务都会启动一个事务。您没有说明如何执行这些任务以及执行每个任务时主键值是什么? 您可能希望将主键字段更改为AUTO_INCREMENT,或确保任务使用的主键确实是唯一的。
如果它没有帮助,另一种(但不建议的)解决方案是保护你的程序在上层用一个互斥体调用代码。