在博主之前的博客中已经对MySQL的原理进行了介绍,但是之前有关 多版本并发控制(Multi-Version Concurrency Control,MVCC)
的知识提了一点,有关MVCC的实现原理将在本文进行详细的介绍,并使用图解的方式来进行。
有关MySQL的原理可以参考之前的文:MySQL原理,看这一篇就够了(InnoDB、MVCC、索引、SQL优化)
多版本并发控制(Multi-Version Concurrency Control,MVCC),是MySQL提高性能的一种方式,配合Undo日志和版本链,让不同事务的读-写、写-读操作可以并发执行,从而提升系统性能。一般在使用 读已提交(READ COMMITTED)和 可重复读(REPEATABLE READ)隔离级别的事务中实现。
在InnoDB中,会在聚集索引中(主键索引)每行数据后添加额外的隐藏的值来实现MVCC。
DB_TRX_ID
:6字节的DB_TRX_ID字段表示插入或更新该行的最后一次操作的标识符。每次对某条聚集索引记录进行改动时,都会把对应的事务id赋值给DB_TRX_ID隐藏列。
删除在内部被视为更新,在该更新中,行中的特殊位被设置为将其标记为已删除。
DB_ROLL_PTR
:7字节的 DB_ROLL_PTR字段,称为回滚指针。回滚指针指向写入回滚段的撤消日志记录。每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到Undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
DB_ROW_ID
:6字节的DB_ROW_ID字段包含一个行ID,该ID在插入新行时会自动增加。此字段不是必须的,当聚集索引中的主键是由MySQL自动生成的自增主键时,才会存在。
我们创建一张数据库表 test 来介绍以下几种概念:
CREATE TABLE test(
id INT NOT NULL AUTO_INCREMENT,
age INT,
name VARCHAR(255),
PRIMARY KEY(id),
KEY(age)
);
版本链,实际上是Undo日志内,链状记录的日志信息。因为在每次对某条聚集索引记录进行改动时,都会把对应的 事务id
赋值给DB_TRX_ID
隐藏列,并且会把旧的版本写入到Undo日志中,然后隐藏列DB_ROLL_PTR
就相当于一个指针,可以通过来它找到该记录修改前的版本信息。
我们假设事务id 从 1开始,事务id是由MySQL内部自增维护的。
注:不能在两个事务中交叉更新同一条记录,第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。
与锁相关的说明已在博主之前 MySQL原理 一文中有过介绍。
ReadView
记录了事务的相关信息,用来与版本链配合使用,从而控制事务的可见性。ReadView
中主要记录当前系统中还有哪些 活跃 的事务,用来 判断版本链中的哪个版本是当前事务可见的。
已开启未提交的事务称为活跃事务。
对于使用 读未提交(READ UNCOMMITTED)隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用 串行化(SERIALIZABLE)隔离级别的事务来说,使用加锁的方式来访问记录。对于使用 读已提交(READ COMMITTED)和 可重复读(REPEATABLE READ)隔离级别的事务来说,就需要用到我们上边所说的版本链了,核心问题就是:需要判断版本链中的哪个版本是当前事务可见的。因此引入ReadView
。
ReadView中主要包含4个比较重要的内容:
m_ids
:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。min_trx_id
:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。max_trx_id
:表示生成ReadView时系统中应该分配给下一个事务的id值。creator_trx_id
:表示生成该ReadView的事务的事务id。注:
max_trx_id
并不是m_ids
中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的记录提交了。那么一个新的读事务在生成Readview
时,m_ids
就包括1和2,min_trx_id
的值就是1,max_trx_id
的值就是4。
事务回退依然会将事务id自增。即事务 提交 和 回退 都会触发事务id自增。
所以判断可见性的步骤就是:
DB_TRX_ID
列小于min_trx_id
,即此事务是在ReadView
创建前提交的,说明其可见。DB_TRX_ID
列大于max_trx_id
,即此事务是在ReadView
创建后开启的,说明其不可见。DB_TRX_ID
列在min_trx_id
和max_trx_id
之间,即此事务是活跃状态。则需要看该DB_TRX_ID
在不在m_ids
列表中,如果在,说明不可见,否则可见。在MySQL中,读已提交(READ COMMITTED)和 可重复读(REPEATABLE READ)隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同,所以可解决的问题不同。
读已提交(READ COMMITTED)每次读取数据前都生成一个ReadView。
类似于Spring bean 中的
原型(prototype)
,每次请求都会创建一个新的实例。
min_trx_id:2
,即表明事务已提交,因此可以显示。min_trx_id:3
,即表明事务已提交,因此可以显示。min_trx_id:4
,即表明事务已提交,因此可以显示。可重复读(REPEATABLE READ)仅在首次读取数据时生成一个ReadView。
类似于Spring bean 中的
单例(singleton)
,Spring容器中只存在一个对象实例,所有该对象的引用都共享这个实例。
name=lee
。从文章内容我们可以看出来,在聚集索引(主键索引)下,所谓的 MVCC(Multi-Version Concurrency Control,多版本并发控制)指的就是在使用 读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)这两种隔离级别的事务时,在执行普通的SEELCT
操作时访问记录的版本链的过程中,可以让不同事务的读-写、写-读操作并发执行,从而提升系统性能。
读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)这两个隔离级别的一个很大不同就是 生成ReadView的时机不同。
SELECT
操作前都会生成一个ReadView
。SELECT
操作前生成一个ReadView
,之后的查询操作都重复使用这个ReadView
。InnoDB多版本并发控制(MVCC)对辅助索引(Secondary Index)的处理方式不同于对聚集索引的处理方式。聚集索引中的记录将就地更新,其隐藏的系统列指向撤消日志记录,可以从中回滚到记录的早期版本。与聚集索引记录不同,辅助索引记录不包含隐藏的系统列,也不会就地更新,而是依赖聚集索引来级联控制。
当辅助索引被更新、删除时,不会覆盖辅助索引,不会直接从覆盖索引返回值,而是到聚集索引中查找记录。在聚集索引中,DB_TRX_ID
检查记录的记录,如果在启动读取事务后修改了记录,则从Redo日志中检索记录的正确版本。
即 辅助索引下的MVCC依赖于主键索引来进行级联变动。
本文参考:
[1] @瘦子没有夏天 :深入理解MySQL8中的MVCC实现原理