MySQL的MVCC总结
@author:Jingdai
@date:2021.04.18
最近学习了一下MVCC的实现,现总结一下。
概念
在介绍之前,先介绍几个概念,后面会用到。
-
RC(READ COMMITTED)
只能读取到其他事务提交的数据,可以解决脏读问题。
-
RR(REPEATABLE READ)
在一次事务中,读取到的数据不会改变,可以解决脏读和不可重复读的问题。
-
快照读
普通的select语句,根据MVCC机制读。
-
当前读
加了
in share mode
或for update
的 select 语句,或者insert、update、delete语句,读取数据库中最新的数据。 -
MVCC(Multi-Version Concurrency Control)
多版本并发控制,在一般的并发场景中,对同一个数据进行并发读写操作,读-读是可以同时进行的,而读-写、写-写是不能同时进行的,需要加锁。而MVCC就是一种机制,可以提供读-写的并发操作,而不用加锁,原理后面会介绍。
MVCC实现
MVCC实现依靠3个方面,隐藏字段、Read View和Undo日志。
隐藏字段
在数据库的表中,除了我们自己定义的字段外,InnoDB引擎还会在每行后面加3个隐藏字段。
- DB_TRX_ID:最近一次对本行记录修改的事务ID。这里的修改包括更新和删除,因为删除并非真的从表中删除该行,只会修改一个删除标志位。
- DB_ROLL_PTR:回滚指针,指向Undo日志。
- DB_ROW_ID:随新行插入而递增的行ID,如果一个表没有主键或唯一非空索引时,InnoDB会使用这个ID产生聚簇索引。(和MVCC关系不大,聚簇索引主要用来提高查找性能)
Read View
当前事务的快照,注意这里的快照和平时理解的快照不一样。平时理解的快照是对原始数据的一个拷贝,而这里的快照则是对当前相关活跃事务列表的一个拷贝,它包含多个字段,这里仅仅记录与MVCC相关的字段。
- low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。
- trx_ids:Read View 创建时其他未提交的活跃事务ID列表。(不包括当前事务自己)
- up_limit_id:活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id。
- creator_trx_id:当前创建事务的ID,这是一个递增的编号。
Undo日志
Undo日志就是一个用链表结构组织的历史版本数据,当查看当前行的事务发现该行数据对自己不可见时,可以顺着 Undo日志去查看下一个,如果可见就查看,如果不可见就继续查看下一个。
如图,当需要修改记录时,假设现在最新的记录数据是93,修改这个记录的事务ID是1001,当事务ID为1003的事务想要把这个记录修改为99时,它首先将这行记录加排他锁,然后将当前行数据拷贝到Undo日志中,然后修改记录的数据为99,并将DB_TRX_ID 字段修改为1003,把DB_ROLL_PTR 字段修改为 指向Undo日志的地址。最后提交事务并释放排他锁。修改后的结果如下图。
算法流程
当处于 RR 隔离级别时,事务开启后,在执行第一条普通的 select 语句时,会创建一个快照Read View,记录目前的活跃事务的信息,一直到事务结束,都一直使用这一个Read View,不会改变。
当处于 RC 隔离级别时,事务开启后,每次执行普通的 select 语句时,都会重新创建一个新的快照 Read View。
而这个算法就是为了知道当前记录是否对当前事务可见,如果可见就直接读,如果不可见就去Undo日志中找到拥有可见性的记录去读。
首先前面说过当前行有一个 DB_TRX_ID 的隐藏字段,这里用 trx_id 代表它的值。
具体如下:
- 若 trx_id < up_limit_id,说明在创建Read View时,当前行已经修改并提交了(若未提交就会在trx_ids 列表中存在),则该记录对当前事务可见,跳到步骤5。
- 若 trx_id >= low_limit_id,说明在创建Read View后,有新的事务对该行进行了修改,这行数据对当前事务不可见,跳到步骤4。
- 若 up_limit_id <= trx_id < low_limit_id,又分两种情况:
- 如果 trx_ids 列表中没有找到了 trx_id,和步骤1类似,说明在创建Read View时,当前行已经修改并提交了,该记录对当前事务可见,跳到步骤5。
- 如果 trx_ids 列表中找到了 trx_id,就有可能是ID为trx_id的事务在当前事务创建Read View 前就修改了记录,但是没有提交,此时该记录对当前事务不可见,跳到步骤4。还有可能是 ID 为 trx_id 的事务在当前事务创建 Read View 后修改了该行记录(应该只有RR隔离级别会发生这种情况),这时不管是否提交都对当前事务不可见,同样跳到步骤4。
- 根据 DB_ROLL_PTR 回滚指针指向的记录,找到Undo日志里的下一条记录,将该行记录的DB_TRX_ID字段的值赋给trx_id,回到步骤1。
- 将该行的值返回。