vlambda博客
学习文章列表

MySQL 的可重复读到底是怎么实现的?图解 ReadView 机制


通过关于事务隔离的文章,你已经知道了事务有 4 个隔离级别:读未提交、读已提交、可重复读、串行化,随着隔离级别的加强,能解决脏写、脏读、不可重复读、幻读的问题。


在之前关于存储引擎的文章中,也着重提到了,InnoDB 是 MySQL 默认的存储引擎,InnoDB 默认的隔离级别就是可重复读。在这个隔离级别下,开启事务之后,多次读写同一行数据,读到的值永远是一致。那 MySQL 是如何做到这一点的呢?


本文就进一步聊聊,大名鼎鼎的读已提交是如何实现的。




回顾一下 undo log 回滚日志


在讲可重复读的底层原理之前,我们有必要看一下之前画的图,重新回顾一下 undo log 回滚日志。


MySQL 的可重复读到底是怎么实现的?图解 ReadView 机制


当 MySQL 执行写操作之前,会把即将被修改的数据记录到 undo log 日志里面。


只有这样,事务要回滚的时候,即使 Buffer Pool 中的数据被修改了,依然可以从 undo log 日志中,读取到原插入、修改、删除之前的值,最终把值重新变回去,这就是回滚操作。


undo log 版本链


undo log 版本链是基于 undo log 实现的。undo log 中主要保存了数据的基本信息,比如说日志开始的位置、结束的位置,主键的长度、表id,日志编号、日志类型。


MySQL 的可重复读到底是怎么实现的?图解 ReadView 机制


此外,undo log 还包含两个隐藏字段 trx_id 和 roll_pointer。trx_id 表示当前这个事务的 id,MySQL 会为每个事务分配一个 id,这个 id 是递增的。roll_pointer 是一个指针,指向这个事务之前的 undo log。


MySQL 的可重复读到底是怎么实现的?图解 ReadView 机制


如下图所示,现在有一个 id 为 10 的事务 A 正在执行,undo log 日志的信息如下所示:


MySQL 的可重复读到底是怎么实现的?图解 ReadView 机制


紧接着 id 为 18 的事务 B 开始执行,就会再生成一条 undo log 日志,同时新生成的日志的 roll_pointer 指向上一条 undo log 日志。


MySQL 的可重复读到底是怎么实现的?图解 ReadView 机制


日志与日志之间通过 roll_pointer 指针连接,就形成了 undo log 版本链。


基于 undo log 版本链实现的 ReadView 机制


铺垫了这么多,到这里终于可以说说什么是 Readiew 了。


ReadView 说白了就是一种数据结构,它主要包含这样几部分:


  • m_ids,当前有哪些事务正在执行,且还没有提交,这些事务的 id 就会存在这里;

  • min_trx_id,是指 m_ids 里最小的值;

  • max_trx_id,是指下一个要生成的事务 id。下一个要生成的事务 id 肯定比现在所有事务的 id 都大;

  • creator_trx_id,每开启一个事务都会生成一个 ReadView,而 creator_trx_id 就是这个开启的事务的 id。


为了能把这些概念说清楚,接下来我会画几张图帮助大家理解,不明白的可以多看几遍图!


先来看第一张图。


事务是可以并发执行的,现在有事务 A、事务 B 这两个事务,且这两个都没有提交。事务 A 将会执行多次读操作,来模拟可重复读中多次读取同一行数据的场景。事务 B 则会修改这一行数据。


MySQL 的可重复读到底是怎么实现的?图解 ReadView 机制


事务 A 开启事务的时候会生成一个 ReadView,所以说这个 ReadView 的创建者就是事务 A,事务 A 的事务 id 是 10,所以 creator_trx_id 就是 10。


此时,总共就只有事务 A、事务 B 这两个事务,而且它们都还没有提交,所以说 m_ids 会把这两个事务 id,10、18 都记录下来。min_trx_id 是 m_ids 里面的最小值,10、18 中最小的显然是 10。当前最大的事务 id 是 18,那么下一个事务的 id 就是 19,max_trx_id 就是 19。


ReadView 生成之后,事务 A 就要去 undo log 版本链中读取值了。


现在只有一条 undo log 日志,但这并不意味着事务 A 就能读到这条日志的值 X。它要先判断这行日志的 trx_id 是否小于当前事务的 min_trx_id。看图我们可以很轻松地发现,日志的 trx_id = 8 小于 ReadView 中 min_trx_id = 10。


这就意味着,这个事务 A 开始执行之前,修改这行数据的事务已经提交了,所以事务 A 是可以查到值 X 的。


如何基于 ReadView 实现可重复读?


我们继续看,事务 A 第一次读完之后,事务 B 要修改这行数据了。undo log 会为所有写操作生成日志,所以就会生成一条 undo log 日志,并且它的 roll_pointer 会指向上一条 undo log 日志。



紧接着,事务 A 第二次去读这行数据了,情况如下图所示:



第一次读的时候,开启事务 A 的时候就生成了一个 ReadView,R                


此时事务 A 第二次去查询的时候,先查到的是 trx_id = 18 的那条数据,它会发现 18 比最小的事务编号 10 大。那就说明事务编号为 18 的事务,有可能它是读不到的。


接着就要去 m_ids 里确认是否有 18 这条数据了。发现有 18,那就说明在事务 A 开启事务的时候,这个事务是没有提交的,它修改的数据就不应该被读到。


事务 A 就会顺着 roll_pointer 指针继续往下找,找到了 trx_id = 8 这条日志,发现这条能读,读到的值任然是 x,与第一次读到的结果一致。


成功实现可重复读!


思考题


这篇文章在事务隔离机制的基础上,进一步介绍了 MySQL 中的 undo log 版本链、ReadView,并且用画图的方式知道了 MySQL 中可重复读这一个隔离级别是如何实现的。


最后,留下一个思考题,如果事务A再次修改了这行数据,那么事务A能读到么?