前言
InnoDB默认的事务隔离级别是repeatable read(后文中用简称RR),它为了解决该隔离级别下的幻读的并发问题,提出了LBCC(锁机制)和多版本并发控制(MVCC)两种方案。其中LBCC解决的是当前读情况下的幻读,MVCC解决的是普通读(快照读)的幻读。
LBCC(解决当前读)
LBCC是Lock-Based Concurrent Control的简称,意思是基于锁的并发控制。在InnoDB中按锁的模式来分的话可以分为共享锁(S)、排它锁(X)和意向锁;如果按照锁的算法来分的话又分为记录锁(Record Locks)、间隙锁(Gap Locks)和临键锁(Next-key Locks)。其中临键锁就可以用来解决RR下的幻读问题。接下来按照锁的算法,介绍一下各种锁。
记录锁(Record locks)
记录锁是针对单个数据记录(行)的锁。当一个事务需要对某一记录进行修改或删除操作时,它会申请获取该记录的锁。一旦获得锁,其他事务将无法修改或删除该记录,直到当前事务完成。这种锁机制可以确保数据的完整性,防止在并发操作时对同一记录进行同时修改。
如图案例,针对id = 1 这行数据加锁:
间隙锁(Gap Locks)
间隙锁不是针对具体的记录,而是针对记录之间的空白区域(间隙)进行加锁。当事务需要对某个范围内的数据进行操作(如范围查询或更新)时,它可能会申请间隙锁以防止其他事务在加锁的间隙中插入新的记录,从而导致不可预见的结果。间隙锁有助于防止幻读(Phantom Reads)现象。
那么有种情况,比如,id>15 加锁,那么究竟锁的是哪里呢
代码语言:sql复制SELECT * FROM user_key_idx WHERE id>15 FOR UPDATE;
可以模拟手动开启两个事务,
其中A事务加锁查询,一个开区间id>15 加锁,事务A不提交
代码语言:sql复制set autocommit=0;
BEGIN;
SELECT * FROM student WHERE id > 15 FOR UPDATE;
COMMIT;
事务A没提交前,事务B 进行插入数据,由于id递增,肯定是大于15
代码语言:sql复制set autocommit=0;
BEGIN;
INSERT INTO `student`.`student` (`student_name`, `student_sex`) VALUES ('1234', '222')
COMMIT;
事务B出现的结果是,一直阻塞着,必须等待A事务提交才行
结论:如果间隙锁开区间,也就是最后一个是无穷,上面问题,id>15加锁,其实是从数据15开始就加锁,所以插入16以后都会阻。
临键锁(Next-key Locks) = 记录锁 间隙锁
临键锁是记录锁和间隙锁的组合。这种锁类型不仅锁定记录本身,还锁定记录之间的间隙。临键锁在读取数据时尤为有用,它可以确保在锁定范围内读取到的数据是一致的。当事务需要对一个范围的数据进行读取时,使用临键锁可以防止在操作过程中其他事务插入新的记录,从而导致读取到过时或不一致的数据。
触发条件:where范围包括表数据,比如id>5,id<11,包括了数据9,左开右闭,与间隙锁不同的是,区间不是空白,必须有数据。
可以来验证一下,为什么是左开右闭,同样也是准备两个事务,要验证这个问题,必须保证应用的字段是非唯一索引。比如使用student_num 这个无索引。
事务A
代码语言:sql复制set autocommit=0;
BEGIN;
SELECT * FROM student WHERE student_num > 5 and student_num < 11 FOR UPDATE;
COMMIT;
事务B
代码语言:sql复制set autocommit=0;
BEGIN;
SELECT * FROM student WHERE student_num = 11 FOR UPDATE;
COMMIT;
结果B事务会阻塞着
这里对 记录锁、间隙锁、临键锁 做一个总结:
InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间。
MVCC(解决快照读)
LBCC是基于锁的并发控制,因为锁的粒度过大,会导致性能的下降,因此提出了比LBCC性能更优越的方法MVCC。MVCC是Multi-Version Concurremt Control的简称,意思是基于多版本的并发控制协议,通过版本号,避免同一数据在不同事务间的竞争,只存在于InnoDB引擎下。它主要是为了提高数据库的并发读写性能,不用加锁就能让多个事务并发读写。MVCC的实现依赖于:三个隐藏字段、Undo log和Read View,其核心思想就是:只能查找事务ID小于等于当前事务ID的行;只能查找删除时间大于等于当前事务ID的行,或未删除的行。接下来让我们从源码级别来分析下MVCC。
MVCC底层原理
MVCC(多版本并发控制,类似CAS):快照读的情况,解决并发访问数据库系统时候,提高读写效率,保证数据一致性(原因加锁导致性能下降,所以使用MVCC),简单介绍一下什么是当前读和快照读
当前读:读取数据的最新版本,总是读取最新的数据
select.....lock in share mode 读锁(乐观锁)
select.....for update 写锁(悲观锁)
这时另一个是如果insert,update,delete都会被阻塞
快照读:读取的是历史版本记录
select.....
对于快照读,在不同隔离级别读到数据不一致,原因是,事务版本的可见性,也就是MVCC的可见性算法
MVCC三部分重要组件
重要部分一:
三个隐藏字段(用户不可见):每一行记录都会有几个隐藏字段
DB_TRX_ID:创建或者最后一次修改该记录的事务id
DB_ROW_ID:隐藏主键 select _row_id 可以查看
DB_ROLL_PTR:回滚指针,指向undolog
重要部分二:
undolog回滚日志,insert、Update,delete操作时,方便进行回滚的日志记录(多个),保存数据的历史版本状态。
重要部分三:
Read View读视图:事务在进行快照读的时候产生的读视图。数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复
读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
MVCC可见性算法
MVCC可见性算法是MySQL中用于处理多版本并发控制(MVCC)的规则。在MVCC机制中,每个事务可以看到一个一致的数据视图,即使其他事务正在修改数据。这是通过维护一个“快照”来实现的,该快照表示事务开始时的数据状态。具体看如图所示:
案例验证
如下场景,四个事务分别操作数据库,其中事务4进行修改数据,事务2再查快照读。
两个事务一起,还没有read view,第一次快照读,所以右边一个事务是修改数据,左边的事务可以看到最新修改数据(测试需要关闭MYSQL自动提交:set autocommit=0)
最终结果,RR隔离级别,在事务4,commit之后,事务2再查快照读,不会生成新的readview,读的还是上一次的,也就是事务0的一开始插入的数据。根据事务并发可见性算法,判断是不能看见另一个事务(事务2)修改的数据。
接下来来验证一直,RR隔离级别下,MVCC快照读会出现幻读的场景:
总结
InnoDB通过使用LBCC(Lock Before Commit Control)和MVCC(Multi-Version Concurrency Control)来解决幻读问题,但是两种方式都有优缺点。
LBCC(Lock Before Commit Control):在当前读(current read)中,InnoDB使用LBCC来解决幻读问题。当一个事务执行当前读时,它会在读取数据之前获取一个行锁,以确保其他事务不能在当前事务完成之前修改或删除这些数据。这样,当前事务可以看到一致的数据,而不会受到其他事务的影响,但是加锁导致性能下降
MVCC(Multi-Version Concurrency Control):在快照读(snapshot read)中,InnoDB使用MVCC来解决幻读问题。快照读是一种轻量级的读操作,它不需要获取行锁。相反,它使用一个快照来确保事务看到的数据是一致的。快照是一个包含事务ID的时间点,它表示事务开始时的数据状态。MVCC可见性算法确保每个事务看到的数据都是一致的,即使其他事务正在修改数据。但是,在RR(可重复隔离级别)幻读还是会出现。
我正在参与2023腾讯技术创作特训营第四期有奖征文,快来和我瓜分大奖!