美文网首页
Innodb-行锁

Innodb-行锁

作者: 多血 | 来源:发表于2021-01-22 11:36 被阅读0次

    基本概念

    脏读是指读到别的事务未提交的修改。
    不可重复度读与幻读的区别,不可重复读的重点在于update和delete(读不到已提交的修改),幻读的重点在于insert。

    隔离级别

    快照读与当前读

    在多版本并发控制中,读操作分成两类:

    • 快照读(snapshot read):快照读,读取的是记录的可见版本,可能是历史版本,不用加锁。
    • 当前读(current read):读取读,读取的是最新版本,当前读返回的记录,都会加锁,保证其他事务不会并发地修改这条记录。
      简单的select操作属于快照读,如下所示,具体的原理参看MVCC。
    select * from table where xxx;
    

    特殊读、插入、更新、删除操作,属于当前读,需要加锁,如下所示。

    select * from table where xxx lock in share mode;
    select * from table where xxx for update;
    insert into table values(xxx);
    update table set xxx where xxx;
    delete from table where xxx;
    

    RR级别下快照读与当前读不一致的问题

    http://mysql.taobao.org/monthly/2017/06/07/
    

    RC级别复制导致数据不一致问题

    参看下文的例子,因为复制commit的顺序导致当前读数据不一致了。
    RR就不会出现这个问题,因为RR会加上Gap锁

    https://www.cnblogs.com/fanguangdexiaoyuer/p/11323248.html
    

    锁类型

    在 Innodb 内部用一个 unsiged long 类型数据表示锁的类型, 如图所示,最低的 4 个 bit 表示 lock_mode, 5-8 bit 表示 lock_type, 剩下的高位 bit 表示行锁的类型。


    image.png

    因此Innodb中的锁是上面三者的组合,比如LOCK_S|LOCK_REC|LOCK_ORDINARY表示S类型的间隙行锁。状态码= 2048+512+32
    对于行锁来说,lock_mode只使用到了LOCK_S、LOCK_X。

    锁冲突矩阵

    对于lock_mode来说错冲突矩阵为
    对于行锁类型的锁冲突矩阵为

    组合起来就是

    image.png

    整个冲突矩阵可以理解为任何行锁类型的LOCK_S都与LOCK_S兼容,LOCK_S与LOCK_X、LOCK_X与LOCK_X的兼容要就要考虑行锁类型了,比如GAP与GAP兼容,LOCK_X|GAP就与LOCK_X|GAP兼容。
    行锁类型要特别注意GAP锁与insert intention lock,GAP锁会阻塞insert intention lock。事实上,GAP锁的存在只是为了阻塞insert intention lock,RR级别解决当前读的幻读问题就是通过他们实现的。insert intention lock不阻塞任何锁。

    加锁的具体分析

    影响加锁的因素

    • 当前事务的隔离级别
    • SQL是一致性非锁定读(consistent nonlocking read)还是DML(INSERT/UPDATE/DELETE)或锁定读(locking read)
    • SQL执行时是否使用了索引,所使用索引的类型(主键索引,辅助索引、唯一索引)

    加锁的顺序

    加锁的因素里讲到了使用的索引,给同一条记录不同索引加锁的顺序也跟使用哪个索引定位有关。
    比如通过主键索引定位的,则先给主键索引加锁,再给辅助索引、唯一索引加锁。
    如果更新的内容不涉及辅助索引或唯一索引,则不需要给该索引加锁。如下图。


    image.png

    当然Delete需要给全部索引加锁。

    MySQL二阶段锁

    传统RDBMS加锁的一个原则,就是2PL (二阶段锁):Two-Phase Locking。相对而言,2PL比较容易理解,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。下面,仍旧以MySQL为例,来简单看看2PL在MySQL中的实现。

    image.png

    释放锁

    大多数情况下事务锁都是在事务提交或回滚时释放(二阶段锁思想),但有三种意外,两种是主动释放、一种是被动释放:

    • AUTO-INC锁在SQL结束时直接释放(innobase_commit --> lock_unlock_table_autoinc);
    • 在RC隔离级别下执行DML语句时,从引擎层返回到Server层的记录,如果不满足where条件,则需要立刻unlock掉(ha_innobase::unlock_row)。
      示例如下图,下面第二张图中因为val条件不满足where查询,最终释放。


      image.png
      image.png
    • 锁等待情况下,如果等待的数据记录消失(比如回滚),本锁可能就消失了。
      如下图,RC级别的锁等待。


      image.png

      RR级别这种情况下的等待锁不会消失,会变为下一条记录的GAP锁,防止幻读。细节参看锁分裂和继承。


      image.png

    锁分裂与继承

    很多复杂的锁分析都依赖这个东西,这个要掌握理解,就可以应对多种情况下的锁变化了。


    image.png
    image.png

    非冲突情况下更新的加锁实现

    等值查询

    select * from table where col=xxx lock in share mode; //把下图中X锁更换为S锁
    select * from table where col=xxx for update;
    update table set xxx where col=xxx;
    delete from table where col=xxx;
    

    加锁情况


    image.png

    范围查询

    范围查找因为B树定位的问题,细节上有些变化。
    

    加锁情况

    Insert加锁实现

    加锁涉及的几个阶段

    1.唯一性约束检查,对于主键或唯一索引判断是否已存在唯一键的记录。
        1)如果存在,判断该记录上是否有活跃事务,
            (1)有活跃事务,给该事务加上冲突记录的锁,然后给自身事务加上该冲突记录的等待S锁,进入锁等待阶段。
            (2)如果没有活跃事务,则给自身事务加上该记录的S锁。
        2)如果不存在,则不加锁。
    2.判断是否是DB_DUPLICATE_KEY(row_ins_dupl_error_with_rec),会返回给客户端报错,但不会释放1阶段中加的锁(对于唯一索引,还要回滚主键上已经插入成功的记录(RR下会产生GAP锁))。
    3.对插入位置的下一条记录进行锁兼容检查,监测插入位置下一条记录上是否与与插入意向锁冲突的记录。
        1)如果有,给自身事务加上该记录的等待锁。
        2)如果没有,不加锁插入数据。

    针对1->1)->(1)中锁等待的后续可能性:
       - 活跃事务提交,获取到等待的S锁,然后在第2阶段报错。
       - 活跃事务回滚,因为当前等待锁的目标记录消失了,发生锁分裂和继承,该等待锁成为目标记录下一条记录的S锁(主键和唯一键测试都是GAP锁)。然后进行2、3阶段检测,如果最终插入成功,还会发生锁分裂和继承,即把下一条记录的S锁,继承到自身上。

    row_ins_clust_index_entry
    ----row_ins_clust_index_entry_low
    --------btr_pcur_open //调用btr_cur_search_to_nth_level 查询索引树,将cursor移动到待插入记录相应的位置,Insert总使用PAGE_CUR_LE,所以会定位到待插入记录的前一条记录
    --------如果判断存在唯一冲突,进入下面函数加锁
    --------if (!index->allow_duplicates // 是否是唯一索引
            && n_uniq //唯一索引的字段数量
            && (cursor->up_match >= n_uniq || cursor->low_match >= n_uniq)) { //是否存在唯一索引冲突
    ------------row_ins_duplicate_error_in_clust //1,2阶段都发生在这个函数里,详细参看https://www.jianshu.com/writer#/notebooks/48095140/notes/80218523
    --------}
    --------if (!index->allow_duplicates && row_ins_must_modify_rec(cursor)) { //判断能否原地更新,参看row_ins_must_modify_rec注释,主键原地更新的前提, 唯一字段一致满足逻辑原地更新的条件(但是能不能做到物理原地更新还要考虑存储空间是否满足,这个判断是发生在update中)
    ------------能走到这里,且没发生唯一冲突的,说明已存在的唯一键冲突的记录都是delete-mark的
    ------------btr_cur_optimistic_update or btr_cur_pessimistic_update
    --------}else{
    ------------btr_cur_optimistic_insert or btr_cur_pessimistic_insert
    ----------------btr_cur_ins_lock_and_undo
    --------------------lock_rec_insert_check_and_lock
    ------------------------判断是否与插入意向锁冲突
    ------------------------lock_rec_other_has_conflicting//3阶段
    ----------------------------因为上文定位到的是待插入记录的前一条记录,所以这里比较插入意向锁,要获取的是定位记录下一条记录的锁
    ----------------------------next_rec = page_rec_get_next_const(rec);
    ----------------------------heap_no = page_rec_get_heap_no(next_rec);
    -------}
    
    https://www.jianshu.com/p/8607f106525c
    

    冲突情况下的加锁实现

    加锁类型

    主键冲突,冲突记录已提交

    image.png

    主键冲突,冲突记录未提交

    image.png
    image.png

    唯一索引冲突,冲突记录已提交

    image.png

    唯一索引冲突,冲突记录未提交

    image.png
    image.png

    主键冲突,冲突记录回滚

    下面发生死锁的原因是因为记录回滚,该等待锁继承成为目标记录下一条记录的S锁(Gap),然后与第三阶段的插入意向锁冲突。
    一个思考:为什么唯一键冲突需要加s锁,如果这个s锁的目标记录回滚后,需要转换为Gap锁。
    因为这个记录没了,所以无法在这个记录上加锁,如果有多个等待的s锁,不转成gap,一起往下走,包括latch这些东西都没有办法再阻止他们插入了。

    RC
    image.png
    RR
    image.png
    插入成功后发生锁分裂
    image.png
    image.png
    插入记录成功后发生锁分裂

    RR级别唯一索引,发生回滚然后死锁,最终情况见下面第二张图。
    有个思考,其实插入记录成功后,后面的S|GAP锁能释放吗?其实理论上可以释放,但是违反了二阶段锁协议。


    image.png
    image.png

    存在Delete Mark记录情况下,主键与唯一键加锁的区别

    这里面有几个前提:

    • 主键存在
    • 二级索引的更新总是先delete再insert
    • 相同uniq字段的delete-mark的记录总是非delete-mark记录的前方(左方),这是MySQL B树Insert操作定位search mode采用PAGE_CUR_LE决定的。
    • 因此二级唯一索引如果没有非delete-mark的重复记录,就需要把锁加到插入位置,具体看代码。


      image.png
    RC主键
    image.png
    RR主键
    image.png
    RC唯一索引
    image.png
    RR唯一索引
    image.png

    当前读优化-半一致读

    半一致读前提:

    • 对Update生效,对Delete不生效。
    • RC隔离界别或开启了innodb_locks_unsafe_for_binlog
    • 发生了锁等待
    • 必须是全表扫描 && 该索引是二级索引
      案例:
    https://mp.weixin.qq.com/s?__biz=MzU0MTczNzA1OA==&mid=2247483804&idx=1&sn=c4360f68444bdc91ff46f662c3f27f9e&chksm=fb242891cc53a187231c797be8777633ccf2e951cb99e63298b944cba5ff6a84000f188208bb&mpshare=1&scene=1&srcid=0331LzGltSmGsIdUujfl5KN6%23rd
    

    疑问:update全表扫遇到未提交的insert,半一致读生效吗?生效。


    image.png

    RR级别就需要等待。


    image.png

    https://mp.weixin.qq.com/s?__biz=MzU2NzgwMTg0MA==&mid=2247484177&idx=1&sn=03916542bbfd8262811142c1db39a5b7&chksm=fc96e18ecbe168983b4c1fd3807948905d38429065dd3d7c112e0bbad329bcc3710c38d1ecc9&scene=21%23wechat_redirect
    http://hedengcheng.com/?p=771
    https://www.itcodemonkey.com/article/13003.html
    http://mysql.taobao.org/monthly/2017/12/02/
    http://mysql.taobao.org//monthly/2016/01/01/
    https://github.com/Yhzhtk/note/issues/42
    https://zhuanlan.zhihu.com/p/52098868
    https://zhuanlan.zhihu.com/p/52100378
    https://zhuanlan.zhihu.com/p/52234835
    https://mp.weixin.qq.com/s?__biz=MzU0MTczNzA1OA==&mid=2247483804&idx=1&sn=c4360f68444bdc91ff46f662c3f27f9e&chksm=fb242891cc53a187231c797be8777633ccf2e951cb99e63298b944cba5ff6a84000f188208bb&mpshare=1&scene=1&srcid=0331LzGltSmGsIdUujfl5KN6%23rd
    https://blog.51cto.com/yanzongshuai/2106100
    https://www.aneasystone.com/archives/2018/06/insert-locks-via-mysql-source-code.html
    https://mp.weixin.qq.com/s/RleocRPvK67aTJqbDXeICw

    相关文章

      网友评论

          本文标题:Innodb-行锁

          本文链接:https://www.haomeiwen.com/subject/otgdzktx.html