我们大多都知道行锁锁住的是一行数据,也知道怎么避免行锁造成的阻塞语句问题,但是还是有很多复杂情况,去加了很多锁,如间隙锁以及next-key lock,甚至他们的混合锁,如果这个不了解,搞不好就是语句问题以及死锁问题。
今天通过案例直观的了解下这几种锁,以及出现哪些问题,大家可以先不看下每个解答,而想想出现这个原因是为什么,如果答出来了恭喜你,答不出来也不气馁,本章节学完你就会了!
所以先来看一下锁的规则:
我的版本是MySql5.7版本
间隙锁在可重复读隔离级别下有效,所以本篇无特殊说明,则是在可重复读隔离级别。
林晓斌大神总结出来的加锁规则,大家快记下来,就能应对很多加锁的场景了,这个加锁的规则包含两个“原则”、两个“优化”和一个“bug”。
1.原则1:加锁的基本单位是next-key lock(默认加next-key lock锁),next-key是前开后闭区间。
2.原则2:查找过程中访问到的对象才加锁。
3.优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
4.优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
5.一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
请牢记这5条要点,后面会根据此依据看看锁的处理和使用。
我们先来创建个表和数据,来说明的验证下:
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 t VALUES(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25)
案例一:等值查询间隙锁
session A | session B | session C |
# 等值查询间隙锁: begin; update t set d=d+1 where id=7; | ||
insert into t values(8,8,8) (blocked) | ||
update t set d=d+1 where id=10; (query ok) |
我们先执行session A里的语句并不提交事务,然后再执行另一个会话session B,插入一条语句然后被阻塞了,然后在执行session C的更新语句发现执行成功了,
这是为什么,为什么插入语句8被阻塞了,而更新id=10就可以?
1.这是因为由于表中没有id=7的记录,所以用我们上面提到的规则的判断的话,根据原则1我们加的是next-key lock,sission A的加锁范围就是(5,10],也就是左开右闭,为什么是(5,10]?我们看下下面的表记录,表中的数据没有7,我们看7在表里是5到10内,所以证明加next-key lock则是(5,10]
2.同时根据优化2这是一个等值查询id=7,而id=10不满足查询条件,next-key lock退化成间隙锁,因此最终加锁的范围是(5,10)开区间,也就是说这个开区间不包含数字本身,所以session C可以更新。所以sessionB要往这个区间插入id=8的记录会被锁住。
案例二:非唯一索引等值锁
这个例子是关于覆盖索引上的锁:
session A | session B | session C |
begin; select id from t where c=5 lock in share mode; | ||
update t set d=d+1 where id=5; (Query OK) | ||
insert into t value(7,7,7); (blocked) |
感觉应该给session B上加锁,为什么又在session C上加锁呢,有一种该锁的没有锁,不该锁的乱锁?
这里session A上给索引c=5这一行加上读锁,
1.根据原则1,我们用next-key lock,因此会给(0,5]加上next-key lock。
2.因为c是普通索引不是唯一索引,因此访问5不会马上停下来,需要向右遍历到c=10才放弃,根据原则2,访问到的数据都要加锁,因此要给(5,10]加next-key lock。
3.但是同时满足优化2,等值判断,向右遍历,最后一个值不满足c=5这个等值条件,因此退化成间隙锁(5,10),此时session C的语句是7存在在(5,10)的区间,所以被阻塞。
4.根据原则2,只有访问到的对象才加锁,这个查询采用的是覆盖索引,并不需要访问主键索引,所以主键索引上没加任何锁,这就是为什么session B的语句可以执行完成。
注意:在这个例子中 lock in share mode 只锁覆盖索引,但是如果是for update 时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上索引。
同时也是说明,锁是加在索引上的,同时,他给我们的指导是要用 lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引得优化,在查询字段中加入索引中不存在的字段,比如将session A的查询语句改成 select d from t where c=5 lock in share mode;,你可以自己验证下结果,更新的语句也会被阻塞掉。
接下来看一个有趣的案例
案例三:主键索引范围锁
这个案例是关于范围查询的,我们先思考这样两个sql语句,下面的sql语句加锁相同吗?
select * from t where id=10 for update;
select * from t where id>=10 and id < 11 for update;
其实,逻辑是相同的,加锁规则却不一样,我们看看
sessin A | session B | session C |
begin; select * from t where id>=10 and id < 11 for update; | ||
insert into t value(8,8,8); (Query OK) insert into t value(13,13,13); (blocked) | ||
update t set d=d+1 where id=15; (blocked) |
1.开始执行的时候,找到第一个id=10的行,因此本该是next-key lock(5,10],根据优化1,主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁。
2.范围查找就往后继续找,找到id=15这一行停下来,因此需要加next-key lock(10,15],这样session B的第二条语句,session C语句阻塞你就能够理解了,注意,session A定位查找id=10的行的时候,是等值查询来判断的,向右扫描到id=15,用的是范围查询判断。
咱们来看一下同样例子这个sql语句(select * from t where id=10 for update;)锁的是不是和你想象的一样。
session A | session B | session C |
begin; select * from t where id=10 for update; | ||
insert into t value(8,8,8); (Query OK) insert into t value(13,13,13); (Query OK) | ||
update t set d=d+1 where id=15; (Query OK) update t set d=d+1 where id=10; (blocked) |
怎么样是否和你想象的一样呢?这时加的就是行锁。
案例四:非唯一索引范围锁
这个案例与案例三相似,唯一不同的是案例三是主键索引条件,案例四是普通索引,我们来看看
session A | session B | session C |
begin; select * from t where c>=10 and c < 11 for update; | ||
insert into t value(8,8,8); (blocked) | ||
update t set d=d+1 where c=15; (blocked) |
第一次用c=10定位记录时,索引c加上(5,10]这个next-key lock后,由于索引不是唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终加的是(5,10]和(10,15]这两个next-key lock,所以session B和session C被堵住了。
案例五:唯一索引范围锁bug
session A | session B | session C |
begin; select * from t where id>10 and id <= 15 for update; | ||
update t set d=d+1 where id=20; (blocked) | ||
insert into t value(16,16,16); (blocked) |
session B和session C的为什么是这样?
按照规则1的话id索引应该只加(10,15]这个next-key lock,并且因为id是唯一键值,所以循环判断到id=15就可以停止扫描了,但现实是还会向后扫描一个不满足条件为止,这里也就是id=20,而且由于这是个范围扫描,因此索引上也会加(15,20]这个next-key lock也会被锁上。
所以session B就会被堵塞住,按里说id=20这一行是没必要锁住的,因为唯一值只能有一个,作者就认为是个bug,找官方提了bug,但是并未证实。
案例六:非唯一索引上存在“等值”的例子
这个是间隙锁的例子,咱们先插入一个新数据来说明这个例子,这样咱们数据里有两个c字段值为10的了。
insert into t values(30,10,30)
session A | session B | session C |
begin; delete from t where c=10; | ||
insert into t value(12,12,12); (blocked) | ||
update t set d=d+1 where c=15; (Query OK) |
1.session A遍历到c=10的记录,根据原子1,需要加(5,10]的next-key lock锁,
2.然后再继续查找,直到查找到15这一行,循环结束,根据优化2等值查询,查询不满足条件的行,会退化成间隙锁,所以是(10,15),所以最终5,和10本身的更新不会阻塞,处理10会阻塞,在5和15之间插入会被阻塞住。
案例七:limit语句加锁
这个案例和案例6是一个对照,那个执行插入被阻塞,这个插入没有被阻塞,这是怎么一回事呢?
session A | session B |
begin; delete from t where c=10 limit 2; | |
insert into t value(12,12,12); (Query OK) |
由于我们加了limit 2,表里数据有两条,在遍历第二条满足的条以后,就不再继续找了,因此加锁范围就变成了(5,10],这样session B是可以执行成功的。
这个案例也告诉我们,有明确的删除条件我们尽量加limit,不只安全,还减少锁的范围。
案例八:一个死锁的案例
最后一个案例:要说明next-key lock 实际上是间隙锁和行锁加起来的结果。
session A | session B |
begin; select id from t where c=10 lock in share mode; | |
update t set d=d+1 where c=10; (blocked) | |
insert into t value(8,8,8); | |
Deadlock found when trying to get lock; try restarting transaction |
1.session A 启动事务后执行查询语句加 lock in share mode,在索引c上加了next-key lock(5,10]和间隙锁(5,10);
2.session B的update语句也要在索引c上加next-key lock(5,10],进入锁等待;
3.然后session A要再插入(8,8,8)这一行,被sessionB的间隙锁锁住,由于出现了死锁,InnoDB让session B回滚。
你可能会问,session B的next-key lock不是还没申请成功吗?
其实是这样的,session B的“加next-key lock(5,10] ”操作,实际上分成了两步,先是加(5,10)的间 隙锁,加锁成功;然后加c=10的行锁,这时候才被锁住的。
也就是说,我们在分析加锁规则的时候可以用next-key lock来分析。但是要知道,具体执行的时 候,是要分成间隙锁和行锁两段来执行的。
怎么样,是不是悟了呢,此篇博客用时7个半小时,创作不易,点赞,关注不过分吧!!