本文概览:在分析死锁时需要从以下三个方面考虑:

1、那些锁引起的死锁

死锁就是由不同事务的 Recored Lock / GAP lock /Insert Intention Lock依赖顺序形成闭环导致的,主要包括如下两种情况

不同事务Record Lock之间冲突。SelectForUpdate和Select in share mode、update 、delete、insert 都会产生ReocdLock

不同事务的GapLock和Insert Intention Locks之间冲突。Insert Intention Locks只有insert语句才有。

2、执行sql时,如何进行加锁

3、分析不同事务中锁的依赖关系,判断是否形成依赖闭环

附:本小节中用户到数据表结构结构和数据。

1 死锁产生和分析

1、死锁的产生

分为共享锁、互斥锁、意向锁三种类型,这三种类型的互斥兼容关系如下表,正因为有如下冲突关系才导致循环等待,从而触发死锁。

共享锁 排他锁 意向共享锁 意向排他锁
共享锁 兼容 冲突 兼容 冲突
排他锁 冲突 冲突 冲突 冲突
意向共享锁 兼容 冲突 兼容 兼容
意向排他锁 冲突 冲突 兼容  兼容

因为意向类型锁只有在表锁的时候才会使用,所以我们经常遇到都是X和S两种类型的锁。X和S这两种锁类型对应具体的锁有 Recored Lock 和 GAP lock (分别包含X和S两种类型);对于Next-key Lock,它也是由Record和Gap Lock的组合而成的。

GapLock只在执行insert时会被用到,GapLock可以保证不同事务在不能在同一个gap中插入数据,避免出现幻读。对于同一个Gap,GapLock和Record Lock、GapLock和GapLock都是兼容的,GapLock只会跟Insert Intention Locks互斥。如下图

GapLock InsertIntentionLock RecordLock
GapLock 兼容 兼容 兼容
InsertIntentionLock 冲突 兼容 兼容
RecordLock 兼容 兼容  冲突

所以死锁就是由不同事务的 Recored Lock / GAP lock /Insert Intention Lock依赖顺序形成闭环导致的,主要包括如下两种情况

  • 不同事务Record Lock之间冲突(RecordLock-X与RecordLock-S、RecordLock-X与Record-X)。SelectForUpdate和Select in share mode、update 、delete、insert 都会产生ReocdLock.
  • 不同事务的GapLock和Insert Intention Locks之间冲突。Insert Intention Locks只有insert语句才有。

2、在分析死锁时,主要有两点

(1)查看执行的sql语句添加了哪些Recored Lock 和 Gap Lock;

可以参考如下

mysql的innodb锁

(2)根据依赖关系,判断不同事务中这些锁是否可以发生死锁。

2 死锁case

可以针对产生死锁两种情况,举例四个常见的case:

(1)不同事务Record Lock之间冲突(RecordLock-X与RecordLock-S、RecordLock-X与Record-X)

  • 四个非空selectForUpdate
  • delte+两个insert。Recored共享锁和Recored互斥锁导致
  • 三个insert。Recored共享锁和Recored互斥锁导致

(2)不同事务的GapLock和Insert Intention Locks之间冲突

  • 两个空的selectForUpdate+ 两个insert

2.1 四个非空selectForUpdate

1、死锁机理

这种死锁是通过不同事务Record Lock之间冲突

2、死锁的复现

步骤 事务1 事务2
1 begin
2 begin
3

select * from student where id = 1 for update;

获取一行数据

where条件中是主键,此时只有Record Lock

4

select * from student where id = 5 for update;

获取一行数据。

where条件中是主键,此时只有Record Lock

5 select * from student where id = 5 for update;

此时hang住

6

select * from student where id = 1 for update;

ERROR : Deadlock found when trying to get lock; try restarting transaction

通过“show  engine innodb status ”查看死锁信息

3、其他类似情况

除了上面情况,还可以是

  • selectForUpdarte的where中为普通索引和唯一索引时。
  • 对于update/delete,类似selectForUpdate可以复现此种case。

2.2 两个空的selectForUpdate和两个insert

1、死锁机理

这个死锁产生其实就是因为GAP Lock和Insert IntentionLocks 互斥产生的。

2、下面以SelectForUpdate为例复现死锁

步骤 事务1 事务2
1 begin
2 begin
3

mysql> select * from student where id = 2 for update;

Empty set (0.00 sec)

where条件必须是主键,此时为空时,只会上锁GapLock(负无穷,6)

4

mysql> select * from student where id = 3 for update;

Empty set (0.00 sec)

where条件必须是主键,此时为空时,只会上锁GapLock(负无穷,6)

5

mysql> insert into student(id,name,score,des) values (2,’ff2′,10,’test2′);

hang住了。

事务1需要等待事务2的Gap锁(10,22),然后上锁Insert Intention Locks,再获取recoredLock,执行insert

6

mysql> insert into student(id,name,score,des) values (3,’ff3′,10,’test3′);

事务2需要等待事务1的Gap锁(10,22),然后上锁Insert Intention Locks,此时产生循环依赖触发死锁。

Deadlock found when trying to get lock; try restarting transaction

通过“show  engine innodb status ”查看死锁信息

当两个selectForUpdate不再同一个Gap时,此时就不会出现死锁,如下

步骤 事务1 事务2
1 begin
2 begin
3

mysql> select * from student where id = 5 for update;

Empty set (0.00 sec)

上锁GapLock(负无穷,6)

4

mysql> select * from student where id = 8  for update;

Empty set (0.00 sec)

上锁GapLock(6,10)

5

mysql> insert into student(id,name,score,des) values (8,’ff5′,5,’test5′);

可以获取Insert Intention Locks,再获取recoredLock,所以不会阻塞

Query OK, 1 row affected (0.00 sec)

6

mysql> insert into student(id,name,score,des) values (9,’ff8′,8,’test8′);

可以获取Insert Intention Locks,再获取recoredLock,所以不会阻塞

Query OK, 1 row affected (0.00 sec)

3、其他case

除了SelectForUpdate,Update、delete都是可以复现的,关键是对于SelectForUpdate/update/delete的where中条件为主键而且查询结果为空,此时才只会上GapLock。

2.3 三个insert

1、死锁机理

这个死锁产生其实就是因为GAP Lock和Insert IntentionLocks 互斥产生的。

2、复现死锁

步骤 事务1 事务2 事务3
1 begin
2 begin
3 begin
4

insert into student(id,name,score) values (3,’ggg’,12);

获取RecoredLock 排他锁

5

insert into student(id,name,score) values (3,’ggg’,12);

此时hang住了。

尝试获取RecordLock共享锁

6

insert into student(id,name,score) values (3,’ggg’,12);

此时hang住了

尝试获取RecordLock共享锁

7 rollback
8 Deadlock found when trying to get lock; try restarting transaction
执行成功

 通过“show  engine innodb status ”查看死锁信息

(1)为什么在rollback时发生死锁,在commit时不会产生死锁?

事务1 执行commit,事务2 和事务3 在执行insert和尝试获取互斥锁时,首先判断是否已经存在元素了,如果已经存在则此时就会报错”Duplicate entry ‘3’ for key ‘PRIMARY‘” ,不会在取获取互斥锁了。

(2)理解事务2和事务3发生死锁

当存在一个事务A已经执行了insert插入相同的元素,则当前事务B的insert操作会尝试获取一个共享锁,此时会进行等等待,直到A进行commit或者rollback,然后事务1就可以获取到共享锁。所以在上面死锁例子中,当事务1进行rollback之后,事务2和事务3同时获取到共享锁,然后执行insert。如下流程:

步骤 事务1 事务2
1 begin
2 begin
3

select * from student where id =3 lock in share mode;

Empty set (0.00 sec)

获取Record lock 共享锁

4

select * from student where id =3 lock in share mode;

Empty set (0.00 sec)

获取Record lock 共享锁

5

insert into student(id,name,score) values (3,’ggg’,12);

尝试获取RecordLock

此时会hang住

6

insert into student(id,name,score) values (3,’ggg’,12);

Deadlock found when trying to get lock; try restarting transaction

7 执行成功

通过“show  engine innodb status ”查看死锁信息

2.4 delete和两个insert

1、死锁机理

这个死锁产生其实就是因为GAP Lock和Insert IntentionLocks 互斥产生的。

2、复现死锁

步骤 事务1 事务2 事务3
1 begin
2 begin
3 begin
4

delete from student where id = 3 ;

获取RecordLock排他锁

5

insert into student(id,name,score) values (3,’ggg’,12);

此时hang住

尝试获取RecordLock共享锁

6

insert into student(id,name,score) values (3,’ggg’,12);

此时hang住

尝试获取RecordLock共享锁

7 commit
8

获取RecordLock共享锁,尝试获取RecoredLock排他锁,此时需要等待事务2的共享锁

获取RecordLock共享锁,尝试获取RecoredLock排他锁,此时需要等待事务1的共享锁,检测到死锁

Deadlock found when trying to get lock; try restarting transaction

9

insert成功

Query OK, 1 row affected (14.76 sec)

通过“show  engine innodb status ”查看死锁信息

在上面的死锁中,必须保证delete和insert操作记录必须保证主键是一样的,才可能死锁,否则不会产生死锁。这是因为,insert只有跟delete主键一样的时候,insert才会尝试去获取共享锁

3 死锁总结

1、使用SELECT…For update时,where条件中要使用唯一索引或者主键。否则

  • where中使用普通索引,会使用到GAP锁,增加死锁的几率。
  • where中不使用索引,会产生锁表,对所有元素进行上锁RecordLock和对所有gap上锁GapLock。

2、如何避免 Select For Update为空时,造成死锁

  • step1  直接查询结果
  • setp2  如果上面结果为空执行step3,否则执行step4
  • step3 执行insert
  • step4 执行select For Update

(全文完)

分类&标签