2025-05-29
文件系统
0

目录

一、事务整体架构和代码概览
二、事务并发控制
(一)事务状态机
1. 事务上层状态机
2. 事务底层状态
(二)事务ID分配及CLOG/CSNLOG
1. 64位xid及其分配
2. CLOG、CSNLOG
(三)CSN机制
(四)提交流程
参考

事务是数据库操作的执行单位,需要满足最基本的ACID(原子性、一致性、隔离性、持久性)属性。

(1) 原子性: 一个事务提交之后要么全部执行,要么全部不执行。

(2) 一致性: 事务的执行不能破坏数据库的完整性和一致性。

(3) 隔离性: 事务的隔离性是指在并发中,一个事务的执行不能被其他事务干扰。

(4) 持久性: 一旦事务完成提交,那么它对数据库的状态变更就会永久保存在数据库中。

显式事务和隐式事务

  • 显式事务是指,用户在所执行的一条或多条SQL语句的前后,显式添加了开启事务START TRANSACTION语句和提交事务COMMIT语句。
  • 隐式事务是指,用户在所执行的一条或多条SQL语句的前后,没有显式添加开启事务和提交事务的语句。在这种情况下,每一条SQL语句在开始执行时,openGauss内部都会为其开启一个事务,并且在该语句执行完成之后,自动提交该事务。

一、事务整体架构和代码概览

事务模块总体结构如图1所示。

图1 总体结构

在openGauss中,事务的实现与存储引擎的实现有很强关联,代码主要集中在src/gausskernel/storage/access/transam及src/gausskernel/storage/lmgr下,关键文件如图1所示。

  1. 事务管理器:事务系统的中枢,它的实现是一个有限循环状态机,通过接受外部系统的命令并根据当前事务所处的状态决定事务的下一步执行过程。
  2. 日志管理器:用来记录事务执行的状态以及数据变化的过程,包括事务提交日志(CLOG)、事务提交序列日志(CSNLOG)以及事务日志(XLOG)。其中CLOG日志只用来记录事务执行的结果状态,CSNLOG记录日志提交的顺序,用于可见性判断;XLOG是数据的redo日志,用于恢复及持久化。
  3. 线程管理机制:通过一片内存区域记录所有线程的事务信息,任何一个线程可以通过访问该区域获取其他事务的状态信息。
  4. MVCC机制:openGauss系统中,事务执行读流程结合各事务提交的CSN序列号,采用了多版本并发控制机制,实现了元组的读和写互不阻塞。详细可见性判断方法见“二 事务并发控制”。
  5. 锁管理器:实现系统的写并发控制,通过锁机制来保证事务写流程的隔离性。

二、事务并发控制

事务并发控制机制用来保证并发执行事务的情况下openGauss的ACID特性。下面将逐一介绍事务并发控制的各组成部分。

(一)事务状态机

openGauss将事务系统分为上层(事务块TBlockState)以及底层(TransState)两个层次。通过分层的设计,在处理上层业务时可以屏蔽具体细节,实现灵活支持客户端各类事务执行语句(BEGIN/START TRANSACTION/COMMIT/ROLLBACK/END)。

  1. 事务块TBlockState:客户端query的状态,用于提高用户操作数据的灵活性,用事务块的形式支持在一个事务中执行多条query语句。
  2. 底层事务TransState:内核端视角,记录了整个事务当前处于的具体状态。

1. 事务上层状态机

事务块上层状态机结构体代码如下:

c++
typeset enum TBlockState { /* 不在事务块中的状态:单条SQL语句 */ TBLOCK_DEFAULT,/* 事务块缺省状态 */ TBLOCK_STARTED,/*执行单条query 语句*/ /* 处于事务块中的状态:一个事务包含多条语句 */ TBLOCK_BEGIN,/* 遇到事务开始命令BEGIN/START TRANSACTION */ TBLOCK_INPROGRESS,/* 表明正在事务块处理过程中*/ TBLOCK_END,/ *遇到事务结束命令END/COMMIT */ TBLOCK_ABORT,/* 事务块内执行报错,等待客户端执行ROLLBACK */ TBLOCK_ABORT_END,/ *在事务块内执行报错后,接收客户端执行ROLLBACK */ TBLOCK_ABORT_PENDING,/* 事务块内执行成功,接收客户端执行ROLLBACK(期望事务回滚)*/ TBLOCK_PREPARE,/ *两阶段提交事务,收到PREPARE TRANSACTION命令*/ /* 子事务块状态,与上述事务块状态类似 */ TBLOCK_SUBBEGIN,/* 遇到子事务开始命令SAVEPOINT */ TBLOCK_SUBINPROGRESS,/* 表明正在子事务块处理过程中*/ TBLOCK_SUBRELEASE,/* 遇到子事务结束命令RELEASE SAVEPOINT */ TBLOCK_SUBCOMMIT,/* 遇到事务结束命令END/COMMIT 从最底层的子事务递归的提交到最顶层事务*/ TBLOCK_SUBABORT,/* 子事务块内执行报错,等待客户端ROLLBACK TO/ROLLBACK */ TBLOCK_SUBABORT_END,/* 在子事务块内执行报错后,接收到客户端ROLLBACK TO上层子事务/ROLLBACK */ TBLOCK_SUBABORT_PENDING,/* 子事务块内执行成功,接收客户端执行的ROLLBACK TO上层子事务/ROLLBACK */ TBLOCK_SUBRESTART,/* 子事务块内执行成功,收到ROLLBACK TO当前子事务*/ TBLOCK_SUBABORT_RESTART/* 子事务块内执行报错后,接收到ROLLBACK TO当前子事务*/ } TBlockState;

为了便于理解,可以先不关注子事务块的状态。当理解了主事务的状态机行为后,子事务块的状态机转换同父事务类似。父子事务的关系类似于一个栈的实现,父事务的子事务相较于父事务后开始先结束。

显式事务块的状态机及相应的转换函数如图2所示。

图2事务块的状态机

表1 事务块状态

事务状态事务状态机结构体
默认TBLOCK_DEFAULT
已开始TBLOCK_STARTED
事务块开启TBLOCK_BEGIN
事务块运行中TBLOCK_INPROGRESS
事务块结束TBLOCK_END
回滚TBLOCK_ABORT
回滚结束TBLOCK_ABORT_END
回滚等待TBLOCK_ABORT_PENDING

在无异常情形下,一个事务块的状态机如图2所示按照默认(TBLOCK_DEFAULT)->已开始(TBLOCK_STARTED)->事务块开启(TBLOCK_BEGIN)->事务块运行中(TBLOCK_INPROGRESS)->事务块结束(TBLOCK_END)->默认(TBLOCK_DEFAULT)循环。剩余的状态机是在上述正常场景下的各个状态点的异常处理分支。

  1. 在进入事务块运行中(TBLOCK_INPROGRESS)之前出错,因为事务还没有开启,直接报错并回滚,清理资源回到默认(TBLOCK_DEFAULT)状态。

  2. 在事务块运行中(TBLOCK_INPROGRESS)出错分为2种情形。

    1. 事务执行失败:事务块运行中(TBLOCK_INPROGRESS)->回滚(TBLOCK_ABORT)->回滚结束(TBLOCK_ABORT_END)->默认(TBLOCK_DEFAULT);
    2. 用户手动回滚执行成功的事务:事务块运行中(TBLOCK_INPROGRESS)->回滚等待(TBLOCK_ABORT_PENDING)->默认(TBLOCK_DEFAULT)。
  3. 在用户执行COMMIT语句时出错:事务块结束(TBLOCK_END)->默认(TBLOCK_DEFAULT)。由图2可以看出,事务开始后离开默认(TBLOCK_DEFAULT)状态,事务完全结束后回到默认(TBLOCK_DEFAULT)状态。

  4. openGauss同时还支持隐式事务块,当客户端执行单条SQL语句时可以自动提交,其状态机相对比较简单:按照默认(TBLOCK_DEFAULT)->已开始(TBLOCK_STARTED)->默认(TBLOCK_DEFAULT)循环。

2. 事务底层状态

TransState结构体代码如下:从内核视角的事务状态,真正意义上的事务状态。

c++
typedef enum TransState { TRANS_DEFAULT,/* 当前为空闲缺省状态,无事务开启*/ TRANS_START,/* 事务正在开启*/ TRANS_INPROGRESS,/* 事务开始完毕,进入事务运行中*/ TRANS_COMMIT,/* 事务正在提交*/ TRANS_ABORT,/* 事务正在回滚*/ TRANS_PREPARE/* 两阶段提交事务进入PREPARE TRANSACTION阶段*/ } TransState;

图3事务底层状态

内核内部底层状态如图3所示,底层状态机的描述见结构体TransState。 (1) 在事务开启前事务状态为TRANS_DEFAULT。 (2) 事务开启过程中事务状态为TRANS_START。 (3) 事务成功开启后一直处于TRANS_INPROGRESS。 (4) 事务结束/回滚的过程中为TARNS_COMMIT/ TRANS_ABORT。 (5) 事务结束后事务状态回到TRANS_DEFAULT。

(二)事务ID分配及CLOG/CSNLOG

为了在数据库内部区别不同的事务,openGauss数据库会为它们分配唯一的标识符,即事务id(transaction id,缩写xid),xid是uint64单调递增的序列。当事务结束后,使用CLOG记录是否提交,使用CSNLOG(commit sequence number log)记录该事务提交的序列,用于可见性判断。

1. 64位xid及其分配

openGauss对每一个写事务均会分配一个唯一标识。当事务插入时,会将事务信息写到元组头部的xmin,代表插入该元组的xid;当事务进行更新和删除时,会将当前事务信息写到元组头部的xmax,代表删除该元组的xid。当前事务id的分配采用的是uint64单调递增序列,为了节省空间以及兼容老的版本,当前设计是将元组头部的xmin/xmax分成两部分存储,元组头部的xmin/xmax均为uint32的数字;页面的头部存储64位的xid_base,为当前页面的xid_base。

元组结构如图8所示,页面头结构如图9所示,那么对于每一条元组真正的xmin、xmax计算公式即为:元组头中xmin/xmax + 页面xid_base。

图8 元组结构

图9 页面头结构

当页面不断有更大的xid插入进来时,可能超过“xid_base + 232”,此时需要通过调节xid_base来满足所有元组的xmin/xmax都可以通过该值及元组头部的值计算出来,详细逻辑见“2. CLOG、CSNLOG”内“3) 关键函数:”中的第(3)小节。

为了使xid不消耗过快,openGauss当前只对写事务进行xid的分配,只读事务不会额外分配xid,也就是说并不是任何事务一开始都会分配xid,只有真正使用xid时才会去分配。在分配子事务xid时,如果父事务还未分配xid,则会先给父事务分配xid,再给子事务分配xid,确保子事务的xid比父事务大。理论上64位xid已经足够使用:假设数据库的tps为1000万,即1秒钟处理1000万个事务,64xid可以使用58万年。

2. CLOG、CSNLOG

CLOG以及CSNLOG分别维护事务ID->CommitLog以及事务ID->CommitSeqNoLog的映射关系。由于内存的资源有限,并且系统中可能会有长事务存在,内存中可能无法存放所有的映射关系,此时需要将这些映射写盘成物理文件,所以产生了CLOG(XID->CommitLog Map)、CSNLOG(XID->CommitSeqNoLog Map)文件。CSNLOG以及CLOG均采用了SLRU(simple least recently used,简单最近最少使用)机制来实现文件的读取及刷盘操作。

1) CLOG用于记录事务id的提交状态。openGauss中对于每个事务id使用2个bit位来标识它的4种状态。CLOG定义代码如下:

C++
#define CLOG_XID_STATUS_IN_PROGRESS 0x00 表示事务未开始或还在运行中(故障场景可能是crash) #define CLOG_XID_STATUS_COMMITTED 0x01 表示该事务已经提交 #define CLOG_XID_STATUS_ABORTED 0x02 表示该事务已经回滚 #define CLOG_XID_STATUS_SUB_COMMITTED 0x03 表示子事务已经提交而父事务状态未知

CLOG页面的物理组织形式如图10所示。

图10CLOG页面的物理组织形式

图10表示事务1、4、5还在运行中,事务2已经提交,事务3已经回滚。

2) CSNLOG用于记录事务提交的序列号。openGauss为每个事务id分配8个字节uint64的CSN号,所以一个8kB页面能保存1k个事务的CSN号。CSNLOG达到一定大小后会分块,每个CSNLOG文件块的大小为256kB。同xid号类似,CSN号预留了几个特殊的号。CSNLOG定义代码如下:

C++
#define COMMITSEQNO_INPROGRESS UINT64CONST(0x0) 表示该事务还未提交或回滚 #define COMMITSEQNO_ABORTED UINT64CONST(0x1) 表示该事务已经回滚 #define COMMITSEQNO_FROZEN UINT64CONST(0x2) 表示该事务已提交,且对任何快照可见 #define COMMITSEQNO_FIRST_NORMAL UINT64CONST(0x3) 事务正常的CSN号起始值 #define COMMITSEQNO_COMMIT_INPROGRESS (UINT64CONST(1) << 62) 事务正在提交中

同CLOG相似,CSNLOG的物理结构体如图11所示。

图11 CSNLOG的物理结构体

事务id 2048、2049、2050、2051、2052、2053的对应的CSN号依次是5、4、7、10、6、8;也就是说事务提交的次序依次是2049->2048->2052->2050->2053->2051。

(三)CSN机制

1) CSN原理简单如图1所示

图1 CSN原理

每个非只读事务在运行过程中会取得一个xid号,在事务提交时会推进CSN,同时会将当前CSN与事务的xid映射关系保存起来(CSNLOG)。图5-12中,实心竖线标识取snapshot(快照)时刻,会获取最新提交CSN(3)的下一个值4。TX1、TX3、TX5已经提交,对应的CSN号分别是1、2、3。TX2、TX4、TX6正在运行,TX7、TX8是未来还未开启的事务。对于当前snapshot而言,严格小于CSN号4的事务提交结果均可见;其余事务提交结果在获取快照时刻还未提交,不可见。

(四)提交流程

事务提交流程如图3所示。

图3提交流程

(1) 设置CSN-XID映射commit-in-progress标记。

(2) 原子更新NextCommitSeqNo值。

(3) 生成redo日志,写CLOG,写CSNLOG。

(4) 更新PGPROC将对应的事务信息从PGPROC中移除,xid设置为InvalidTransactionId、xmin设置为InvalidTransactionId等。

热备支持

在事务的提交流程步骤(1)和(2)之间,增加 commit-in-progress 的XLOG 日志。备机在读快照时,首先获取轻量锁 ProcArrayLock ,并计算当前快照。如果使用当前快照中的CSN,碰到 XID 对应的 CSN 号有 COMMITSEQNO_INPROGRESS 标记,则必须等待相应的事务提交 XLOG 回放结束后再读取对应的 CSN 判断是否可见。为了实现上述等待操作,备机在对 commit-in-progress 的 XLOG 日志 做 redo 操作时,会调用 XactLockTableInsert 函数获取相应 XID 的事务排他锁;其他的读事务如果访问到该XID,会等待在此 XID 的事务锁上,直到相应的事务提交XLOg回放结束后再继续运行。

参考

[1]openGauss数据库源码解析,https://www.zhihu.com/column/c_1233714215367319552

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:司小远

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!