什么是间隙锁
间隙锁(Gap Lock):间隙锁是(RR级别下)一个在索引记录之间的间隙上的锁,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间
首先要明确,间隙锁是为了防止幻读而产生的锁。在RR下才生效,在RC下锁算法就是记录锁,而在RR情况间隙锁会生效,虽然RR也不能完全避免幻读,因为直接select并不上锁
间隙锁的作用:
间隙锁是MySQL行锁的一种,与行锁不同的是间隙锁可能锁定的是一行数据,也可能锁住一个间隙。锁定规则如下:
当修改的数据存在时,间隙锁只会锁定当前行。
当修改的数据不存在时,间隙锁会向左找第一个比当前索引值小的值,向右找第一个比当前索引值大 的值(没有则为正无穷),将此区间锁住,从而阻止其他事务在此区间插入数据。
比如事务A update table ···· where id >100
那么加上这个锁,不仅id大于100的行记录上锁,还能保证id>100区间在 A未提交之前不能插入新的数据。
间隙锁是封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。
产生间隙锁的条件(RR事务隔离级别下;):
使用普通索引锁定;
使用多列唯一索引;
使用唯一索引锁定多行记录。
以上情况,都会产生间隙锁,
当我们用范围条件而不是相等条件索引数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项枷锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。
InnoDB也会对这个“间隙”枷锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
临键锁
临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
注:临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
插入意向锁
插入意向锁是对插入操作的优化,仅仅插入数据时不会产生对某个区间的间隙锁,而是检测区间是否正在被间隙锁锁定,如果是会被阻塞,否则进行插入。是对间隙锁在插入数据这个场景的优化
(1)插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生。
(2)在多事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。
(3)假设有一个记录索引包含键值4和7,,在没用间隙锁,行锁,等其他锁的情况下,不同的事务分别插入5和 6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。
(4)插入意向锁不会阻止任何锁,但是会被其他锁阻止,例如间隙锁,对于插入的记录会持有一个记录锁。
插入意向锁实际上就是一种间隙锁,不过这种间隙锁不会阻塞其他锁,是对间隙锁针对插入的优化。
间隙锁的危害
因为Query执行过程中通过范围查找的话,他会锁定整个范围内所有的索引键值,即使这个键值并不存在。间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,也造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。
间隙锁与导致超时
最近用户反馈说系统老是出现insert时,等待超时了,最后发现是insert间隙锁!间隙锁是innodb中行锁的一种, 但是这种锁锁住的却不止一行数据,他锁住的是多行,是一个数据范围。间隙锁的主要作用是为了防止出现幻读,但是它会把锁定范围扩大,
有时候也会给我们带来麻烦,我们就遇到了。 在数据库参数中, 控制间隙锁的参数是:
innodb_locks_unsafe_for_binlog
这个参数默认值是OFF, 也就是启用间隙锁, 他是一个bool值, 当值为true时表示disable间隙锁。
那为了防止间隙锁是不是直接将innodb_locaks_unsafe_for_binlog设置为true就可以了呢? 不一定!
而且这个参数会影响到主从复制及灾难恢复, 这个方法还尚待商量。
间隙锁导致死锁
情况一(两个事务互相锁住对方区间)
我们先准备一个表mysql> select * from t_gap_lock;
±—±-------±-----+
| id | name | age |
±—±-------±-----+
| 1 | 张一 | 21 |
| 5 | 李五 | 25 |
| 6 | 赵六 | 26 |
| 9 | 王九 | 29 |
| 12 | 十二 | 12 |
±—±-------±-----+
表中的id数据咱们准备了三个间隙:
- 间隙一:1-5
- 间隙二:6-9
- 间隙三:12-正无穷
1、此时我们开启事务一,然后执行更新id=3的数据,按照咱们的理论,id=3这个数据不存在,说明它会在1-5之间加间隙锁。
2、然后我们开启事务二,然后执行更新id=7的数据,按照咱们的理论,id=7这个数据不存在,说明它
那么重点来了,此时我们需要做的操作就是让事务一在6-9之间插入数据,会发现此时事务已经被阻塞,无法执行insert,因为事务二已经对该区间加了间隙锁
在事务一等待锁的同时,咱们让事务二同时在1-5之间插入数据,这个时候会发现,只要事务二一执行插入。MySQL立即报了死锁,我们就会见到如下提示:
[40001][1213] Deadlock found when trying to get lock; try restarting transaction 。
整个死锁过程进行原理分析
1、首先事务一开启事务后,更新id=3的数据,此数据不存在,所以事务一会锁住1-5这个间隙,即为1-5这个间隙添加间隙锁,同理,事务二会为6-9这个间隙添加间隙锁;
2、然后我们让事务一在6-9这个间隙插入数据,因为事务二已经加了间隙锁,所以事务一需要等待事务二释放间 隙锁才能进行插入操作,此时事务一等待事务二释放间隙锁;
3、同理,事务二在1-5间隙插入时需要等待事务一释放间隙锁,两个事务相互等待,死锁产生。
那么咱们此时就能大概明白最初那个Mybatis-plus的saveOrUpdate方法为什么会造成间隙锁死锁的问题,也就是线上存在两个并发事务,然后更新的时候都没有更新到,此时都在自己的间隙加了间隙锁,然后再到彼此的区间进行数据插入,此时就会造成两个事务互相等待对方的释放间隙锁,从而导致死锁。也许有同学会想,线上的数据几乎不可能刚好会存在1-5,6-9这种间隙,来给并发事务各自加锁,又刚好到彼此区间插入数据的场景,所以我们就会有接下来验证间隙锁加锁是非互斥的,再一次深度还原间隙锁死锁的场景。
情况二(间隙锁非互斥)
验证间隙锁加锁非互斥
然以t_gap_lock为例
mysql> select * from t_gap_lock;
±—±-------±-----+
| id | name | age |
±—±-------±-----+
| 1 | 张一 | 21 |
| 5 | 李五 | 25 |
| 6 | 赵六 | 26 |
| 9 | 王九 | 29 |
| 12 | 十二 | 12 |
±—±-------±-----+
1、此时咱们开启事务一,然后执行更新id=13的数据,按照咱们的理论,id=13这个数据不存在,说明它会在13-正无穷(因为当前索引树上没有比13更大的值)之间加间隙锁。
2、然后我们开启事务二,然后也执行更新id=13的数据,按照咱们的理论,事务二也会对13-正无穷之间加间隙锁
3、那么重点来了,此时我们需要做的操作就是让事务一在13-正无穷之间插入数据,会发现此时事务已经被阻塞,无法执行insert,因为事务二已经对该区间加了间隙锁。
4、在事务一等待锁的同时,咱们让事务二同时在13-正无穷之间插入数据,这个时候会发现,只要事务二一执行插入。MySQL立即报了死锁,我们就会见到如下提示:
[40001][1213] Deadlock found when trying to get lock; try restarting transaction 。
因为咱们已经用1-5以及6-9这种明显的间隙还原了间隙锁死锁,所以13-正无穷发生间隙锁死锁的原理与其无异,这里有个非常大的区别就是事务一已经在13-正无穷加了间隙锁,事务二依然可以对此间隙加间隙锁,所以我们用实际证明了间隙锁加锁是非互斥的。此时咱们回忆一下Mybatis-plus的saveOrUpdate方法,发现线上只要出现两个并发事务去修改同一条不存在的数据,就会立马出现间隙锁死锁。
情况三(当修改数据存在时,间隙锁只会锁住当前行)
select * from t_gap_lock;
±—±-------±-----+
| id | name | age |
±—±-------±-----+
| 1 | 张一 | 21 |
| 5 | 李五 | 25 |
| 6 | 赵六 | 26 |
| 9 | 王九 | 29 |
| 12 | 十二 | 12 |
±—±-------±-----+
1、此时我们开启事务一,然后执行更新id=12的数据,按照咱们的理论,id=12这个数据存在,说明MySQL只会锁定id=12这一行数据。
2、然后我们开启事务二,然后执行更新id=13的数据,按照咱们的理论,id=13这个数据不存在,说明它会在13-正无穷(因为当前索引树上没有比13更大的值)之间加间隙锁
3、那么重点来了,此时我们需要做的操作就是让事务一在13-正无穷之间插入数据,会发现此时事务已经被阻塞,无法执行insert,因为事务二已经对该区间加了间隙锁。
4、在事务一等待锁的同时,咱们让事务二在12-正无穷之间插入数据,这个时候会发现,事务二能够正常插入,说明事务二没有被间隙锁阻塞,待事务二提交或回滚后,事务一也正常提交。
5、通过以上验证,MySQL在更新id=12,即数据存在时,并没有对12-正无穷添加间隙锁,而是只锁定了4、id=12这一行数据,从而降低锁的颗粒度以提高性能。
间隙锁能够防止幻读吗?
那么我们说RR级别下的幻读是怎么产生的呢
Mysql官方给出的幻读解释是:只要在一个事务中,第二次select多出了row就算幻读。
a事务先select,b事务insert确实会加一个gap锁,但是如果b事务commit,这个gap锁就会释放(释放后a事务可以随意dml操作),a事务再select出来的结果在MVCC下还和第一次select一样,接着a事务不加条件地update,这个update会作用在所有行上(包括b事务新加的),a事务再次select就会出现b事务中的新行,并且这个新行已经被update修改了,实测在RR级别下确实如此。
可见如果在RR隔离级别,快照读情况下,间隙锁与MVCC同时发挥作用的情况下,是无法完全防止幻读的
如果select for update时上锁,那么就会自然加上gap锁,那么就可以避免幻读
针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁方式解决了幻读),因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。