Innodb-行锁

2021-01-22  本文已影响0人  多血

基本概念

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

隔离级别

快照读与当前读

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

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不阻塞任何锁。

加锁的具体分析

影响加锁的因素

加锁的顺序

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


image.png

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

MySQL二阶段锁

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

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记录情况下,主键与唯一键加锁的区别

这里面有几个前提:

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

当前读优化-半一致读

半一致读前提:

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

上一篇下一篇

猜你喜欢

热点阅读