【连载】如何掌握openGauss数据库核心技术?秘诀四:拿捏事务机制(3)

2022-02-25 16:41:38 浏览数 (1)

前文回顾:

1.如何掌握openGauss数据库核心技术?秘诀一:拿捏SQL引擎(1)

2.如何掌握openGauss数据库核心技术?秘诀一:拿捏SQL引擎(2)

3.如何掌握openGauss数据库核心技术?秘诀一:拿捏SQL引擎(3)

4.如何掌握openGauss数据库核心技术?秘诀一:拿捏SQL引擎(4)

5.如何掌握openGauss数据库核心技术?秘诀二:拿捏执行器技术(1)

6.如何掌握openGauss数据库核心技术?秘诀二:拿捏执行器技术(2)

7.如何掌握openGauss数据库核心技术?秘诀三:拿捏存储技术(1)

8.如何掌握openGauss数据库核心技术?秘诀三:拿捏存储技术(2)

9.如何掌握openGauss数据库核心技术?秘诀三:拿捏存储技术(3)

10.如何掌握openGauss数据库核心技术?秘诀三:拿捏存储技术(4)

11.如何掌握openGauss数据库核心技术?秘诀三:拿捏存储技术(5)

12.如何掌握openGauss数据库核心技术?秘诀三:拿捏存储技术(6)

13.如何掌握openGauss数据库核心技术?秘诀三:拿捏存储技术(7)

14.如何掌握openGauss数据库核心技术?秘诀四:拿捏事务机制(1)

15.如何掌握openGauss数据库核心技术?秘诀四:拿捏事务机制(2)

目录

  • openGauss数据库SQL引擎
  • openGauss数据库执行器技术
  • openGauss存储技术
  • openGauss事务机制

Ⅰ.openGauss数据库事务概览

Ⅱ.openGauss事务ACID特性介绍

1.openGauss中的事务持久性

2.openGauss中的事务原子性

3.openGauss中的事务一致性

4.openGauss中的事务隔离性

Ⅲ.openGauss并发控制

Ⅳ.openGauss分布式事务

  • openGauss数据库安全

openGauss事务机制

openGauss事务ACID特性介绍

openGauss中的事务一致性

03

在上一节的图3分布式事务一致性的例子中,对于并发执行的事务,如果没有一种机制来保障,那么其中的读事务,可能会只读到并发写事务的部分数据。事实上,对于并发的单机事务,也可能存在类似的现象。

仍考虑第一节中的例子,只是插入事务T1和查询事务T2都发生在同一个DN上。如图6所示,首先T1在表t中插入v1和v2两条记录,在其提交之前,查询事务T2开始执行。在T2顺序扫描表t的过程中,首先扫描到v1记录,但是由于此时v1记录的xmin对应的XID1(T1的事务号)还没有提交,因此v1不可见。然后T1完成提交,T2继续扫描,并扫描到v2记录,此时v2记录的xmin对应的XID1已经提交,因此v2可见。这样,查询事务T2只看到了T1的部分插入数据,破坏了事务的一致性要求。

图6 单机事务一致性问题示意图

为了解决上面这个问题,openGauss采用多版本并发控制(MVCC)来保证与写事务并发执行的查询事务的一致性。

MVCC的基本机制是:写事务不会原地修改元组内容,而是将被修改的元组标记为这条记录的一个旧版本(标记xmax),同时插入一条修改后的元组,从而产生这条记录的一个新版本;对于在一个查询事务开始时还没有提交的写事务,那么这个查询事务始终认为该写事务没有提交。

在上面的例子中,在T2开始的时候,T1还没有提交,那么对于T2扫描上来的v1和v2记录,T2会认为它们xmin对应的XID1均为未提交的,即这两个新版本对于T1均不可见,因此不会返回任何一条记录,也就不会发生读到部分事务内容的异常情况了。

在MVCC中,最关键的技术点有两个:①元组版本号的实现;②快照的实现。下面详细说明这两个技术点在openGauss中的实现。

在openGauss中,采用全局递增的事务号来作为一个元组的版本号,每个写事务都会获得一个新的事务号。如上所述,一个元组的头部会记录两个事务号xmin和xmax,分别对应元组的插入事务和删除(更新)事务。xmin和xmax决定了元组的生命期,亦即该版本的可见性窗口。

相比之下,快照的实现要更为复杂。在openGauss中,有两种方式来实现快照。

§ 方法一:活跃事务数组法。

在数据库进程中,维护一个全局的数组,其中的成员为正在执行的事务信息,包括事务的事务号,该数组即活跃事务数组。在每个事务开始的时候,拷贝一份该数组内容。当事务执行过程中扫描到某个元组时,需要通过判断元组xmin和xmax这两个事务(即元组的插入事务和删除事务)对于查询事务的可见性,来决定该元组是否对查询事务可见。以xmin为例,首先查询CLOG,判断该事务是否提交,如果未提交,则不可见;如果提交,则进一步判断该xmin是否在查询事务的活跃事务数组中。如果xmin在该数组中,或者xmin的值大于该数组中事务号的最大值(事务号是全局递增发放的),那么该xmin事务一定在该查询事务开始之后才会提交,因此对于查询事务不可见;如果xmin不在该数组中,或者小于该数组中事务号的最小值,那么该xmin事务一定在该查询事务开始之前就已经提交,因此对于查询事务可见。上述判断逻辑如图7所示。

图7 基于活跃事务数组方法的事务可见性判断示意图

元组xmax事务对于查询事务的可见性判断类似。最终,xmin(元组的插入事务事务号)和xmax(元组的删除事务事务号)的不同组合,决定了该元组是否对于查询事务可见,如表1所示。

表1 事务可见性判断

§ 方案二:时间戳方法

使用活跃事务数组方法,由于该数组一般比较大,无法使用原子操作,因此在其上的读-写并发操作需要加锁互斥,写-写并发操作亦需要加锁互斥。其中,读操作是指事务开始时拷贝数组内容获取快照的操作,写操作是指事务开始时将事务信息加入到该数组中以及事务结束时将事务信息从该数组中移除的操作。在高并发的场景下,活跃事务数组会成为加锁的热点和性能瓶颈。

获取快照,本质上是要获取事务运行状态与时间的映射关系f(t)。对每一个事务来说,该f(t)函数为一个阶梯函数,如图8所示,在该事务的提交时刻点tcommit之前,f(t)为未提交状态,在tcommit之后,f(t)为提交状态。

图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)。

如图9所示,在一个事务的实际执行过程中,并不会在一开始就加载全量的CSN日志,而是在扫描到某条记录以后,才会去CSN日志中查询该条记录头部xmin和xmax这两个事务号对应的CSN值,并基于此进行可见性判断。

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

openGauss中的事务隔离性

04

在上小节中,事务的一致性反映的是某一个事务在其它并发事务“眼中”的状态。本小节要介绍事务的隔离性,是某一个事务执行过程中,它“眼中”其它所有并发事务的状态。一致性和隔离性,两者相互联系,在openGauss中均是基于MVCC和快照实现的;同时,两者又有一定区别,对于较高的隔离级别,除了MVCC和快照之外,还需要辅以其它的机制来实现。

如表2所示,在数据库业界,一般将隔离性按由低到高分为以下四个隔离级别,每个隔离级别按照在该级别下禁止发生的异常现象来定义。这些异常现象包括:

§ 脏读,指一个事务在执行过程中读到并发的、还没有提交的写事务的修改内容。

§ 不可重复读,指在同一个事务内,先后两次读到的同一条记录的内容发生了变化(被并发的写事务修改)。

§ 幻读,指在同一个事务内,先后两次执行的、谓词条件相同的范围查询,返回的结果不同(并发写事务插入了新的记录)。

隔离级别越高,在一个事务执行过程中,它能“感知”到的并发事务的影响越小。在最高的可串行化隔离级别下,任意一个事务的执行,均“感知”不到有任何其它并发事务执行的影响,并且所有事务执行的效果就和一个挨一个顺序执行的效果完全相同。

隔离级别

脏读

不可重复读

幻读

读未提交

允许

允许

允许

读已提交

不允许

允许

允许

可重复读

不允许

不允许

允许

可串行化

不允许

不允许

不允许

表2 事务隔离级别

在openGauss中,隔离级别的实现基于MVCC和快照机制,因此这种隔离方式被称为快照隔离(Snapshot Isolation,简称SI)。目前,openGauss支持读已提交(Read Committed)和可重复读(Repeatable Read)这两种隔离级别。两者实现上的差别在于在一个事务中获取快照的次数。

如果采用读已提交的隔离级别,那么在一个事务块中每条语句的执行开始阶段,都会去获取一次最新的快照,从而可以看到那些在本事务块开始以后、在前面语句执行过程中提交的并发事务的效果。如果采用可重复读的隔离级别,那么在一个事务块中,只会在第一条语句的执行开始阶段,获取一次快照,后面执行的所有语句都会采用这个快照,整个事务块中的所有语句均不会看到该快照之后提交的并发事务的效果。

我们通过具体的例子来说明一下读已提交和可重复读的区别。

考虑以下三个并发执行的事务(表t包含一个整型字段a):

T1:

代码语言:javascript复制
START TRANSACTION;
INSERT INTO t VALUES (v1);
COMMIT;

T2:

代码语言:javascript复制
START TRANSACTION;
INSERT INTO t VALUES (v2);
COMMIT;

T3:

代码语言:javascript复制
START TRANSACTION;
SELECT * FROM t;
SELECT * FROM t;
SELECT * FROM t;
COMMIT;

这三个事务的并发执行顺序如图10所示。我们考虑T3事务三条查询的返回结果。如果采用读已提交的隔离级别,那么在第一条查询开始时,首次获取快照,T1和T2均没有提交,因此它们都在快照中,查询结果不会包含它们插入的新记录;在第二条查询开始时,第二次获取快照,T1已经提交,在第二条查询语句的快照中,只有T2,因此可以查询到T 1插入的记录v1;同理,在第三条查询开始时,第三次获取快照,T1和T2均已经提交,它们都不在第三条语句的快照中,因此可以查询到它们插入的记录v1和v2。

另一方面,如果采用可重复读的隔离级别,对于T3中的三条查询语句,均会采用第一条语句执行开始时的快照,而T1和T2均在该快照中,因此在该隔离级别下,T3的三条查询语句均不会返回v1和v2。

图10 读已提交和可重复读隔离级别在并发事务下的表现区别

未完待续......

0 人点赞