目录
openGauss数据库SQL引擎
openGauss数据库执行器技术
openGauss存储技术
openGauss事务机制
Ⅰ.openGauss数据库事务概览
Ⅱ.openGauss事务ACID特性介绍
1.openGauss中的事务持久性
2.openGauss中的事务原子性
3.openGauss中的事务一致性
4.openGauss中的事务隔离性
Ⅲ.openGauss并发控制
Ⅳ.openGauss分布式事务
openGauss数据库安全
openGauss事务机制
openGauss事务ACID特性介绍
本节主要介绍openGauss中如何保证单机事务的ACID,在此基础上,在之后文章的第四节中将说明如何保证分布式事务的ACID。
openGauss中的事务持久性01
和业界几乎所有的数据库一样,openGauss通过将事务对于数据库的修改写入可以永久(长时间)保存的存储介质中,来保证事务的持久性。这个过程被称为事务的持久化过程。持久化过程是保证事务持久性所必不可少的环节,其效率对于数据库整体性能影响很大,常常成为数据库的性能瓶颈所在。
最常用的存储介质是磁盘。对于磁盘来说,其每次读写操作都有一个“启动”代价,因此在单位时间内(每秒内),一个磁盘可以进行的读写操作次数(Input/Output Operations Per Second,简称IOPS)是有上限的。HDD磁盘的IOPS一般在1000次/秒以下,SSD磁盘的IOPS可以达到10000次/秒左右。另外一方面,如果多个磁盘读写请求的数据在磁盘上是相邻的,那么可以被合并为一次读写操作,这导致磁盘顺序读写的性能通常要远优于随机读写。
一般来说,尤其是在OLTP场景下,用户对于数据库数据的修改是比较分散随机的。如果在持久化过程中,直接将这些分散的数据写入磁盘,那么这个随机写入的性能是比较差的。因此,数据库通常都采用预写日志(Write Ahead Log,简称WAL)来避免持久化过程中的随机IO,如图4(a)所示。所谓预写日志,是指在事务提交的时候,先将事务对于数据库的修改写入一个顺序追加的WAL日志文件中。由于WAL日志的写操作是顺序IO,因此其可以达到一个比较高的性能。另一方面,对于真正修改的物理数据文件,再等待合适的时机写入磁盘,以尽可能合并该数据文件上的IO操作。
在一个事务完成日志的下盘操作(即写入磁盘)以后,该事务就可以完成提交动作。如果在此之后数据库发生宕机,那么数据库会首先从已经写入磁盘的WAL文件中恢复出该事务对于数据库的修改操作,从而保证事务一旦提交即具备持久性的特点。
下面结合图4(b)中的例子,简单说明数据库故障恢复的原理。假设一个事务需要在表A(对应数据文件A)和表B(对应数据文件B)中各插入一行新的记录,在数据库内部,其执行的顺序如下:(1)记录修改数据文件A的日志,(2)记录修改数据文件B的日志,(3)在数据文件A中写入新的记录,(4)在数据文件B中写入新的记录。在上述过程中,如果在第(4)步执行时数据库发生宕机,那么该事务对于数据文件B的修改可能全部或部分丢失。当数据库再次启动以后,在其能够接受新的业务之前,需要将这些可能丢失的修改从日志中找回来(该操作被称为日志回放操作)。
在日志回放过程中,数据库会根据日志记录的先后顺序,依次读取每个日志的内容,然后判断该日志记录的事务对数据库数据文件的修改是否和当前相关数据文件的内容一致。如果一致,说明上次数据库停机之前修改已经写入数据文件中,该日志修改无需回放;如果不一致,说明上次数据库停机之前修改还未写入数据文件中,上次数据库停机可能是异常宕机导致,该日志对应的事务操作需要重新在相关数据文件中再次执行,才能保证恢复成功。
对于本例,在数据库恢复过程中,首先读取到在数据文件A中插入记录的日志,将数据文件A读取上来之后,发现数据文件A中已经包含该记录,因此该日志无需回放;然后读取到在数据文件B中插入记录的日志,将数据文件B读取上来之后,发现数据文件B中未包含新插入的记录,因此需要将日志中的记录再次写入到数据文件B中,从而完成恢复。最终,该事务对于数据库所有的修改都得以恢复出来,事务的持久性得到了保证。
(a) WAL日志和数据页面的关系示意图
(b)WAL日志和故障恢复示意图
图4 WAL日志和事务持久性示意图
openGauss中的事务原子性02
如图5所示,openGauss通过WAL日志、事务提交日志以及更新记录的多版本来保证写事务的原子性。
图5 openGauss事务的原子性示意图
(1)对于插入事务,例如以下插入事务:
START TRANSACTION;
INSERT INTO t(a) VALUES (v1);
INSERT INTO t(a) VALUES (v2);
COMMIT;
通常,我们将一条记录在数据库内部的物理组织方式称为元组,其在形式上类似一个结构体。在上述插入事务的执行过程中,对于每一条新插入的记录,在它们元组结构体头部的xmin成员处都附加了插入事务的唯一标识,即一个全局递增的事务号(Transaction ID,简称XID)。如10.2.1节中所述,这两条插入的记录(元组)连同它们的头部会被顺序写入WAL日志中。
在该事务的提交阶段,在WAL日志中,会插入一条事务提交日志,以持久化该事务的提交结果,并会在专门的事务提交信息日志(Commit LOG,CLOG)中记录该事务号对应的事务提交结果(提交还是回滚)。此后,如果有查询事务读到这两条记录,会首先去CLOG中查询记录头部事务号对应的提交信息,如果为提交,并且通过可见性判断,那么这两条记录会在查询结果中返回;如果CLOG中事务号为回滚状态,或者CLOG中事务号为提交状态但是该事务号对该查询不可见,那么这两条记录不会在查询结果中返回。如上,在没有故障发生的情况下,上述插入两行记录的事务是原子的,不会发生只看到插入一条的“中间状态”。
下面考虑故障场景。
§ 如果在事务写下提交日志之前,数据库发生宕机,那么数据库恢复过程中虽然会把这两条记录插入到数据页面中,但是并不会在CLOG中将该插入事务号标识为提交状态,后续查询也不会返回这两条记录。
§ 如果在事务写下提交日志之后,数据库发生宕机,那么数据库恢复过程中,不仅会把这两条记录插入到数据页面中。同时,还会在CLOG中将该插入事务号标识为提交状态,后续查询可以同时看见这两条插入的记录。如上,在故障场景下,上述插入两行记录的事务操作亦是原子的。
(2)对于删除事务,例如:
START TRANSACTION;
DELETE FROM t WHERE a = v1;
DELETE FROM t WHERE a = v2;
COMMIT;
在该删除事务的执行过程中,对于上面每一条被删除的记录,在它们元组头部的xmax成员处都附加了删除事务的事务号。同时,与插入操作相同,该删除事务的提交状态通过事务提交日志物化,并记录到CLOG中。从而,无论在正常场景还是故障场景下,如果后续查询涉及上述被删除的那些记录,它们的可见性均取决于统一的、在CLOG中记录的删除事务的状态,不会发生部分记录能查询到、部分记录不能查询到的“中间状态”。
(3)对于更新事务,例如:
START TRANSACTION;
UPDATE t set a = v1’ WHERE a = v1;
UPDATE t set a = v2’ WHERE a = v2;
COMMIT;
在openGauss中,上述更新事务等同于先删除v1和v2这两行老版本记录,再插入v1和v2这两行新版本记录,删除和插入事务的原子性已经在(1)和(2)中说明,因此更新事务亦是原子的。
未完待续......