为什么我的MySQL出现了死锁?

17

我的MySQL表中发生了死锁。只涉及单个表,我可以重现它。只有在运行多个线程的代码时才会发生。

这是表格:

CREATE TABLE `users_roles` (
  `role_id` bigint(20) NOT NULL,
  `user_id` bigint(20) NOT NULL,
  `created` datetime NOT NULL,
  PRIMARY KEY (`user_id`,`role_id`),
  KEY `created` (`created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

然后,我在每个线程中运行这两个查询,并为每个线程设置不同的user_id值。

BEGIN;
DELETE FROM `users_roles` WHERE user_id = X;
INSERT INTO `users_roles` VALUES (7, X, NOW()); -- DEADLOCK ON THIS QUERY
COMMIT;

需要注意的是,当调用DELETE语句时,用户ID为X在数据库中从未存在。运行这些查询的代码段用于创建新用户。但是,该函数允许我修改用户所在的帐户,并因此从旧用户团队中删除现有角色。

因此,当足够多的这些查询并行运行时,就会出现死锁。InnoDB状态的死锁部分在每次死锁后显示了以下内容。

------------------------
LATEST DETECTED DEADLOCK
------------------------
2014-05-09 16:02:20 7fbc99e5f700

*** (1) TRANSACTION:
TRANSACTION 6241424274, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 6 lock struct(s), heap size 1248, 3 row lock(s), undo log entries 6
MySQL thread id 3772090, OS thread handle 0x7fbc1f451700, query id 4010665755 10.0.141.36 1403_users update

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1331 page no 10665 n bits 192 index `PRIMARY` of table `users_data`.`users_roles` trx id 6241424274 lock_mode X insert intention waiting

*** (2) TRANSACTION:
TRANSACTION 6241424275, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
6 lock struct(s), heap size 1248, 3 row lock(s), undo log entries 6
MySQL thread id 3770297, OS thread handle 0x7fbc99e5f700, query id 4010665767 10.0.137.28 1403_users update
INSERT INTO users_roles(role_id, user_id, created) values(5, 102228093, NOW()) ON DUPLICATE KEY UPDATE user_id=user_id

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 1331 page no 10665 n bits 192 index `PRIMARY` of table `users_data`.`users_roles` trx id 6241424275 lock_mode X

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 1331 page no 10665 n bits 192 index `PRIMARY` of table `users_data`.`users_roles` trx id 6241424275 lock_mode X insert intention waiting

*** WE ROLL BACK TRANSACTION (2)
关于调试或尝试找到问题实际所在,我已经通过从代码中删除 DELETE 语句来消除了所有死锁。虽然这确实解决了问题,但我想理解它。
我了解 MySQL 处理间隙锁的方式。我知道它们在此问题中起作用,因为当我执行 DELETE 语句时,行不存在。我不明白的是 innodb 状态中的两个事务都是由相同的代码生成的,但只有一个事务(2)具有独占锁。就像事务(1)甚至没有尝试获取该独占锁(没有插入意图)一样。
假设锁定正确,我可以理解死锁的原因:事务(2)获得了独占锁,事务(1)请求插入意图,然后事务(2)请求插入意图。这很有道理。不合理的是事务(1)缺少一个独占锁(没有插入意图)。
编辑:
我能够使用特定的命令顺序重现这个问题。
以下是表格:
CREATE TABLE `a` (
  `id` tinyint(3) unsigned NOT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

以下是查询语句。打开4个MySQL终端,并按照以下顺序执行这些查询。

session 1: BEGIN;
session 2: BEGIN;
session 3: BEGIN;
session 4: BEGIN;
session 1: DELETE FROM `a` WHERE `id` = 5;
session 2: DELETE FROM `a` WHERE `id` = 10;
session 3: DELETE FROM `a` WHERE `id` = 7;
session 4: DELETE FROM `a` WHERE `id` = 12;
session 1: INSERT INTO `a` VALUES (5, 1);
session 2: INSERT INTO `a` VALUES (10, 1); -- deadlock here
session 3: INSERT INTO `a` VALUES (7, 1); -- deadlock here
session 4: INSERT INTO `a` VALUES (12, 1); -- deadlock here

这是在任何插入之前的InnoDB状态。

------------
TRANSACTIONS
------------
Trx id counter 11396965
Purge done for trx's n:o < 11396913 undo n:o < 0 state: running but idle
History list length 1248
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 11396962, ACTIVE 9 sec
2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 3425, OS thread handle 0x7fcd14197700, query id 29686 localhost dev cleaning up
TABLE LOCK table `matthew`.`a` trx id 11396962 lock mode IX
RECORD LOCKS space id 7291 page no 3 n bits 80 index `PRIMARY` of table `matthew`.`a` trx id 11396962 lock_mode X
---TRANSACTION 11396961, ACTIVE 10 sec
2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 3426, OS thread handle 0x7fccda225700, query id 29673 localhost dev cleaning up
TABLE LOCK table `matthew`.`a` trx id 11396961 lock mode IX
RECORD LOCKS space id 7291 page no 3 n bits 80 index `PRIMARY` of table `matthew`.`a` trx id 11396961 lock_mode X
---TRANSACTION 11396960, ACTIVE 11 sec
2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 3391, OS thread handle 0x7fccd4d7f700, query id 29672 localhost dev cleaning up
TABLE LOCK table `matthew`.`a` trx id 11396960 lock mode IX
RECORD LOCKS space id 7291 page no 3 n bits 80 index `PRIMARY` of table `matthew`.`a` trx id 11396960 lock_mode X
---TRANSACTION 11396959, ACTIVE 13 sec
2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 3392, OS thread handle 0x7fccd4bf9700, query id 29671 localhost dev cleaning up
TABLE LOCK table `matthew`.`a` trx id 11396959 lock mode IX
RECORD LOCKS space id 7291 page no 3 n bits 80 index `PRIMARY` of table `matthew`.`a` trx id 11396959 lock_mode X

第一次插入调用后,会话1的终端在锁定状态下挂起。 InnoDB状态显示:

---TRANSACTION 11396959, ACTIVE 841 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 376, 2 row lock(s)
MySQL thread id 3392, OS thread handle 0x7fccd4bf9700, query id 30234 localhost dev update
------- TRX HAS BEEN WAITING 32 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7291 page no 3 n bits 80 index `PRIMARY` of table `matthew`.`a` trx id 11396959 lock_mode X insert intention waiting
------------------
TABLE LOCK table `matthew`.`a` trx id 11396959 lock mode IX
RECORD LOCKS space id 7291 page no 3 n bits 80 index `PRIMARY` of table `matthew`.`a` trx id 11396959 lock_mode X
RECORD LOCKS space id 7291 page no 3 n bits 80 index `PRIMARY` of table `matthew`.`a` trx id 11396959 lock_mode X insert intention waiting

InnoDB 使用哪种锁定模式?如果您使用多粒度锁定,那可能会导致额外的死锁。请参考:http://dev.mysql.com/doc/refman/5.6/en/innodb-lock-modes.html。 - user645280
我没有其他信息,只有innodb状态。看起来事务(2)以X模式持有锁定,事务(1)和(2)都希望以插入意图的X模式锁定。我假设您想要其他内容,因为这两个都在我的原始问题输出中。您能否更具体地说明,或给我一个示例? - mbarlocker
我的意思是...在my.ini(和相关文件)中如何配置InnoDB?你的日志似乎暗示了表锁和记录锁都是活动的。你能否只开启其中一个并再次尝试?你绝对需要这两个级别吗? - user645280
1个回答

13

我认为我找到了问题所在。

根据http://dev.mysql.com/doc/refman/5.6/en/innodb-locks-set.html

在插入行之前,会设置一种称为插入意向间隙锁的间隙锁类型。这个锁信号表明以这样的方式插入,即如果它们没有插入到间隙的同一位置,则插入相同索引间隙的多个事务无需等待彼此。假设有值为4和7的索引记录。尝试插入5和6的单独事务在获得插入的行的排他锁之前使用插入意图锁锁定4和7之间的间隙,但不会互相阻止,因为行是非冲突的。

如果发生重复键错误,则会设置重复索引记录上的共享锁。如果另一个会话已经拥有独占锁,则此共享锁的使用可能导致死锁,因为有多个会话尝试插入相同的行。如果另一个会话删除该行,则可能会发生这种情况。

对于我的案例,其含义如下。

删除操作因未影响任何行而在表末尾间隙上获得了共享锁(模式IX)。一旦执行插入操作,所有线程仍然持有该共享锁,并且插入意向锁需要等待此共享锁的释放。

解决方案是不要并行执行以下操作:

  1. 在行不存在时删除要插入的行。
  2. 插入行

因此,InnoDB引擎状态仅仅是错误的。它未能显示每个事务持有相同的锁。它未能显示每个锁是lock_mode IX,而不是X。它未能显示每个线程还有一个插入意向锁正在等待授予。总之,这是SHOW ENGINE INNODB STATUS;的一个非常出色的失败。


我认为“X intention waiting”等同于IX。 - Michael - sqlbot
我同意你的观点,迈克尔。那些没有“意向等待”标识的锁让我感到困惑,因为它们应该显示为共享锁或 IX 锁。而那些标有“意向等待”标识的锁则非常明显。 - mbarlocker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接