MySQL ReadView 详解

之前我们在讲事务的时候,已经提到过 ReadView 了,但是没有仔细讲解,今天我们就借助这篇文章,来深入、透彻的聊一聊 ReadView ,相信你读完这篇文章之后,肯定会对 ReadView 有一个全新的认识。

这篇文章可以分为以下三个章节:

  • ReadView 在MVCC里是如何工作的?
  • 可重复读是如何工作的?
  • 读提交是如何工作的?

ReadView 在 MVCC 里是如何工作的?

在介绍 ReadView 的工作机制之前,我们需要学习两个基本知识:

  • ReadView 中的四个字段分别是什么
  • 聚簇索引记录中跟事务有关的隐藏列是什么

我们先来看第一个基本知识,我画了一张图,这样更直观一些。

image-20250226160150211

creator_trx_id m_ids min_trx_id max_trx_id

ReadView 中有四个重要的字段:

  • creator_trx_id:表示当前读取数据的事务 ID。它指明了 ReadView 是在哪个事务的上下文中创建的。它用来标识事务的唯一性。每个事务都有一个唯一的 ID,MySQL 会在 ReadView 中记录当前事务的 ID,以便在该事务读取数据时,能够过滤掉其他事务未提交的操作。
  • m_ids:它是一个列表,包含了所有 活跃事务 的事务 ID。即系统中所有尚未提交的事务都在这个列表中。m_ids 使得当前事务在读取数据时,能够知道有哪些事务是“活跃的”(还在进行中),从而避免读取到这些未提交事务的变更。在 MVCC 中,当前事务只能看到已提交事务的数据,而不能看到未提交事务的变化
  • min_trx_id:表示当前系统中所有 活跃事务(正在执行且未提交的事务)中的 最小事务 ID。也就是说,这个字段标记了系统中最早的一个未提交事务的事务 ID。min_trx_id 用来帮助识别在 ReadView 创建时,哪些事务的操作是可以被当前事务看到的。

小于 min_trx_id 的事务(即这些事务已经提交)对当前事务可见,而 大于 **min_trx_id **的事务则被认为是未提交的。

  • max_trx_id:表示当前系统中所有 活跃事务 中的 最大事务 ID。即它标记了系统中最晚的一个未提交事务的事务 ID。它用来帮助判断哪些操作在 ReadView 创建时是 不可见 的。

因为在 ReadView 中,当前事务看不到那些 大于 max_trx_id 的事务的操作,意味着这些事务的数据变更在当前事务的视图中不可见。

了解完以上四个字段之后,我们再来看看聚簇索引中的两个隐藏列分别是什么。

假设我插入了一条记录,如下图所示,我把隐藏列也画出来了:

image-20250226160155016

trx_id roll_pointer

图中的 trx_id 和 roll_pointer 就是隐藏列了。

  • trx_id:代表最后一次修改该记录的事务 ID。 当一个事务对某行数据进行更新时,MySQL 会在该行的 trx_id 字段中存储执行该操作的 事务 ID
  • roll_pointer:回滚指针,指向 Undo Log 中存储的该行记录的上一个版本(即事务修改前的版本),从而 让 MySQL 能够通过 回溯 Undo Log 找到旧版本的数据。

在创建一个 ReadView 之后,我们可以把所有的 trx_id 划分为这三类:

image-20250226160158745

当一个事务去访问某些记录的时候,被这个事务更新的记录肯定也是对这个事务可见的。但是除了这种比较简单的情况之外,还有三种比较复杂的情况:

  • 当记录的 trx_id 值 小于 min_trx_id 值时,意味着该记录是 已提交的事务所产生的数据,并且对当前事务是 可见 的。
  • 如果某条记录的 trx_id 大于或等于 max_trx_id ,则说明该记录由 未提交事务 修改,或者是当前事务正在进行的修改。由于事务是 未提交的该记录的数据对当前事务不可见
  • 当记录的 trx_id 位于 min_trx_id 和 max_trx_id 之间时,需要判断 trx_id 是否在 m_ids 列表中:如果记录的 trx_id 在 m_ids列表中,代表该记录由 一个活跃的未提交事务 所修改,因此该记录的数据 对当前事务不可见;如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

MVCC 允许多个事务并发地访问数据库,它通过为每个事务生成数据的 多个版本,确保每个事务只能看到一个 一致的快照,并且不会受到其他事务未提交的修改的影响。

以上就是 ReadView 在 MVCC 中的工作机制了。

可重复读是如何工作的?

我们知道的是,在可重复读隔离级别下,事务刚启动的时候会创建一个 ReadView ,然后在接下来的整个事务期间都用这个 ReadView 。

现在我们通过一个例子,结合 ReadView 的知识,再来深入的学习一下这个过程。

假设启动一个 事务A,事务 ID 是 50 。

此时 事务A 的 ReadView 就是这样的:

在这个 ReadView 中,它的事务 ID 是 50 ,由于它是第一个启动的,所以此时活跃事务的事务列表就是[50],并且活跃事务列表中的最小事务 ID 是 50,下一个事务 ID 是 51 。

紧接着,再启动一个事务 B ,事务 ID 是 51。

此时,事务 B 的 ReadView 就是这样的:

image-20250226160217838

在这个 ReadView 中,它的事务 ID 是 51 ,此时活跃事务的事务列表就是[50,51],并且活跃事务列表中的最小事务 ID 是 50,下一个事务 ID 是 52 。

此时,数据库中有这样一条记录:

image-20250226160221297

假设事务 A 和事务 B 分别按顺序对这条记录进行操作:

  1. 事务 B 读取余额,值是 100 ;
  2. 事务 A 将余额修改成 200 ,并没有提交事务;
  3. 事务 B 读取余额,读到的值还是 100 ;
  4. 事务 A 提交事务;
  5. 事务 B 读取余额,读到的值依然还是 100 ;

好,现在我们仔细分析下这个过程:

事务 B 在第一次读取记录的时候,它会先检查这条记录的 trx_id ,检查之后发现这条记录的 trx_id = 49 ,要比活跃事务列表中的 min_trx_id = 50 小,那这就说明修改这条记录的事务早就在事务 B 启动之前提交了,所以这条记录对事务 B 来说是可见的。

然后事务 A 对这条记录进行修改,修改之后的记录就是新版本,修改之前的记录即为旧版本,于是这两个版本的记录就通过 roll_pointer 串联起来,形成了一个版本链。如下图所示:

image-20250226160226607

当事务B再次去读取记录的时候,就会发现这条记录的 trx_id 变为 50 了,并且这个 trx_id 在活跃事务列表中,那就说明修改这条记录的事务并没有提交,所以这个新版本的记录对事务B就是不可见的。事务B就需要沿着版本链往下找,直到找到 trx_id 小于 min_trx_id 的第一条记录,所以事务B第二次读取到的记录还是旧的记录,余额值是 100 的这条记录。

当事务A提交之后,事务B再次读取的时候,由于在可重复读隔离级别下,所以整个事务期间使用的一直都是事务B刚启动的时候创建的ReadView,新版本的记录依旧不可见,所以事务B第三次读到的值依旧是100。

所以,通过我们细致的分析,你应该懂了,可重复读就是这么实现的啦。

读提交是如何工作的?

我们已经知道了可重复读是如何实现的了,那么读提交隔离级别又是如何实现的呢?

我们还是接着上面的例子来分析。

假设事务 A 和事务 B 分别按顺序对这条记录进行操作:

  1. 事务 B 读取余额,值是 100 ;
  2. 事务 A 将余额修改成 200 ,并没有提交事务;
  3. 事务 B 读取余额,读到的值还是 100 ;
  4. 事务 A 提交事务;
  5. 事务 B 读取余额,读到的值依然还是 100 ;

事务B第一次读余额的操作,与可重复读隔离级别一样;

事务A修改记录的操作,与可重复读隔离级别一样;

事务B第二次读余额的操作,与可重复读隔离级别一样;

重点来了,我们之前提到过,在可重复读隔离级别下,事务B会一直沿用刚刚启动时创建的ReadView。但是现在是在读提交隔离级别下,所以当事务A提交事务之后,事务B第三次读取余额的时候,这时它就会重新创建一个新的 ReadView。

这个新的 ReadView 是这样的:

image-20250226160230395

可以很直观的看到,此时的 ReadView 中,活跃事务列表已经变了,所以当它第三次读取余额的时候,会发现新版本记录中的 trx_id = 50 小于 新 ReadView 中的 min_trx_id = 51,就能读取到新版本的记录了,所以它读取到的值就是 200 。

所以,之前我们在讲事务的时候,说过读提交可能会出现读取的数据前后不一致的情况。其实问题就出在这,当有新的事务提交时,它会自作主张创建新的 ReadView 。

总结

好啦!这篇文章我们详细地学习了 ReadView ,包括它的四个字段,以及它分别在可重复读隔离级别下和读提交隔离级别下是如何工作的。关于 ReadView 的相关知识我们就先讲到这里啦!

发表评论

后才能评论