21 为什么只修改一行的语句,锁这么多?

上一篇中介绍了间隙锁和next-key lock的概念,但是没有说明加锁规则

加锁规则两个前提说明:

1 mysql后面的版本可能会改变加锁策略,所以这个规则只限于截止到目前最新的版本,即5.x系列 <=5.7.24, 8.0系列 <=8.0.13.

2 如果大家在验证中发现有bad case的话,请提出来,后面会进行补充。

因为间隙锁在可重复读隔离级别下才有效,所以本篇文章的描述,若没有特殊说明,都是在RR隔离级别下面。

我总结的加锁规则里面,包含两个”原则”,两个”优化”和一个”bug”

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

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

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

4 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁

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

建表语句和初始

CREATE TABLE `t20` (

  `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 t20 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 t20 set d=d+1 where id=7;

insert into t20 values(8,8,8);

(blocked)

update t20 set d=d+1 where id=10;

(query ok)

 

由于表t20中没有id=7的记录,所以用上面的加锁规则判断一下:

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

2 同时根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lock退化为间隙锁,因此最终加锁的范围是(5,10)

所以,session B要往这个间隙里面插入id=8的记录会被锁住,但是session C修改id=10这行是可以的。

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

第二个例子是关于覆盖索引上的锁:

SESSION A

SESSION B

SESSION C

begin;

select id from t20 where c=5 lock in share mode;

update t20 set d=d+1 where id=5;(query ok)

insert into t20 values(7,7,7);

(blocked)

                    只在非唯一索引上的锁

 

这里session A要给索引cc=5的这一行加上读锁。

1 根据原则1,加上单位是next-key lock,因此会给(0,5]加上next-key lock

2 要注意c是普通索引,因此仅范围c=5这一条记录是不能马上停下来,需要向右遍历,查到c=10才放弃,根据原则2,访问到的都要加锁,因此要给(5,10]next-key lock

3 但是同时这个符合优化2:等值判断,向右遍历,最后一个值不满足c=5这个条件,因此退化为间隙锁(5,10).

4 根据原则2只有访问到的对象才会加锁。这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么session Bupdate语句可以完成。

session C要插入一个(7,7,7)的记录,就会被session A的间隙锁(5,10)锁住。

需要注意,在这个例子中,lock in share mode只锁覆盖索引,但是如果是for update就不一样了。执行for update时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。

这个例子说明,锁是加在索引上的;同时,它给我们的指导是,如果你要用lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段。比如session A的查询语句改成select d from t where c=5 lock in share mode,可以验证一下效果。

案例三:主键范围索引

第三个例子是关于范围查询的

举例之前,你可以先思考一下这个问题:对于我们这个表t20,下面这两条语句,加锁的范围相同吗?

mysql> select * from t20 where id=10 for update;

mysql> select * from t20 where id>=10 and id<11 for update;

你可能会想,id定义为int类型,这2个语句就是等价的吧,其实,他们并不是完全等价。

在逻辑上,这两条语句肯定是等价的,但是他们的加锁规则不太一样,

SESSION A

SESSION B

SESSION C

begin;

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

insert into t20 values(8,8,8);

(query ok)

insert into t20 values(13,13,13);

(blocked)

update t20 set d=d+1 where id=15;

(blocked)

主键索引上范围查询的锁

现在用前面的加锁规则,来分析session A会加什么锁

1, 开始执行的时候,要找到第一个id=10的行,因此本该是next-key lock5,10]。根据优化1,主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁。

2, 范围查询就往后继续找,找到id=15的这一行停下来,因此需要加上next-key lock(10,15]

所以,session A这时候锁住的范围就是主键索引上,行锁id=10next-key lock(10,15]。这样session Bsession C的结果就可以理解。

这里需要注意,首次session A 定位查找id=10的行的时候,是当做等值来判断的,而向右扫描到id=15的时候,用的是范围查询来判断。

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

接下来,我们在看两个范围查询加锁的例子,可以对照案例三

SESSION A

SESSION B

SESSION C

begin;

select * from t20 where c>=10 and c<11 for update;

insert into t20 values(8,8,8);

(blocked)

update t20 set d=d+1 where c=15;(blocked)

非唯一索引范围锁

这次session A 用字段c来判断,加锁规则跟案例3唯一不同的是,在第一次c=10的定位记录时候,索引c上加上了(5,10]这个next-key lock,后,

由于索引c是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终session A加的锁是,索引c上的(5,10]和(10,15]这两个next-key lock

所以从结果上来看,session B要插入(8,8,8)这个insert语句就会被堵住。

这里需要扫描到c=15停止扫描,是合理的,因为innodb要扫到c=15才知道不需要继续往后找了。

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

前面的四个案例,我们已经用到了加锁规则中的两个原则和两个优化,接下来再看一个关于加锁规则中的bug案例。

SESSION A

SESSION B

SESSION C

begin;

select * from t20 where id>10 and id<=15 for update;

update t20 set d=d+1 where id = 20;(blocked)

insert into

t20 values(16,16,16);

(blocked)

唯一索引范围锁的bug

SESSION A是一个范围查询,按照原则1的话,应该是索引id上只加了(10,15]这个next-key lock,并且因为id是唯一键,所以循环判断到id=15这一行就应该停止了。

但是实现上,innodb会往前扫描到第一个不满足条件的行为止,也就是id=20。而已由于是范围扫描,因此索引id上的(15,20]这个next-key lock也会被锁上。

所以你看到了,session B 要更新id=20这一行,是会被锁住的,同样的session C要插入id=16的一行,也会被锁住。

照理说,这里锁着id=20这一行的行为,其实实际上是没有必要的,因为扫描到id=15,就可以确定不用往后再找了,但实现上还是这么做了。

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

接下来的例子,是为了更好的说明”间隙”这个概念,这里给表t20插入一条记录

([email protected]:3306) [test]> insert into t20 values (30,10,30);

Query OK, 1 row affected (0.01 sec)

([email protected]:3306) [test]> select * from t20;

+----+------+------+

| id | c    | d    |

+----+------+------+

|  0 |    0 |    0 |

|  5 |    5 |    5 |

| 10 |   10 |   10 |

| 15 |   15 |   15 |

| 20 |   20 |   20 |

| 25 |   25 |   25 |

| 30 |   10 |   30 |

+----+------+------+

7 rows in set (0.00 sec)

新插入的这一行c=10,也就说表t20有两个c=10的行。那么,这时候索引c上的间隙是什么状态呢?要知道,由于非唯一索引上包含主键的值,所以是不可能存在”相同”的两行的。

可以看到,虽然有两个c=10,但是它们的主键id是不同的(分别为1030),因此这两个c=10的记录之间,也是有间隙的。

图中画出了索引c上的主键id。为了跟间隙锁的开区间形式进行区别,用(c=10,c=30)这样的形式,来表示索引上的一行。

案例六,用delete语句来验证,注意,delete语句加锁的逻辑,其实跟select...for update是类似的,也就是在开始总结的两个原则,两个优化和一个bug

SESSION A

SESSION B

SESSION C

begin;

delete from t20 where c=10;

insert into t20

 values(12,12,12);(blocked)

update t20 set d=d+1 where c=15;(query ok)

 

这时,session A在遍历的时候,先访问第一个c=10的记录,同样的,根据原则1,这里加的是(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上的加锁范围,是下面蓝色的区域。

这个区域左右两边都是虚线,表示开区间,即(c=5,id=5)(c=15,id-15)这两行上没有锁。

案例七:limit语句加锁

例子6也有一个对照案例,场景如下

SESSION A

SESSION B

begin;

delete from t20 where c=10 limit 2;

insert into t20 values(12,12,12);(query ok)

这个例子里,session Adelete语句加了limit 2。要指定表t20c=10其实也就两条记录,因此加不加limit 2,删除的效果都是一样。但是在加锁的效果却不同。可以看到,session Binsert语言执行通过了,跟案例6的结果却不同。

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

因此,索引c上的加锁范围就变成了从(c=5,id=5)(c=10,id=30)这个前开后闭的区间,如图所示

可以看到,(c=10,id=20)之后的这个间隙并没有在加锁范围里,因此insert语句插入c=12是可以成功的。

这个例子对我们实践的指导意义就在,在删除数据的时候尽量加limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。

案例八:一个死锁的例子

前面的例子中,我们在分析的时候,是按照next-key lock加锁的逻辑来分析的,因此在分析的时候比较方便。最后在看一个例子,目的是说明:next-key lock实际上是间隙锁和行锁加起来的结果

SESSION A

SESSION B

begin;

select id from t20 where c=10 lock in share mode;

update t20 set d=d+1 where c=10;(blocked)

insert into t20 values(8,8,8);

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

现在,我们按时间顺序来分析一下为什么是这样的结果

1,session A启动事务后执行查询语句加lock in share mode,在索引c上加了next-key lock(5,10]和间隙锁(10,15);

2,Session Bupdate语句也要在索引c上加next-key lock(5,10],进入等待;

3,然后session A再插入(8,8,8)这一行,被session B的间隙锁锁住,由于出现了死锁,innodbsession B回滚了。

你可能会问,session Bnext-key lock不是还没有申请成功吗?

其实是这样的。Session Bnext-key lock(5,10]”操作,实际上分成了两步,先是加了(5,10)的间隙锁,加锁成功,然后加c=10的行锁,这时候才被锁住的。

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

小结

这里再次说明,上面的所有案例都是在可重复读隔离级别(rr)下验证。同时,可重复读隔离级遵守两阶段锁协议,所有加锁的资源,都是在事务提交或回滚时才释放的。

在最后的案例中,可以清楚的知道next-key lock实际上是有加间隙锁和行锁实现,如果切换到rc隔离级别,就好理解,过程中去掉了间隙锁的部分,只剩下行锁的部分。

另外,在rc隔离级别下还有一个优化,即:语句执行过程中加上的行锁,在语句执行完成后,就要把不满足条件的行”上的行锁直接释放了,不需要等待事务的提交。

也就是说,rc隔离级别下,锁的范围更小,锁的时间更短,这也是不少业务都默认使用rc隔离级别的原因。

在业务需要使用rr隔离级别时候,能够更细致的设计操作数据库的语句,解决幻读问题的同时,最大限度的提升系统并行处理事务的能力。

猜你喜欢

转载自www.cnblogs.com/yhq1314/p/10221411.html
21
21)