死锁的产生是因为满足了四个条件:
- 互斥
- 占有且等待
- 不可强占用
- 循环等待
这个网站收集了很多死锁场景
接下来介绍几种常见的死锁发生场景。其中,id 为主键,no(学号)为二级唯一索引,name(姓名)和 age(年龄)为二级非唯一索引,score(学分)无索引。数据库隔离级别为 RR。
多个事务加锁顺序不一致
两条记录锁,X锁,相互再想获取对方的,会卡住
间隙锁之间虽然不会互相阻塞,但插入意向锁会和间隙锁阻塞
事务A和B先后再(20, 30)的区间上加了间隙锁,此时间隙锁之间是没影响的,因为间隙锁主要是为了防止幻读的发生也就是插入的发生。但是A此时有想插入数据了,是需要在(20, 30)内生成插入意向锁的,但这个区间在B的间隙锁范围内,所以就会冲突。B事务的插入同理。形成了死锁。要解决这个死锁很简单,显然,前面两条 UPDATE 语句是无效的,将其删除即可。另外也可以将数据库隔离级别改成 RC,这样在 UPDATE 的时候就不会有间隙锁了。
忽视范围查找的行锁是一个个加的
虽然只有一条查询语句,看起来是不该有锁的。但要知道在范围查询时,加锁是一条记录一条记录挨个加锁的,所以虽然只有一条 SQL 语句,如果两条 SQL 语句的加锁顺序不一样,也会导致死锁。所以这个和第一个场景其实很像。第一个场景两个事务一个是20 -> 30另一个是30 -> 20,互相等待了。这个也是一样。事务 A 的范围条件为 id < 30,加锁顺序为:id = 15 -> 18 -> 20,事务 B 走的是二级索引 age,加锁顺序为:(age, id) = (24, 18) -> (24, 20) -> (25, 15) -> (25, 49),其中,对 id 的加锁顺序为 id = 18 -> 20 -> 15 -> 49。可以看到事务 A 先锁 15,再锁 18,而事务 B 先锁 18,再锁 15,从而形成死锁。
注意:
很多同学误以为如果是二级索引的「唯一索引」,加锁也是只加在二级索引项上。
其实这是不对的,所以这里特此说明下,如果是用二级索引(不管是不是非唯一索引,还是唯一索引)进行锁定读查询的时候,除了会对二级索引项加行级锁(如果是唯一索引的二级索引,加锁规则和主键索引的案例相同),而且还会对查询到的记录的主键索引项上加「记录锁」。
主键索引和唯一二级索引插入时候要先生成一个S型锁来判断是否唯一,然后才是升级成X型锁
insert正常是通过trx_id来隐式的保护记录的,MVCC其实就是靠的这个。但在主键索引会生成S型记录锁,唯一二级索引则是S型next-key锁
这个博客提供了一个案例,S锁可能会和其他事务的X锁阻塞。
如何解决死锁?
思索的四个条件,其实破坏任意一个都能避免死锁,MySQL常用的是设置事务等待锁的超时时间和开启主动死锁检测。前者设置一个事务等待超过时间阈值就自动回滚(这样锁就释放了另一个事务就可以继续了)。后者则是主动检测发现死锁后会回滚死锁中的一个事务。