zoukankan      html  css  js  c++  java
  • 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插入一条记录

    (system@127.0.0.1:3306) [test]> insert into t20 values (30,10,30);

    Query OK, 1 row affected (0.01 sec)

    (system@127.0.0.1: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隔离级别时候,能够更细致的设计操作数据库的语句,解决幻读问题的同时,最大限度的提升系统并行处理事务的能力。

  • 相关阅读:
    HDOJ 1207 汉诺塔II
    [转]写代码的小女孩
    POJ Subway tree systems
    HDOJ 3555 Bomb (数位DP)
    POJ 1636 Prison rearrangement (DP)
    POJ 1015 Jury Compromise (DP)
    UVA 10003
    UVA 103 Stacking Boxes
    HDOJ 3530 Subsequence
    第三百六十二、三天 how can I 坚持
  • 原文地址:https://www.cnblogs.com/yhq1314/p/10221411.html
Copyright © 2011-2022 走看看