55 Read Committed隔离级别是如何基于ReadView机制实现的?
如何基于ReadView机制来实现RC隔离级别呢?
其实这里的一个非常核心的要点在于,当你一个事务设置他处于RC隔离级别的时候,他是每次发起查询,都重新生成一个ReadView!
大家注意,这点是非常重要的,接着我们通过画图一步一步来给大家演示这个RC隔离级别是怎么做到的。
首先假设我们的数据库里有一行数据,是事务id=50的一个事务之前就插入进去的,然后现在呢,活跃着两个事务,一个是事务A(id=60),一个是事务B(id=70),此时如下图所示。

现在的情况就是,事务B发起了一次update操作,更新了这条数据,把这条数据的值修改为了值B,所以此时数据的trx_id会变为事务B的id=70,同时会生成一条undo log,由roll_pointer来指向,看下图:

这个时候,事务A要发起一次查询操作,此时他一发起查询操作,就会生成一个ReadView,此时ReadView里的min_trx_id=60,max_trx_id=71,creator_trx_id=60,此时如下图所示。

这个时候事务A发起查询,发现当前这条数据的trx_id是70。也就是说,属于ReadView的事务id范围之间,说明是他生成ReadView之前就有这个活跃的事务,是这个事务修改了这条数据的值,但是此时这个事务B还没提交,所以ReadView的m_ids活跃事务[^1]列表里,是有[60, 70]两个id的,所以此时根据ReadView的机制,此时事务A是无法查到事务B修改的值B的。
接着就顺着undo log版本链条往下查找,就会找到一个原始值,发现他的trx_id是50,小于当前ReadView里的min_trx_id,说明是他生成ReadView之前,就有一个事务插入了这个值并且早就提交了,因此可以查到这个原始值,如下图。

接着,咱们假设事务B此时就提交了,好了,那么提交了就说明事务B不会活跃于数据库里了,是不是?可以的,大家一定记住,事务B现在提交了。那么按照RC隔离级别的定义,事务B此时一旦提交了,说明事务A下次再查询,就可以读到事务B修改过的值了,因为事务B提交了。
那么到底怎么让事务A能够读到提交的事务B修改过的值呢?
很简单,就是让事务A下次发起查询,再次生成一个ReadView。此时再次生成ReadView,数据库内活跃的事务只有事务A了,因此min_trx_id是60,mac_trx_id是71,但是m_ids这个活跃事务列表里,只会有一个60了,事务B的id=70不会出现在m_ids活跃事务列表里了,如下图。

此时事务A再次基于这个ReadView去查询,会发现这条数据的trx_id=70,虽然在ReadView的min_trx_id和max_trx_id范围之间,但是此时并不在m_ids列表内,说明事务B在生成本次ReadView之前就已经提交了。
那么既然在生成本次ReadView之前,事务B就已经提交了,就说明这次你查询就可以查到事务B修改过的这个值了,此时事务A就会查到值B,如下图所示。

RC隔离级别的实现关键点在于每次查询都生成新的ReadView,那么如果在你这次查询之前,有事务修改了数据还提交了,你这次查询生成的ReadView里,那个m_ids列表当然不包含这个已经提交的事务了,既然不包含已经提交的事务了,那么当然可以读到人家修改过的值了。
这就是基于ReadView实现RC隔离级别的原理,实际上,基于undo log多版本链条以及ReadView机制实现的多事务并发执行的RC隔离级别、RR隔离级别,就是数据库的MVCC多版本并发控制机制。
他本质是协调你多个事务并发运行的时候,并发的读写同一批数据,此时应该如何协调互相的可见性。
脚注
[^1]: 活跃的意思是事务启动了但是没有提交,老师已经说过了,从这么几点来判断: (1)视图数组m_ids:就是说这个事务启动瞬间,当前系统里面正在活跃的事务ID (2)低水位min_trx_id,就是视图数组里面ID最小值 (3)高水位max_trx_id:就是当前系统里面已经创建过的事务id的最大值加1,不管这个事务是什么状态。 一个数据版本,大致总结了这么几点: (1)自己更新的数据自己总是可见的; (2)事务A启动了即一致性视图开启了的时候,另一个事务B的版本没有提交,那事务B的更新对事务A不可见; (3)事务A启动了即一致性视图开启了的时候,另一个事务B的版本提交了,但是是在事务A一致性视图开启之后提交的,那么事务B的更新对事务A不可见; (4)事务A启动了即一致性视图开启了的时候,另一个事务B的版本提交了,但是是在事务A一致性视图开启之前提交的,那么事务B的更新对事务A可见。