安然写字的地方

插入死锁的问题

一个事务内大量的insert语句导致事务出现死锁的问题,为什么单纯的insert语句会导致死锁呢? 首先,我们数据库有两种锁,一种叫S锁,也就是sharable lock,共享锁,可以理解为读锁就是共享锁; 一种叫X锁,也就是exclusive lock,排它锁。故名思议,排它锁与其他任何锁都不兼容,而共享锁之间是兼容的,可以理解为写锁就是排它锁。 第二个,还有一种锁,叫意向锁,intend lock,意向锁也分为共享锁和排它锁。用于加在所要加S锁或X锁对象的所有父节点上,也就是说你要获取Y(Y代表S或者X)锁,你先要(在父节点)成功获取IY锁。加锁的顺序是从上往下的,任何一个节点加锁失败,事务都处于等待状态。所谓父子节点可以参考以下图: 你可以理解为意向锁总是要加的,但是除非你被加上了S锁或者X锁,意向锁总是能成功的。

另外第三个概念:隐式锁和显式锁,又是令人蛋碎的概念,隐式锁你可以理解为乐观锁,也就是正常来说不加锁或共享锁,但是遇到冲突则加锁或升级为排它锁。显式锁,那就是真的锁上了。不明白为什么总是要用这么晦涩的术语来描述。

OK,言归正传,我们基于上面这些介绍开始分析为什么会insert出现死锁。

先看看这个表,注意token字段是一个唯一索引

mysql> CREATE TABLE `deadlocktest` (
    ->   `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    ->   `token` varchar(255) NOT NULL,
    ->   PRIMARY KEY (`id`),
    ->   UNIQUE KEY `ux_token` (`token`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected (0.10 sec)

然后,开启三个TRX执行简单的插入操作:

step1:s1-s3 开始,查看锁等待和上锁状态

step2:  s1插入一条记录

mysql> insert into deadlocktest (token) values ('token1');
Query OK, 1 row affected (0.03 sec)

这时的锁、锁等待和事务状态如下:

mysql> select * from performance_schema.data_locks\G
Empty set (0.00 sec)

mysql> select * from performance_schema.data_lock_waits\G
Empty set (0.00 sec)

SHOW ENGINE INNODB STATUS

注意看,表里面并没有显示加锁(这就是传说的隐式锁),但是实际上,s1加了两个锁:一个是表级别的IX锁,一个是行级别的S锁

mysql> select * from performance_schema.data_locks\G
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 15922:1079
ENGINE_TRANSACTION_ID: 15922
            THREAD_ID: 51
             EVENT_ID: 15
        OBJECT_SCHEMA: tech
          OBJECT_NAME: deadlocktest
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140497454685400
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
1 row in set (0.00 sec)
mysql> select * from performance_schema.data_locks;
+--------+----------------+-----------------------+-----------+----------+---------------+--------------+----------------+-------------------+------------+-----------------------+-----------+-----------+-------------+-----------+
| ENGINE | ENGINE_LOCK_ID | ENGINE_TRANSACTION_ID | THREAD_ID | EVENT_ID | OBJECT_SCHEMA | OBJECT_NAME  | PARTITION_NAME | SUBPARTITION_NAME | INDEX_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+--------+----------------+-----------------------+-----------+----------+---------------+--------------+----------------+-------------------+------------+-----------------------+-----------+-----------+-------------+-----------+
| INNODB | 15924:1079     |                 15924 |        53 |       13 | tech          | deadlocktest | NULL           | NULL              | NULL       |       140497454689368 | TABLE     | IX        | GRANTED     | NULL      |
| INNODB | 15924:22:5:2   |                 15924 |        53 |       13 | tech          | deadlocktest | NULL           | NULL              | ux_token   |       140497507137048 | RECORD    | S         | WAITING     | 'token1'  |
| INNODB | 15923:1079     |                 15923 |        52 |       13 | tech          | deadlocktest | NULL           | NULL              | NULL       |       140497454687368 | TABLE     | IX        | GRANTED     | NULL      |
| INNODB | 15923:22:5:2   |                 15923 |        52 |       13 | tech          | deadlocktest | NULL           | NULL              | ux_token   |       140497507132440 | RECORD    | S         | WAITING     | 'token1'  |
| INNODB | 15922:1079     |                 15922 |        51 |       15 | tech          | deadlocktest | NULL           | NULL              | NULL       |       140497454685400 | TABLE     | IX        | GRANTED     | NULL      |
| INNODB | 15922:22:5:2   |                 15922 |        52 |       13 | tech          | deadlocktest | NULL           | NULL              | ux_token   |       140497507127832 | RECORD    | X         | GRANTED     | 'token1'  |
+--------+----------------+-----------------------+-----------+----------+---------------+--------------+----------------+-------------------+------------+-----------------------+-----------+-----------+-------------+-----------+
6 rows in set (0.00 sec)

mysql> select * from performance_schema.data_lock_waits;
Empty set (0.00 sec)

mysql>

step3:s2/s3插入相同的token。

这时的锁、锁等待和事务状态如下:

这里第一个关键:事务1,也就是S1的S锁,原来是看不到的锁,变成X锁,这就是所谓的隐式锁转换为显式锁。 第二个:S2和S3,在相同的记录上等待获取S锁。这就是死锁的关键,如果没有这一步就不会死锁。至于为什么,这里先按下不表,我们先解释下为毛一个插入操作会加入一个S锁:因为在事务被唤醒后,需要检测冲突,没错,因为被挂起的事务知道这一行数据被X锁锁住了,一旦事务被唤醒,那么被锁住的数据就可能被更改,所以需要检测冲突。而冲突检测是通过读取是否存在类似的记录实现的,所以这货加了个S锁请求。从上文的兼容性中我们可以知道,S锁之间是兼容的,所以,一旦S2-S3被唤醒,那么他们都可以得到这个锁。

进入关键的步骤第四步:

s1 rollback;

这时,s2(或者s3)能成功,而剩下的一个因为死锁检测被重置。当然,前者能成功就是因为后者被重置了。

s2

mysql> insert into deadlocktest (token) values ('token1');
Query OK, 1 row affected (14.86 sec)
mysql>

s3

mysql> insert into deadlocktest (token) values ('token1');
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
mysql>

顺理成章地,数据库爆出了死锁和成功者的加锁信息:

OK,答案显而易见了。

首先,由于冲突检测,后面的家伙都申请了兼容的S锁,导致他们被唤醒后都获取到了这个S锁,经过冲突检测,他们惊喜的发现可以继续了,然后尝试加上不兼容的锁(IX锁),于是杯具发生了,要成功获取(IX)锁,都要等待对方先释放不兼容的S锁,于是死锁发生了。

commit就不会失败,因为commit后数据落地,两边拿到S锁发现冲突,自然就插入失败了,没有后续加IX锁的行为,也就没有死锁了

另一个死锁案例

CREATE TABLE `tt` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `tid` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_tid` (`tid`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
+----+------+-----+
| id | name | tid |
+----+------+-----+
|  1 | a    |   1 |
|  2 | b    |   2 |
|  4 | t    |   4 |
|  5 | c    |   5 |
|  6 | c    |  16 |
|  7 | c    |  17 |
+----+------+-----+

gap锁: 简单的理解就是执行 delete from tt where tid = 7,tid存在一个索引,mysql根据索引进行搜索,7不在这些key里面而是位于(5,16)间隙中,对(5,16)这个间隙加的锁就叫做Gap锁。 Insert Intention锁: 插入意向锁,insert语句会对插入的行加一个X锁,但是在插入这个行的过程之前,会设置一个Insert intention锁。

存在Insert Intention 锁时,申请Gap锁是允许的;但是存在Gap锁时,申请Insert Intention锁时是被阻止的。

T1:delete from tt where tid = 7; T2:delete from tt where tid = 8; T1:insert into tt values(null,‘a’,8); T2:insert into tt values(null,‘b’,7);

T1 持有了Gap(5,16)的X锁; T2 申请Gap(5,16)的X锁,该申请被授权,所以T2 持有了Gap(5,16)的X锁。 T1 申请Insert Intention(5,16)的X锁,根据之前讲的互斥关系,由于T2持有Gap(5,16)的X锁,该申请被block。 T2 申请Insert Intention(5,16)的X锁,根据之前讲的互斥关系,由于T1持有Gap(5,16)的X锁,该申请被block。

死锁很明显的出现了,T1与T2都持有一个锁,同时都在等对方释放一个锁。

相关链接