图解MySQL 8.0 中的MVCC原理

在博主之前的博客中已经对MySQL的原理进行了介绍,但是之前有关 多版本并发控制(Multi-Version Concurrency Control,MVCC) 的知识提了一点,有关MVCC的实现原理将在本文进行详细的介绍,并使用图解的方式来进行。

有关MySQL的原理可以参考之前的文:MySQL原理,看这一篇就够了(InnoDB、MVCC、索引、SQL优化)


图解MySQL 8.0 中的MVCC原理

  • 多版本并发控制 MVCC
    • 版本链
    • ReadView
      • 读已提交下的MVCC
      • 可重复读下的MVCC
    • 总结
    • 辅助索引与MVCC


多版本并发控制 MVCC

多版本并发控制(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 8.0 中的MVCC原理_第1张图片

注:不能在两个事务中交叉更新同一条记录,第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。
与锁相关的说明已在博主之前 MySQL原理 一文中有过介绍。


ReadView

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。

图解MySQL 8.0 中的MVCC原理_第2张图片

注: 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_idmax_trx_id之间,即此事务是活跃状态。则需要看该DB_TRX_ID在不在m_ids列表中,如果在,说明不可见,否则可见。

在MySQL中,读已提交(READ COMMITTED)和 可重复读(REPEATABLE READ)隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同,所以可解决的问题不同。


读已提交下的MVCC

读已提交(READ COMMITTED)每次读取数据前都生成一个ReadView。

类似于Spring bean 中的原型(prototype),每次请求都会创建一个新的实例。

图解MySQL 8.0 中的MVCC原理_第3张图片
查询结果分析

  1. 第 ③ 步时:查询结果为 lee;此时 1 < min_trx_id:2,即表明事务已提交,因此可以显示。
  2. 第 ④ 步时:查询结果为 liu;此时 2 < min_trx_id:3,即表明事务已提交,因此可以显示。
  3. 第 ③ 步时:查询结果为 nier;此时 3 < min_trx_id:4,即表明事务已提交,因此可以显示。

可重复读下的MVCC

可重复读(REPEATABLE READ)仅在首次读取数据时生成一个ReadView。

类似于Spring bean 中的单例(singleton),Spring容器中只存在一个对象实例,所有该对象的引用都共享这个实例。

图解MySQL 8.0 中的MVCC原理_第4张图片
查询结果分析

  • 事务 4 开启后,首次查询生成了ReadView,虽然后面两个事务的操作已经提交,但后两次查询不会再生成新的ReadView,在事务 4 提交之前沿用的第一次查询时生成的ReadView ,因此 3 次查询结果一直为 name=lee

总结

从文章内容我们可以看出来,在聚集索引(主键索引)下,所谓的 MVCC(Multi-Version Concurrency Control,多版本并发控制)指的就是在使用 读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)这两种隔离级别的事务时,在执行普通的SEELCT操作时访问记录的版本链的过程中,可以让不同事务的读-写、写-读操作并发执行,从而提升系统性能。

读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)这两个隔离级别的一个很大不同就是 生成ReadView的时机不同

  • READ COMMITTD 在每一次进行普通SELECT操作前都会生成一个ReadView
  • REPEATABLE READ 只在首次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView

辅助索引与MVCC

InnoDB多版本并发控制(MVCC)对辅助索引(Secondary Index)的处理方式不同于对聚集索引的处理方式。聚集索引中的记录将就地更新,其隐藏的系统列指向撤消日志记录,可以从中回滚到记录的早期版本。与聚集索引记录不同,辅助索引记录不包含隐藏的系统列,也不会就地更新,而是依赖聚集索引来级联控制。

当辅助索引被更新、删除时,不会覆盖辅助索引,不会直接从覆盖索引返回值,而是到聚集索引中查找记录。在聚集索引中,DB_TRX_ID检查记录的记录,如果在启动读取事务后修改了记录,则从Redo日志中检索记录的正确版本。

辅助索引下的MVCC依赖于主键索引来进行级联变动


本文参考:

[1] @瘦子没有夏天 :深入理解MySQL8中的MVCC实现原理

你可能感兴趣的:(MySQL)