MySQL-Innodb-行锁

576 阅读9分钟

基本概念

脏读是指读到别的事务未提交的修改。

不可重复度读与幻读的区别,不可重复读的重点在于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来说错冲突矩阵为:todo(需补个图)

对于行锁类型的锁冲突矩阵为:todo(需补个图)

组合起来就是

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

隐式锁

所谓隐式锁,其实并不是一个真正的事务锁对象,可以理解为一个标记:对于聚集索引页的更新,记录本身天然带事务ID,对于二级索引页,则在page上记录最近一次更新的最大事务ID,通过回表的方式判断可见性。

目的

隐式锁是InnoDB实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。

特点

隐式锁是针对被修改的B+ Tree记录,因此都是记录类型的锁,不可能是间隙锁或Next-Key类型。

场景

Insert

二级索引

image.png

细节

mysql.taobao.org/monthly/202… mp.weixin.qq.com/s/547fdypJD…

加锁的具体分析

影响加锁的因素

  • 当前事务的隔离级别
  • 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

RC级别唯一索引插入会有间隙锁

zhuanlan.zhihu.com/p/52098868

zhuanlan.zhihu.com/p/52100378

zhuanlan.zhihu.com/p/52234835

参考

mp.weixin.qq.com/s?__biz=MzU…

hedengcheng.com/?p=771

www.itcodemonkey.com/article/130…

mysql.taobao.org/monthly/201…

mysql.taobao.org//monthly/20…

github.com/Yhzhtk/note…

zhuanlan.zhihu.com/p/52098868

zhuanlan.zhihu.com/p/52100378

zhuanlan.zhihu.com/p/52234835

mp.weixin.qq.com/s?__biz=MzU… blog.51cto.com/yanzongshua… www.aneasystone.com/archives/20…

mp.weixin.qq.com/s/RleocRPvK…