初学MySQL—加锁规则

坚强是说给别人听的谎言 提交于 2019-11-27 12:40:20


间隙锁在可重复读隔离级别下才有效,本篇博文默认是可重复隔离级别

加锁规则

总结为两个“原则”、两个“优化”和一个“bug”

  1. 原则1:加锁的基本单位是next-key lock,前开后闭区间;
  2. 原则2:查找过程中访问到的对象都会加锁;
  3. 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁;
  4. 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,nextkeylock 退化为间隙锁;
  5. 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止;

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

案例一:等值查询间隙锁

等值查询的间隙锁
加锁规则判断:

  1. 表中没有id=7记录,根据原则1,加锁的单位是next-key lock,sessionA的加锁范围是(5,10];
  2. 根据优化2,这是一个等值查询(id=7),id=10不满足查询条件,next-key Lock退化成间隙锁,最终加锁的范围就是(5,10);

案例二:非唯一索引等值锁

非唯一索引等值锁
sessionA给索引c=5这一行加了读锁;

  1. 根据原则1,加锁单位是next-key lock,因此会给(0,5]加上next-key lock;
  2. c是普通索引,向右遍历查到c=10才放弃,根据原则2,访问到的都要加锁,索引(5,10]加next-key lock;
  3. 符合优化2,等值判断,向右遍历,最后一个值不满徐足c=5这个等值条件,退化成间隙锁(5,10);
  4. 根据原则2,只有访问到的才加锁,查询采用的覆盖索引,并不需要访问主键索引,主键索引上没有任何锁,所以sessionB的update语句可以执行;

sessionC要插入(7,7,7)就会被sessionA的间隙锁(5,10)挡住

案例三:主键索引范围锁

主键索引范围锁

  1. 开始执行的时候找到第一个id=10的行,根据原则1,next-key lock(5,10]退化成行锁,只加了id=10这一行的行锁;
  2. 范围查找向后继续查找,找到id=15这一行停下来,需要加next-key lock(10,15]。

所以sessionB和sessionC都被阻塞。

案例四:非唯一索引范围锁

非唯一索引范围锁
sessionA的加锁场景,索引c添加了(5,10]和(10,15]这两个next-key lock
所以sessionB和sessionC的操作都被阻塞;

案例五:唯一索引范围锁bug

唯一索引范围锁
sessionA是一个范围查询,根据原则1,在索引d上只加了(10,15]这个next-key lock,id是唯一键,循环判断到id=15这一行停止;

实现上,InnoDB会往前扫描到第一个不满足条件为止,所以索引id上(15,20]这个next-key lock也会被锁上;

所以sessionB和sessionC的操作都会被锁住;

案例六:非唯一索引上存在“等值”的例子

insert into t values(30,10,20);

新插入的这一行c=10,现在表中存在两个c=10的行
非唯一索引等值的例子
虽然有两个c=10,但是主键值id是不同的(分别是10和30),因此两个10之间也是有间隙的
采用delete语句来验证:
delete示例
sessionA遍历的时候是(c=5,id=5) 到 (c=10,id=10) 这个 next-key lock,session A 向右查找,直到碰到 (c=15,id=15) 这一行,循环才结束;

根据优化2,等值查询,向右查找到了不满足条件的行,所以会退化成(c=10,id=10) 到
(c=15,id=15) 的间隙锁;

也就是说,这个 delete 语句在索引 c 上的加锁范围,就是下图中蓝色区域覆盖的部分
delete 加锁效果示例
蓝色区域是虚线,表示开区间,即(c=5,id=5) 和 (c=15,id=15) 这两行上都没有锁;

案例七:limit加锁

图 9 limit 语句加锁
和案例六相比加锁的效果不同,可以看到,session B 的 insert 语句执行通过了,跟案例六的结果不同。

案例七里的 delete 语句明确加了 limit 2 的限制,因此在遍历到 (c=10,id=30) 这一行之后,满足条件的语句已经有两条,循环就结束了。

因此索引 c 上的加锁范围就变成了从(c=5,id=5) 到(c=10,id=30) 这个前开后闭区间。
图 10 带 limit 2 的加锁效果
可以看到,(c=10,id=30)之后的这个间隙并没有在加锁范围里,因此 insert 语句插入
c=12 是可以执行成功的。

在删除数据的时候尽量加limit,不仅可以控制删除数据的条数,还可以让数据更加安全,减少加锁的范围。

案例八:一个死锁的例子

next-key lock 实际上是间隙锁和行锁加起来的结果
图 11 案例八的操作序列

  1. session A 启动事务后执行查询语句加 lock in share mode,在索引 c 上加了 nextkey
    lock(5,10] 和间隙锁 (10,15);
  2. session B 的 update 语句也要在索引 c 上加 next-key lock(5,10] ,进入锁等待;
  3. session A 要再插入 (8,8,8) 这一行,被 session B 的间隙锁锁住。由于出现了死
    锁,InnoDB 让 session B 回滚;

sessionB的加锁操作实际上分成了两步,先是加(5,10)的间隙锁,加锁成功,然后加c=10的行锁,才会被锁住;

在分析加锁规则的时候可以用 next-key lock 来分析。但是要知道,具体
执行的时候,是要分成间隙锁和行锁两段来执行的;

总结

  • 两个原则 两个优化 一个bug;
  • 可重复读隔离级别遵守两阶段锁协议,所有加锁的资源,都是在事务提交或者回滚的时候才释放的;
  • next-key lock 实际上是由间隙锁加行锁实现的。如果切换到读提交隔离级别 (read-committed) 的话,就好理解了,过程中去掉间隙锁的部分,也就是只剩下行锁的部分
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!