MySQL间隙锁详细分析

间隙锁是MySQL中防止幻读的机制,主要在RR隔离级别下工作。它锁定索引记录间的间隙,防止其他事务在此区间插入数据。临键锁是记录锁和间隙锁的结合,用于避免幻读。插入意向锁是插入操作的优化,允许并发插入而不产生额外的间隙锁冲突。然而,间隙锁可能导致插入超时和死锁问题,特别是在并发事务中处理不存在的键值时。文章通过示例展示了如何在特定情况下产生死锁,并探讨了其对性能的影响和防止幻读的效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是间隙锁

间隙锁(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 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

### MySQL中的临界锁和间隙锁 #### 一、概念定义 在MySQL中,InnoDB存储引擎支持多种类型的行级锁定机制来保障事务的一致性和隔离级别。其中临界锁(Record Lock)是对索引记录本身加上的锁;而间隙锁(Gap Lock),则是针对索引记录之间的间隔或者第一条记录之前的范围以及最后一条之后的空间施加的锁定措施[^1]。 对于临界锁而言,当某个事务获取了一条特定数据行上的S锁(共享锁)或X锁(独占锁),其他任何试图修改这条记录的操作都将被阻塞直到当前持有该锁的事物完成并释放相应的锁资源为止。这种锁定方式能够有效防止并发更新相同的数据项造成冲突问题的发生[^2]。 至于间隙锁,则主要用于解决幻读现象,在可重复读(Repeatable Read)这一较高隔离级别的环境下尤为常见。通过锁定两个相邻键值间的空白区域,可以阻止新插入的数据进入这个区间从而避免了因后续查询返回不同结果集而导致逻辑错误的情况出现[^3]。 #### 二、使用场景分析 - **临界锁适用场合** - 当应用程序需要确保同一时间只有一个进程能访问某一行具体的数据时; - 对单个实体对象执行写入操作期间保护其不受到来自其它会话的影响。 - **间隙锁应用场景** - 需要在一定范围内维持稳定的结果集合,比如分页显示商品列表时要保持前后页面间商品顺序不变; - 实现乐观/悲观锁控制下的业务流程设计,特别是在高并发条件下维护数据库状态一致性方面发挥重要作用。 #### 三、两者区别对比 | 特性 | Record Lock (临界锁)| Gap Lock (间隙锁) | | --- | --- | ---| | 锁定目标 | 单个具体的索引记录 | 记录之间存在的空间 | | 主要作用 | 控制对现有记录的更改权限 | 防止新的记录插入到已有的记录之间形成幻影行 | | 影响范围 | 只影响指定的关键字对应的那一行 | 能够覆盖整个表内未被占用的位置 | 综上所述,理解这两种不同的锁定策略有助于开发者更好地规划应用架构,并合理设置SQL语句以优化性能表现的同时保证系统的可靠性与稳定性[^4]。 ```sql -- 示例:演示如何利用SELECT ... FOR UPDATE实现临界锁 START TRANSACTION; SELECT * FROM orders WHERE order_id = 100 FOR UPDATE; -- 这里可以安全地对该订单进行处理... COMMIT; -- 或 ROLLBACK; ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值