Mysql间隙锁

Posted 唐伯虎点蚊香dw

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mysql间隙锁相关的知识,希望对你有一定的参考价值。

学习mysql, 总会有一座绕不过去的大山, 那就是锁。理论上,锁的花样再多,也超不出操作系统课上讲的那些范畴,但是Mysql锁让我翻车了。

在Mysql中锁的粒度可分为:表级锁,行级锁,间隙锁 三种。表级锁和行级锁都没什么太难理解的地方。只有间隙锁我无法准确理解其设计意图,而且我试验下来的现象让我觉得很诡异。

那么为什么会有间隙锁这种东西呢,按大部分能查到的资料表示,间隙锁的引入是为了解决在RR隔离级别的幻读问题。

下面来看一个实例,首先创建一个Table:

Create Table: CREATE TABLE `foo` (
  `uid` int(11) NOT NULL,
  `age` int(11) NOT NULL,
  PRIMARY KEY (`uid`),
  KEY `age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into foo values(1,1),(4,4),(7,7),(9,9);

然后,开两个mysql客户端(M1,M2),其执行顺序如下:

M1: begin;
M1: select * from foo were uid > 1 and uid < 5 for update;
M2: begin;insert into foo values(2,2);commit;
M1: select * from foo were uid > 1 and uid < 5 for update;
M1: commit;

如果在M1第一次执行select语句时只加行锁,那么锁住的就只有uid=4这一行。 在M1第二次执行select语句时,由于M2插入了一条(2,2), 因此会多查询出一条(2,2)的记录。 这就会产生幻读。

mysql的解决方案是:使用间隙锁,将uid在间隙区间(1,4),(4,7)的全部加锁,这样当M2在insert行数据(2,2)甚至(6,6)会被锁阻塞以防止M1出现幻读。

如果事情到这里完美结束,那我也不会翻车了,再看另外三条sql语句:

M1: begin;
M1: select * from foo were age = 4 for update;
M2: begin;insert into foo values(6,6);commit;
M1: select * from foo were age = 4 for update;
M1: commit;

手动执行一下就会发现M2会被锁阻塞住,这是因为他对age加了间隙锁(锁是加在索引上的)。

由于锁是加在索引上的, 按照我第一反应,直接对age=4这一条索引加锁就解决问题了,为什么要加间隙锁?

我查了很久,才找到一个很少有人提到但很重要的点二级索引中存储的主键,会参于二级索引排序,比如age索引进行排序时,实际用的是(age,uid)来进行排序。而之所以会使用uid参与排序我想大部分原因应该是B+树内不允许存储相同的值。使用age,uid进行拼接之后可以保证所有的二级索引,在B+树中的值一定是惟一的。

换句话说,我们无法单纯的锁住age=4这一条件,因为可能会存在(age,uid)= (4,1)/(4,2)/(4,5)等任意索引。

二级索引在拼接时,由于age在前uid在后,因此age的值在一定程度上就代表了整个索引值。这也是为什么间隙锁可以锁住age=4这一条件。

为了验证上述说法正确性,来看如下sql:

M1: begin;
M1: select * from foo were age = 4 for update;
M2: begin;update foo set age = 2 where uid = 1;commit;
M1: select * from foo were age = 4 for update;
M1: commit;

先简单分析一下 : 1. age是非惟一二级索引 2. 二级索引在内部实现是由age,uid拼接之后才参与排序的 3. 间隙锁住了(age,uid) = (1,1) ~ (4,4)的开区间 4. M2执行的语句是想插入一个二级索引值(2,1)

根据间隙锁原理,我们可以推段出M2会被间隙锁给阻塞住,而事实也正是这样。

ps. 二级索引中存储的主键会参于二级索引排序,这一点我认为非常重要。不知道为什么很多参考书都有意无意略过去了。

------------------------------------------------------------------------------------------------------------------------------

总结:

Mysql MVCC解决了RR事务的可重复读问题,使用间隙锁解决了RR级别的幻读问题

Mysql的间隙锁是为了在RR级别解决幻读问题而引入的,间隙锁是gap lock ,而mysql 用的是间隙锁和gap锁的结合,也就是next-key lock,而在不同的索引上,mysql加锁的方式也不一样:

唯一索引上:如果条件为=5 ,间隙锁退化为行锁,也就是只会锁住条件中的那一行对象,如果是>5,则会添加一个[5, ∞) 的一个next-key锁,5这个行锁和(5,∞)这个间隙锁

普通索引上:如果条件为5,那么mysql会通过5查询左右两边的一个间隙,也就是比5小的第一个值和比5大的第一个值,然后加一个间隙锁,比如数据库还有两条数据的索引值为 3 和 7,那么mysql会加一个(3-5)[5](5-7]的这么一个间隙锁,为什么普通索引需要这么加,那是因为普通索引是可以重复的,这里引入之前的一句话

二级索引中存储的主键,会参于二级索引排序,比如age索引进行排序时,实际用的是(age,uid)来进行排序。而之所以会使用uid参与排序我想大部分原因应该是B+树内不允许存储相同的值。使用age,uid进行拼接之后可以保证所有的二级索引,在B+树中的值一定是惟一的。

换句话说,我们无法单纯的锁住age=4这一条件,因为可能会存在(age,uid)= (4,1)/(4,2)/(4,5)等任意索引。

二级索引在拼接时,由于age在前uid在后,因此age的值在一定程度上就代表了整个索引值。这也是为什么间隙锁可以锁住age=4这一条件。

        间隙锁是一个左开右闭的一个区间,比如上面的例子,等值查询的时候 where c = 5,那么会加一个(3,7]的一个左开右闭间隙锁,如果我们插入c=3的一条记录是不会阻塞的,但是如果我们插入一条c=7的记录,那是会阻塞的

无索引:因为mysql的锁都是在索引上,如果没有索引则是使用表锁

mysql间隙锁 转

前面一文 mysql锁 介绍了mysql innodb存储引擎的各种锁,本文介绍一下innodb存储引擎的间隙锁,就以下问题展开讨论

1.什么是间隙锁?间隙锁是怎样产生的?

2.间隙锁有什么作用?

3.使用间隙锁有什么隐患?

一、间隙锁的基本概念

1.什么叫间隙锁

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(NEXT-KEY)锁。

2.间隙锁的产生

上面的文字很抽象,现在举个栗子,介绍间隙锁是怎么产生的:

假设有以下表t_student:(其中id为PK,name为非唯一索引)

id name sex address
1 zhaoyi 0 beijin
3 sunsan 1 shanghai
4 lisi 0 guangzhou
5 zhouwu 0 shenzhen
6 wuliu 1 hangzhou

这个时候我们发出一条这样的加锁sql语句:

select id,name from t_student where id > 0 and id < 5 for update;

这时候,我们命中的数据为以下着色部分:

id name sex address
1 zhaoyi 0 beijin
3 sunsan 1 shanghai
4 lisi 0 guangzhou
5 zhouwu 0 shenzhen
6 wuliu 1 hangzhou

细心的朋友可能就会发现,这里缺少了条id为2的记录,我们的重点就在这里。

select ... for update这条语句,是会对数据记录加锁的,这里因为命中了索引,加的是行锁。从数据记录来看,这里排它锁锁住数据是id为1、3和4的这3条数据。

但是,看看前面我们的介绍——对于键值在条件范围内但不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁。

好了,我们这里,键值在条件范围但是不存在的记录,就是id为2的记录,这里会对id为2数据加上间隙锁。假设这时候如果有id=2的记录insert进来了,是要等到这个事务结束以后才会执行的

二、间隙锁的作用

总的来说,有2个作用:防止幻读和防止数据误删/改

1.防止幻读

关于幻读的概念可以参考我这篇文章 https://blog.csdn.net/mweibiao/article/details/80805031 ,这里就不多做解释了

假设有下面场景

时间 事务A 事务B
T1 select count(1) from t_student where id > 1;  
T2   insert into t_student values(2,\'qianer\',1,\'nanjing\');
T3   commit;
T4 select count(1) from t_student where id > 1;  
T5 commit;  

如果没有间隙锁,事务A在T1和T4读到的结果是不一样的,有了间隙锁,读的就是一样的了

2.防止数据误删/改

这个作用比较重要,假设以下场景:

时间 事务A 事务B
T1 delete from t_student where id < 4;  
T2   insert into t_student values(2,\'qianer\',1,\'nanjing\');
T3   commit;
T4 commit;  

这种情况下,如果没有间隙锁,会出现的问题是:id为2的记录,刚加进去,就被删除了,这种情况有时候对业务,是致命性的打击。加了间隙锁之后,由于insert语句要等待事务A执行完之后释放锁,避免了这种情况

三.使用间隙锁的隐患

最大的隐患就是性能问题

前面提到,假设这时候如果有id=2的记录insert进来了,是要等到这个事务结束以后才会执行的,假设是这种场景  

时间 事务A 事务B
T1 select * from t_student where id>1 and id < 100 for update;  
T2   insert into t_student values(2,\'qianer\',1,\'nanjing\');
T3 update t_student set xxxx where id=xxx;  
T4 update t_student set xxxx where id=xxx;  
T5 update t_student set xxxx where id=xxx;  
T6  
T7 commit;  

这种情况,对插入的性能就有很大影响了,必须等到事务结束才能进行插入,性能大打折扣

更有甚者,如果间隙锁出现死锁的情况下,会更隐晦,更难定位

原文 https://www.cnblogs.com/billmiao/p/9872161.html

以上是关于Mysql间隙锁的主要内容,如果未能解决你的问题,请参考以下文章

跑了4个实验,实战讲解 MySQL的行锁间隙锁...

mysql 间隙锁和临键锁原理

mysql间隙锁

mysql间隙锁

Mysql加锁过程详解-innodb下的记录锁,间隙锁,next-key锁

Mysql加锁过程详解-innodb下的记录锁,间隙锁,next-key锁