首先在说明间隙锁之前,我们说下mysql中的幻读

假设具有一张表 t

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);

并插入了一些初始化的数据

那么,考虑如下的语句的执行,是如何加锁的(在可重复读隔离级别下)

begin;

select * from t where d=5 for update;

commit;

首先说,这个语句会命中d=5的这一行,然后加一个写锁,然后这个写锁会在commit的时候进行释放

其次,由于字段d没有索引,然后会进行一个全表扫描

然后重点来了,其他的行再扫描过程中会不会加锁呢

如果所有行都不加锁

图片

执行如上语句会出现如下的问题

由于T1,T3,T5时刻都是查询的当前读,会导致将新修改的和新插入的数据一并读出来

这样就导致了幻读问题,即为同一个事务中前后两次查询同一个范围,却有新的数据出现

还会带来破坏锁的问题

sessionA在T1时刻声明了,将所有d=5的行锁住,但是后来插入了两条新的,如果对这两条新的数据进行修改的话,那么就破坏了原本的锁住所有d=5的数据

然后还会破坏数据的一致性

锁的设置是为了保证数据的一致性,但是如果出现了幻读问题,可能会带来日志上的不一致

如果按照如下进行更新

图片

而且没有加上锁的话

在主库中,出现如下的情况

T1之后,id=5变成了5,5,100

T2之后,id = 0 变成了 0,5,5

T4过后,多了一行 1,5,5

sessionA原本想要将d=5修改的语义也不符了

那么不加任何的锁,这个设定并不合理,需要改,改为了将扫描过程中碰到的行,加上写锁

图片

这样就能阻止session B妄想再修改id=0这一行了,

必须等到T6时刻后才能继续修改了

但是也可以看出来,把所有的记录都加上了锁,仍然阻止不了新插入的记录,虽然把所有的行都加上了锁,但是插入新的数据的这一操作,是对记录之间的间隙进行的操作,所以想要解决幻读问题,需要使用间隙锁

比如这个表中,有六个数据,d索引就有这七个间隔

图片

然后在进行执行select * from t where d = 5 for update的时候,还会加上了间隙锁

然后说明下行锁和间隙锁的区别

行锁是具有区分的,分为读锁和写锁

图片

而间隙锁之间不会出现冲突,其目的都是为了保护这个间隔不被插入新的数据,所以之间不出现冲突

间隙和行锁组合起来称为next-key lock,那么这个被锁住的表上共有七个next-key-lock为

(-无穷,0](0,5)一直到(25,+supermum)

但是出现了新的问题,假设一个语句这样执行

图片

由于间隙锁 (5,10]的存在

sessionB的插入被A的间隙所阻塞了

sessionA的插入被B的间隙锁阻塞了

导致了死锁,因此mysql启动了死锁回滚

于是可以将隔离级别设置了读已提交,避免间隙锁的出现

同样将binlog设置为row,避免日志和数据的不一致

那么关于间隙锁如何加的,接下来我们就说一下

可以简单的分为了,两个”原则” 两个”优化” 一个bug

原则1:加锁的基本单位是 next-key-lock 前开后闭的区间

原则2:查找过程中访问到的对象都加锁

优化1:等值查询,在给唯一索引加锁的时候,next-key-lock 退化为行锁

优化2:向右遍历到最后一个不满足条件的值的时候,next-key-lock 退化为间隙锁

一个bug,唯一索引上的范围查询会访问到不满足条件的第一个值为止

我们拿下面的数据进行举例

CREATE TABLE `t1` (

`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 t1 values(0,0,0),(5,5,5),

(10,10,10),(15,15,15),(20,20,20),(25,25,25);

接下来如何证明

图片

上面的流程中对于id=8的被锁了,id为10的没有锁

可以判断得出

1.根据原则1,加锁的单位是next-key lock session A的加锁范围就是(5,10]

2,根据优化2,这是一个等值查询,会退化为间隙锁(5,10)

所以SessionB这个新插入的记录会被锁住,但是sessionC修改 id=10这行可以的

接下来这情况

图片

这个加锁的方式就比较绕了

根据原则1,需要给(0,5]加上 next-key-lock

然后由于c并不是普通索引,所以不会立刻停止,而是继续向右遍历,查到了c=10,于是给(5,10]加上了索引

根据优化2,退化为了间隙锁(5,10)

于是sessionC中插入一个(7,7,7),会被间隙锁(5,10)锁住

但是由于锁只锁当前索引,而c上有一个索引,所以可以进行update语句

如果想过要绕过索引优化,可以采用如下方法:

1.避免使用lock in share mode,因为其加的是写锁

可以使用for update,执行for update的时候,会给主键索引上满足条件的行加上行锁

2.在查询的时候,多查询几种数据的值,比如使用select *就可以避免使用直接索引

索引范围是如何加锁的

select * from t where id>=10 and id<11 for update

图片

加锁的步骤是

现在(5,10)加上next-key-lock,但由于优化,next-key-lock化为了行锁,只加了一行的行锁

然后是id = 15,于是SessionA的范围就是主键索引上,行锁id=10和next-key lock(10,15]

优化二并没有生效,是因为采用的范围查询

普通索引的范围查询

图片

因为c并非是唯一索引,可能会出现相同的,

导致了在加上(5,10]这一区间锁的时候,因为是非唯一索引,所以并不会退化为行锁,最后SessionA加的是(5,10]和(10,15]这两个next-key lock

导致的插入8失败了

最后看,那个所谓bug的出现,就是如果唯一索引上进行范围查询,会进行额外的搜索

图片

可以看出,实际上仍然往后走了一格,给(15,20]这个区间加上next-key lock

如果一个非主键的索引上存在等值的话,如何去应对的呢

假设

图片

如何应对呢?

主键id不同,但是值都是10

图片

这个加锁的范围为

首先在会加锁到第一个碰见的值,也就是加上一个next key lock (5,10]

然后加上第二个锁(10,10],

然后加上c(10,15],由于是一个等值查询,所以退化为了(10,15)的间隔锁

图片

整体如上

limit语句的加锁能力

进行如下的语句,会加上什么样的锁呢

图片

上面不限制12是因为delete语句中指明了只删除两个limit2的限制,于是在遍历到c(10,30)这一行,就不在往后找一行了

图片

然后是一个死锁的问题

图片

首先是根据lock in share mode 加上了 next-key lock (5,10] 和间隙锁 (10,15)

这时候sessionB理所当然的阻塞了,但是sessionB已经加上了间隙锁,而sessionA在下一时刻需要插入(8,8,8)这一行,发现被sessionB的间隙锁锁住了,发现死锁,进行回滚

这就说明了,加next-key lock是分为两步的,首先加间隙锁,然后加行锁,在加间隙锁的时候是能够成功的,但是在加行锁的时候,发生了锁住

上面说的next-key-lock是由行锁加上间隙锁完成的,如果将隔离级别转为了读已提交的级别的话,就next-key-lock只剩下了行锁的部分

读已提交的情况下,有一个关于update语句的优化

图片

sessionB并不会被b=1的锁进行阻塞

图片

这是因为在读已提交的隔离级别下,update语句有一个semi-consistent的优化

对于update语句,如果碰到了已经锁了的行,会读入最新的版本,然后判断一下是否满足条件,然后进行跳过或者等待

但是对于delete语句,并没有作用

最后,是关于倒序的话,如何进行加锁

图片

由于倒序,会先加上(20,25)和next-key lock(15,20]

然后在索引C上遍历,会一直扫描到c=10,于是next-key lock会加到(5,10]

导致了阻塞了sessionB

发表评论

邮箱地址不会被公开。 必填项已用*标注