在10.1.2节图10-3分布式事务一致性的例子中,对于并发执行的事务,如果没有一种机制来保障,那么其中的读事务,可能会只读到并发写事务的部分数据。事实上,对于并发的单机事务,也可能存在类似的现象。
仍考虑10.1.2节中的例子,只是插入事务T1和查询事务T2都发生在同一个DN上。如图10-6所示,首先T1在表t中插入v1和v2两条记录,在其提交之前,查询事务T2开始执行。在T2顺序扫描表t的过程中,首先扫描到v1记录,但是由于此时v1记录的xmin对应的XID1(T1的事务号)还没有提交,因此v1不可见。然后T1完成提交,T2继续扫描,并扫描到v2记录,此时v2记录的xmin对应的XID1已经提交,因此v2可见。这样,查询事务T2只看到了T1的部分插入数据,破坏了事务的一致性要求。

图10-6 单机事务一致性问题示意图
为了解决上面这个问题,openGauss采用多版本并发控制(MVCC)来保证与写事务并发执行的查询事务的一致性。
MVCC的基本机制是:写事务不会原地修改元组内容,而是将被修改的元组标记为这条记录的一个旧版本(标记xmax),同时插入一条修改后的元组,从而产生这条记录的一个新版本;对于在一个查询事务开始时还没有提交的写事务,那么这个查询事务始终认为该写事务没有提交。
在上面的例子中,在T2开始的时候,T1还没有提交,那么对于T2扫描上来的v1和v2记录,T2会认为它们xmin对应的XID1均为未提交的,即这两个新版本对于T1均不可见,因此不会返回任何一条记录,也就不会发生读到部分事务内容的异常情况了。
在MVCC中,最关键的技术点有两个:①元组版本号的实现;②快照的实现。下面详细说明这两个技术点在openGauss中的实现,在10.3.2节中将结合具体示例说明基于MVCC机制的读-写并发控制实现方式。
在openGauss中,采用全局递增的事务号来作为一个元组的版本号,每个写事务都会获得一个新的事务号。如上所述,一个元组的头部会记录两个事务号xmin和xmax,分别对应元组的插入事务和删除(更新)事务。xmin和xmax决定了元组的生命期,亦即该版本的可见性窗口。
相比之下,快照的实现要更为复杂。在openGauss中,有两种方式来实现快照。
方法一:活跃事务数组法。
在数据库进程中,维护一个全局的数组,其中的成员为正在执行的事务信息,包括事务的事务号,该数组即活跃事务数组。在每个事务开始的时候,拷贝一份该数组内容。当事务执行过程中扫描到某个元组时,需要通过判断元组xmin和xmax这两个事务(即元组的插入事务和删除事务)对于查询事务的可见性,来决定该元组是否对查询事务可见。以xmin为例,首先查询CLOG,判断该事务是否提交,如果未提交,则不可见;如果提交,则进一步判断该xmin是否在查询事务的活跃事务数组中。如果xmin在该数组中,或者xmin的值大于该数组中事务号的最大值(事务号是全局递增发放的),那么该xmin事务一定在该查询事务开始之后才会提交,因此对于查询事务不可见;如果xmin不在该数组中,或者小于该数组中事务号的最小值,那么该xmin事务一定在该查询事务开始之前就已经提交,因此对于查询事务可见。上述判断逻辑如图10-7所示。

图10-7 基于活跃事务数组方法的事务可见性判断示意图
元组xmax事务对于查询事务的可见性判断类似。最终,xmin(元组的插入事务事务号)和xmax(元组的删除事务事务号)的不同组合,决定了该元组是否对于查询事务可见,如表10-1所示。
表10-1 事务可见性判断
| xmax状态 xmin状态 | xmax对于查询可见 | xmax对于查询不可见 |
| xmin对于查询可见 | 记录不可见(先插入,后删除) | 记录可见(先插入,未删除) |
| xmin对于查询不可见 | 不可能发生 | 记录不可见(未插入,未删除) |
方案二:时间戳方法
使用活跃事务数组方法,由于该数组一般比较大,无法使用原子操作,因此在其上的读-写并发操作需要加锁互斥,写-写并发操作亦需要加锁互斥。其中,读操作是指事务开始时拷贝数组内容获取快照的操作,写操作是指事务开始时将事务信息加入到该数组中以及事务结束时将事务信息从该数组中移除的操作。在高并发的场景下,活跃事务数组会成为加锁的热点和性能瓶颈。
获取快照,本质上是要获取事务运行状态与时间的映射关系f(t)。对每一个事务来说,该f(t)函数为一个阶梯函数,如图10-8所示,在该事务的提交时刻点tcommit之前,f(t)为未提交状态,在tcommit之后,f(t)为提交状态。

图10-8 事务运行状态与时间函数关系的示意图
由此,某一个事务T的快照内容,即是其它所有事务Tother的事务状态函数fother(t)在该事务开始时刻点tstart的取值状态。根据fother的定义,可知,若tstart <= ,则该事务Tother在T的快照中为未提交状态,其对数据库的写操作对事务T不可见;若tstart > ,则该事务Tother在T的快照中为提交状态,其对数据库的写操作对事务T可见。
在openGauss内部,使用一个全局自增的长整数来作为逻辑的时间戳,模拟数据库内部的时序,该逻辑时间戳被称为提交顺序号(Commit Sequence Number,简称CSN)。每当一个事务提交的时候,在提交顺序号日志中(Commit Sequence Number Log,简称CSN日志)会记录该事务事务号XID(事务的全局唯一标识)对应的逻辑时间戳CSN值。CSN日志中记录的XID值与CSN值的对应关系,即决定了所有事务的状态函数f(t)。
如图10-9所示,在一个事务的实际执行过程中,并不会在一开始就加载全量的CSN日志,而是在扫描到某条记录以后,才会去CSN日志中查询该条记录头部xmin和xmax这两个事务号对应的CSN值,并基于此进行可见性判断。

图10-9 基于时间戳方法的事务可见性判断示意图




