经过上一篇基本锁的介绍,基本上Mysql的基础加锁步奏,有了一个大概的了解。接下来咱们进入最后一个锁的议题:间隙锁。间隙锁,与行锁、表锁这些要复杂的多,是用来解决幻读的一大手段。经过这种手段,咱们不必将事务隔离级别调整到序列化这个最严格的级别,而致使并发量大大降低。读取这篇文章以前,我想,咱们要首先仍是读一下我先前的两篇文章,不然一些理念还真的透彻不了:java
为了进行整个间隙锁的深刻,咱们要构建一些基础的数据,本章咱们都会用构建的基础数据来进行,下面是数据表与索引的创建:sql
create table `t` ( `id` int(11) not null, `c` int(11) DEFAULT null, `d` int(11) DEFAULT null, primary key (`id`), key `c` (`c`) ) ENGINE=InnoDB;
而后咱们插入一些基础的数据:数据库
insert into t values (0,0,0), (5,5,5), (10,10,10), (15,15,15), (20,20,20), (25,25,25);
另外,咱们本次讲解,都是使用默认的数据库隔离级别:可重复读session
好,这个问题,就很关键了!咱们来细说,幻读的具体出现的经典场景。其实很简单,先看下面的具体的复现sql语句:并发
sessionA | sessionB | sessionC | |
---|---|---|---|
t1 | begin; | ||
select * from t where d = 5 for update; | |||
t2 | update t set d = 5 where id = 0; | ||
t3 | select * from t where d = 5 for update; | ||
t4 | insert into t values(1,1,5) | ||
t5 | select * from t where d = 5 for update; | ||
t6 | commit; |
针对这一系列的操做,咱们来一个个分析:mvc
大概上,有两个影响,以下。高并发
select * from t where d = 5 for update;
相似的,咱们这条语句,其实语义上面是想锁住全部d等于5的行数据,让其不能update和insert。然而,咱们接下来的sessionB和sessionC里面,若是没有相关的解决幻读的机制存在,那么都会有问题:测试
-- sessionB增长点操做 update t set d = 5 where id = 0; update t set c = 5 where id = 0;
可见第二条sql已经操做了id等于0,d等于5这一行的数据,与以前的锁全部等于5的行语义上面冲突。优化
这个很关键,涉及到binglog问题。下面是咱们具体操做的sql表格:.net
sessionA | sessionB | sessionC | |
---|---|---|---|
t1 | begin; | ||
select * from t where d = 5 for update; | |||
update t set d = 100 where d = 5; | |||
t2 | update t set d = 5 where id = 0; | ||
update t set c = 5 where id = 0; | |||
t3 | select * from t where d = 5 for update; | ||
t4 | insert into t values(1,1,5); | ||
update t set c = 5 where id = 1; | |||
t5 | select * from t where d = 5 for update; | ||
t6 | commit; |
因为,binglog是要等commit以后,才会记录的(后面文章会有细节的讲解),因此,上面这一系列的sql操做,到了binglog里面会变成下面的样子:
update t set d=5 where id=0; /*(0,0,5)*/ update t set c=5 where id=0; /*(0,5,5)*/ insert into t values(1,1,5); /*(1,1,5)*/ update t set c=5 where id=1; /*(1,5,5)*/ update t set d=100 where d=5;/* 全部 d=5 的行,d 改为 100*/
能够看到,因为咱们前面说,只对id等于5这一行,加了行锁,因此sessionB的操做是能够进行的,因此,最终会发现,咱们sessionA里面的update操做,是最后执行的,若是拿着这个binglog同步从库的话,必然会致使,(0,5,100)、(1,5,100) 和 (5,5,100)这种数据出现,和主库彻底不一致!(主库里面,只有id为5的数据,d才为100)。
那么咱们将全部扫秒到的数据行都加了锁,会如何呢?那么,sessionB里面的第一条update语句将被阻塞,binglog里面的数据以下:
insert into t values(1,1,5); /*(1,1,5)*/ update t set c=5 where id=1; /*(1,5,5)*/ update t set d=100 where d=5;/* 全部 d=5 的行,d 改为 100*/ update t set d=5 where id=0; /*(0,0,5)*/ update t set c=5 where id=0; /*(0,5,5)*/
这样的结果,id为0的这一行的数据,的确能保证数据的一直性,可是,会发现,刚刚插进去的id为1的这同样,在主库里面,d的值为5,可是在从库里面执行了binglog以后,会变成100,又会有不一致的状况出现了!
针对幻读问题,咱们平常理论中常常"背诵"的,是:第三事务隔离级别会出现幻读状况,只有经过提升隔离级别,到最高级别的串行化,能解决幻读这样的问题。可是这样,每个时刻只能有一个线程操做同一个表,并发性大大的下降,根本没法知足,高并发的需求,要知道,Mysql这东西,但是各大顶级互联网公司趋之若鹜的基础数据库,怎么能效率这么差呢?在这里,Mysql就引入了间隙锁的概念。下面咱们来看看,间隙锁如何加锁。
首先,若是咱们使用下面语句进行查询:
select * from t where d = 5 for update;
这样,因为d是没有索引的,那么会走全表查询,默认走的是id的主键索引,按照id的主键值,会产生以下的区间:
例如上面的select语句中,d是没有索引的,因此经过id索引进行全表扫面,又由于是for update,那么,会将表中仅有的六条数据,都加上行锁,而后,针对上面的六个区间,也会加上间隙锁。行锁+间隙锁就是咱们喜闻乐见的:next-key lock了!因此,总体上看也就是7个next-key lock:
这个+∞是能够进行配置的,给每一个索引分配一个不存在的值
前面的文章,咱们彷佛聊过行锁之间的互斥形式:
读锁 | 写锁 | |
---|---|---|
读锁 | 兼容 | 冲突 |
写锁 | 冲突 | 冲突 |
可是间隙锁不是。和间隙锁冲突的,是往这个间隙里面插入一条数据!这一点也是很好的保持并发性的一个挽回。下面看一个操做:
sessionA | sessionB |
---|---|
begin; | |
select * from t where c = 7 lock in share model; | |
begin; | |
select * from t where c = 7 for update; |
虽然,两个事务,都是真对同一条数据,进行可见读的查询,可是并不会阻塞!由于c没有7的这个值,那结果就是,只会在数据库里面加上了(5,10)这个间隙锁,两个可见读并不会由于间隙锁和互斥冲突!
若是这样,加上间隙锁的特性,和行锁的特性,针对上面章节的sql操做:
sessionA | sessionB | sessionC | |
---|---|---|---|
t1 | begin; | ||
select * from t where d = 5 for update; | |||
update t set d = 100 where d = 5; | |||
t2 | update t set d = 5 where id = 0;(阻塞) | ||
update t set c = 5 where id = 0; | |||
t3 | select * from t where d = 5 for update; | ||
t4 | insert into t values(1,1,5);(阻塞) | ||
update t set c = 5 where id = 1; | |||
t5 | select * from t where d = 5 for update; | ||
t6 | commit; |
最终生成的binglog就会是:
update t set d=100 where d=5;/* d 改为 100*/ insert into t values(1,1,5); /*(1,1,5)*/ update t set c=5 where id=1; /*(1,5,5)*/ update t set d=5 where id=0; /*(0,0,5)*/ update t set c=5 where id=0; /*(0,5,5)*/
这样,就解决了数据一致性的问题了,主从库里面都能保持一致。
虽然,间隙锁能比较好的解决上诉咱们探讨的问题,可是同时也会带来些麻烦,要咱们特别的注意。例以下面的操做,是一段业务上面的伪代码:
tx.beginTransaction(); var t = select * from t where id = 9 for update; if(t){ update t set d = 45 where id = 9; }else{ insert into t values(9,5,45); } tx.commit();
(假设id等于9这一行不存在)这段业务逻辑代码,普通状况下,我也常常看到,问题不太会出现,一旦并发量上去了,就会出问题,会形成死锁,下面咱们看看形成死锁的sql执行序列:
sessionA | sessionB | |
---|---|---|
t1 | begin; | |
select * from t where id = 9 for update; | ||
begin; | ||
t2 | select * from t where id = 9 for update; | |
insert into t values(9,5,45);(阻塞,等待sessionA的(5,10)的间隙锁释放) | ||
t3 | insert into t values(9,5,45); (阻塞,等待sessionB的(5,10)的间隙锁释放,死锁!) |
固然,InnoDB的自动死锁检测,会发现这一点,主动将sessionA回滚,报错!
有关于间隙锁,是最后一层级的细节所在,因此在判断是否加、怎么加、是否会阻塞方面,有很是多的考量。接下来咱们来分别来讲一下4个细节,分别对应4个例子,来说讲,首先咱们列出五条规则:
加锁的基本单位是next-key lock,就是针对扫描过的数据进行加间隙锁
先来看看几个sql语句:
select * from t where id = 5 for update; select * from t where id = 10 lock in share model;
两个分别对5和10这两行加了写锁与读锁,可是最开始,再索引树上面,首先加载id为5和10的这两行的时候,加锁步骤以下:
索引上进行等值查询时,给惟一索引加锁的时候,next-key lock退化为行锁
仍是第一条规则的两天语句,发现,id是主键索引(惟一索引),因此去掉了(0,5)(5,10)的这两个间隙锁,因此整个next-key lock变成了单纯的行锁
索引上进行等值查询时,向右遍历,最后一个数值不知足等值的条件的时候,next-key lock退化为间隙锁,就是先后都是开区间
先来看看下面的操做过程:
sessionA | sessionB | sessionC |
---|---|---|
begin; | ||
update t set d = d+1 where id = 7; | ||
insert into t values (8,8,8);(阻塞!) | ||
update t set d = d+1 where id = 10;(成功) |
咱们来分析:
因此根据这个规则,(5,10)这个区间是被锁住额,因此insert会被阻塞,另外10这一行的行锁解除,因此sessionC中的update会成功。
惟一索引的范围查询,会访问到第一个不知足的条件为止
看看下面的操做序列:
sessionA | sessionB | sessionC |
---|---|---|
begin; | ||
select * from t where id > 10 and id <=15 for update; | ||
update t set d = d+1 where id = 20;(阻塞) | ||
insert into t values(16,16,16);(阻塞) |
分析:
这么一看,20这一行被行锁锁住,并且15,20的区间还有间隙锁,因此sessionB和sessionC的操做才会阻塞。
每次加锁,其实都是锁索引树。众所周知,InnoDB的非主键索引的最终叶子节点,都只存储id主键值,而后还要遍历id主键索引,才能搜索出整条的数据,咱们一般将这个过程称之为:回表。固然,若是select的是一个字段,这个字段恰好是id,那么Mysql就不用进行回表查询,由于直接在索引树上就能读取到值,MySQL会进行这种优化,一般咱们称之为:索引下推。根据这个特性,咱们来看看下面的操做序列:
sessionA | sessionB | sessionC |
---|---|---|
begin; | ||
select id from t where c = 5 lock in share model; | ||
update t set d = d+1 where id = 5;(成功) | ||
insert into t values(3,3,3);(阻塞) |
锁,在个人能力范围能,能说的就这么多,具体仍是要用于实践。接下来,打算写很重要的两个日志文件的介绍:binglog和redolog