基本概念
脏读是指读到别的事务未提交的修改。
不可重复度读与幻读的区别,不可重复读的重点在于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 表示行锁的类型。
因此Innodb中的锁是上面三者的组合,比如LOCK_S|LOCK_REC|LOCK_ORDINARY表示S类型的间隙行锁。状态码= 2048+512+32
对于行锁来说,lock_mode只使用到了LOCK_S、LOCK_X。
锁冲突矩阵
对于lock_mode来说错冲突矩阵为
对于行锁类型的锁冲突矩阵为
组合起来就是
整个冲突矩阵可以理解为任何行锁类型的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执行时是否使用了索引,所使用索引的类型(主键索引,辅助索引、唯一索引)
加锁的顺序
加锁的因素里讲到了使用的索引,给同一条记录不同索引加锁的顺序也跟使用哪个索引定位有关。
比如通过主键索引定位的,则先给主键索引加锁,再给辅助索引、唯一索引加锁。
如果更新的内容不涉及辅助索引或唯一索引,则不需要给该索引加锁。如下图。
当然Delete需要给全部索引加锁。
MySQL二阶段锁
传统RDBMS加锁的一个原则,就是2PL (二阶段锁):Two-Phase Locking。相对而言,2PL比较容易理解,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。下面,仍旧以MySQL为例,来简单看看2PL在MySQL中的实现。
释放锁
大多数情况下事务锁都是在事务提交或回滚时释放(二阶段锁思想),但有三种意外,两种是主动释放、一种是被动释放:
- AUTO-INC锁在SQL结束时直接释放(innobase_commit --> lock_unlock_table_autoinc);
-
在RC隔离级别下执行DML语句时,从引擎层返回到Server层的记录,如果不满足where条件,则需要立刻unlock掉(ha_innobase::unlock_row)。
示例如下图,下面第二张图中因为val条件不满足where查询,最终释放。
-
锁等待情况下,如果等待的数据记录消失(比如回滚),本锁可能就消失了。
如下图,RC级别的锁等待。
RR级别这种情况下的等待锁不会消失,会变为下一条记录的GAP锁,防止幻读。细节参看锁分裂和继承。
锁分裂与继承
很多复杂的锁分析都依赖这个东西,这个要掌握理解,就可以应对多种情况下的锁变化了。
非冲突情况下更新的加锁实现
等值查询
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;
加锁情况
范围查询
范围查找因为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阶段都发生在这个函数里,详细参看//www.greatytc.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);
-------}
//www.greatytc.com/p/8607f106525c
冲突情况下的加锁实现
加锁类型
主键冲突,冲突记录已提交
主键冲突,冲突记录未提交
唯一索引冲突,冲突记录已提交
唯一索引冲突,冲突记录未提交
主键冲突,冲突记录回滚
下面发生死锁的原因是因为记录回滚,该等待锁继承成为目标记录下一条记录的S锁(Gap),然后与第三阶段的插入意向锁冲突。
一个思考:为什么唯一键冲突需要加s锁,如果这个s锁的目标记录回滚后,需要转换为Gap锁。
因为这个记录没了,所以无法在这个记录上加锁,如果有多个等待的s锁,不转成gap,一起往下走,包括latch这些东西都没有办法再阻止他们插入了。
RC
RR
插入成功后发生锁分裂
插入记录成功后发生锁分裂
RR级别唯一索引,发生回滚然后死锁,最终情况见下面第二张图。
有个思考,其实插入记录成功后,后面的S|GAP锁能释放吗?其实理论上可以释放,但是违反了二阶段锁协议。
存在Delete Mark记录情况下,主键与唯一键加锁的区别
这里面有几个前提:
- 主键存在
- 二级索引的更新总是先delete再insert
- 相同uniq字段的delete-mark的记录总是非delete-mark记录的前方(左方),这是MySQL B树Insert操作定位search mode采用PAGE_CUR_LE决定的。
-
因此二级唯一索引如果没有非delete-mark的重复记录,就需要把锁加到插入位置,具体看代码。
RC主键
RR主键
RC唯一索引
RR唯一索引
当前读优化-半一致读
半一致读前提:
- 对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,半一致读生效吗?生效。
RR级别就需要等待。
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