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间隙锁的主要内容,如果未能解决你的问题,请参考以下文章