Mysql心路历程:Mysql各类锁机制(进阶篇)

经过上一篇基本锁的介绍,基本上Mysql的基础加锁步奏,有了一个大概的了解。接下来咱们进入最后一个锁的议题:间隙锁。间隙锁,与行锁、表锁这些要复杂的多,是用来解决幻读的一大手段。经过这种手段,咱们不必将事务隔离级别调整到序列化这个最严格的级别,而致使并发量大大降低。读取这篇文章以前,我想,咱们要首先仍是读一下我先前的两篇文章,不然一些理念还真的透彻不了:java

1、基础测试表与数据

为了进行整个间隙锁的深刻,咱们要构建一些基础的数据,本章咱们都会用构建的基础数据来进行,下面是数据表与索引的创建: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

2、什么叫幻读

好,这个问题,就很关键了!咱们来细说,幻读的具体出现的经典场景。其实很简单,先看下面的具体的复现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

  • sessionA在t1时刻,可见读的结果是:(5,5,5),d没有索引,因此是全表扫描,对id为5的那一行,加行锁的写锁
  • 因为sessionB再t2时刻,将id为0的数据改了下,因此t3时刻,sessionA的可见读的结果是:(0,0,5),(5,5,5)
  • 因为sessionC再t4时刻,插入了条不存在的数据,因此t6时刻,sessionA的可见读结果是:(0,0,0)(1,1,5)(5,5,5)
  • 若是,咱们不添加for update进行可见读,普通的一致性读的状况下,因为mvcc的建立快照机制的影响,sessionA一直都会只看到(5,5,5)这一条数据
  • update以后,可见读查出来的多一条数据,并非幻读,只有插入以后的可见读,多读出来的数据,才叫幻读。就比如咱们原本有两条原始数据,但是在事务的没结束以前的先后去读,分别读出来2条和3条,多出一条,就好像我在以后读出的3条数据,是幻影同样,忽然出现了,因此叫幻读。
  • 虽然咱们平时几乎不会使用select for update进行查询,可是,要记住,update语句以前就是要进行一次for update的select查询的!

3、幻读会有什么影响

大概上,有两个影响,以下。高并发

一、语义冲突

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,又会有不一致的状况出现了!

4、初入"间隙锁"

针对幻读问题,咱们平常理论中常常"背诵"的,是:第三事务隔离级别会出现幻读状况,只有经过提升隔离级别,到最高级别的串行化,能解决幻读这样的问题。可是这样,每个时刻只能有一个线程操做同一个表,并发性大大的下降,根本没法知足,高并发的需求,要知道,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回滚,报错!

5、晋级"间隙锁"

有关于间隙锁,是最后一层级的细节所在,因此在判断是否加、怎么加、是否会阻塞方面,有很是多的考量。接下来咱们来分别来讲一下4个细节,分别对应4个例子,来说讲,首先咱们列出五条规则:

  • 加锁的基本单位是next-key lock,就是针对扫描过的数据进行加间隙锁
  • 索引上进行等值查询时,给惟一索引加锁的时候,next-key lock退化为行锁
  • 索引上进行等值查询时,向右遍历,最后一个数值不知足等值的条件的时候,next-key lock退化为间隙锁,就是先后都是开区间
  • 惟一索引的范围查询,会访问到第一个不知足的条件为止

一、第一条规则

加锁的基本单位是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的这两行的时候,加锁步骤以下:

  • 加(0,5)和(5,10)这两个间隙锁
  • 加5的这一行的行锁(写锁),加10这一行的行锁(读锁)
  • 因此目前为止,基础加锁的单位为next-key lock

二、第二条规则

索引上进行等值查询时,给惟一索引加锁的时候,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;(成功)

咱们来分析:

  • update以前会进行select for update操做,因此就是对id为7的这一行进行可见读
  • 因为7这行记录不存在,可是7落在了(5,10)这个区间,而根据第一条原则,加锁基本单位是next-key lock,因此加锁会加上(5,10)的间隙锁,和10这一行的行锁(写锁),就是(5,10]
  • 因为最后一条记录10和等值查询中的7并不相等,因此退化成了间隙锁,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);(阻塞)

分析:

  • 因为10不是等值,15是等值,因此10这一条不会加next-key lock,15会,因此首先加上了(10,15]
  • 虽然是惟一索引,可是是区间查询,并不会中止加锁的脚步,会继续向右
  • 找到20这条记录,加上了next-key lock的(15,20]
  • 因为不是等值查询,是范围查询,因此应用不了规则三,因此最终造成的锁是:(10,15],(15,20]

这么一看,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);(阻塞)
  • 只在c这个非惟一索引上,加了行读锁,基础的加锁单位是(0,5],因为是非惟一索引的查询,并不能退化为行锁
  • 因为非惟一索引,要继续往下,加上了(5,10]这一个的next-key lock,因为最右边的最后一个值,和等值查询并不相等,因此退化成间隙锁(5,10),因此sessionC会被阻塞
  • 因为sessionA中的可见读是读锁,而且只查询id的值,因此启动了索引下推优化,只会加c这个索引上面的行锁。若是换成for update,那就会顺便将主键索引上面也加上锁。因此这里要分清两种行锁的粒度。
  • 因此,最后,sessionB能成功的愿意是:主键索引上并无加锁

6、结束

锁,在个人能力范围能,能说的就这么多,具体仍是要用于实践。接下来,打算写很重要的两个日志文件的介绍:binglog和redolog

相关文章
相关标签/搜索