学习 探索 分享数据库知识 共建数据库技术交流圈
(1) 原子性:一个事务提交之后要么全部执行,要么全部不执行。
(2) 一致性:事务的执行不能破坏数据库的完整性和一致性。
(3) 隔离性:事务的隔离性是指在并发中,一个事务的执行不能被其他事务干扰。
(4) 持久性:一旦事务完成提交,那么它对数据库的状态变更就会永久保存在数据库中。
事务
整体架构和代码概览
及
事务并发控制
两方面展开讨论,其中事务并发控制从事务状态机、事务ID分配及CLOG/CSNLOG两方面进行详细介绍。
一、事务整体架构和代码概览
(1) 事务管理器:事务系统的中枢,它的实现是一个有限循环状态机,通过接受外部系统的命令并根据当前事务所处的状态决定事务的下一步执行过程。
(2) 日志管理器:用来记录事务执行的状态以及数据变化的过程,包括事务提交日志(CLOG)、事务提交序列日志(CSNLOG)以及事务日志(XLOG)。其中CLOG日志只用来记录事务执行的结果状态,CSNLOG记录日志提交的顺序,用于可见性判断;XLOG是数据的redo日志,用于恢复及持久化。
(3) 线程管理机制:通过一片内存区域记录所有线程的事务信息,任何一个线程可以通过访问该区域获取其他事务的状态信息。
(4) MVCC机制:openGauss系统中,事务执行读流程结合各事务提交的CSN序列号,采用了多版本并发控制机制,实现了元组的读和写互不阻塞。详细可见性判断方法见“二 事务并发控制”。
(5) 锁管理器:实现系统的写并发控制,通过锁机制来保证事务写流程的隔离性。
二、事务并发控制
(一)事务状态机
(1) 事务块TBlockState:客户端query的状态,用于提高用户操作数据的灵活性,用事务块的形式支持在一个事务中执行多条query语句。
(2) 底层事务TransState:内核端视角,记录了整个事务当前处于的具体状态。
1. 事务上层状态机
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 事务块状态机
图2中的事务状态相对应的事务状态机结构体中的值如表1所示。
表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种情形。事务执行失败:事务块运行中(TBLOCK_INPROGRESS)->回滚(TBLOCK_ABORT)->回滚结束(TBLOCK_ABORT_END)->默认(TBLOCK_DEFAULT);用户手动回滚执行成功的事务:事务块运行中(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结构体代码如下:从内核视角的事务状态,真正意义上的事务状态。
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。
3. 事务状态机系统实例
本小节给出一条SQL的状态机运转实例,有助于更好地理解内部事务如何运作。在客户端执行SQL语句:
BEGIN;
SELECT * FROM TABLE1;
END;
1) 整体流程
整体执行过程如图4,任何语句的执行总是先进入事务处理接口事务块中,然后调用事务底层函数处理具体命令,最后返回到事务块中。
图4 整体流程
2) BEGIN执行流程,如图5所示。
(1) 入口函数exec_simple_query处理begin命令。
(2) start_xact_command函数开始一个query命令,调用StartTransactionCommand函数,此时事务块上层状态未TBLOCK_DEFAULT,继续调用StartTransaction函数,设置事务底层状态TRANS_START,完成内存、缓存区、锁资源的初始化后将事务底层状态设为TRANS_INPROGRESS,最后在StartTransactionCommand函数中设置事务块上层状态为TBLOCK_STARTED。
(3) PortalRun函数处理begin语句,依次向下调用函数,最后调用BeginTransactionBlock函数转换事务块上层状态为TBLOCK_BEGIN。
(4) finish_xact_command函数结束一个query命令,调用CommitTransactionCommand函数设置事务块上层状态从TBLOCK_BEGIN变为TBLOCK_INPROGRESS,并等待读取下一条命令。
图5 BEGIN执行流程
3) SELECT执行流程,如图6所示。
(1) 入口函数exec_simple_query处理“SELECT * FROM table1;”命令。
(2) start_xact_command函数开始一个query命令,调用StartTransactionCommand函数,由于当前上层事务块状态为TBLOCK_INPROGRESS,说明已经在事务块内部,则直接返回,不改变事务上层以及底层的状态。
(3) PortalRun执行SELECT语句,依次向下调用函数ExecutorRun根据执行计划执行最优路径查询。
(4) finish_xact_command函数结束一条query命令,调用CommitTransactionCommand函数,当前事务块上层状态仍为TBLOCK_INPROGESS,不改变当前事务上层以及底层的状态。
图6 SELECT执行流程
4) END执行流程,如图7所示。
(1) 入口函数exec_simple_query处理end命令。
(2) start_xact_command函数开始一个query命令,调用StartTransactionCommand函数,当前上层事务块状态为TBLOCK_INPROGESS,表明事务仍然在进行,此时也不改变任何上层及底层事务状态。
(3) PortalRun函数处理end语句,依次调用processUtility函数,最后调用EndTransactionBlock函数对当前上层事务块状态机进行转换,设置事务块上层状态为TBLOCK_END。
(4) Finish_xact_command函数结束query命令,调用CommitTransactionCommand函数,当前事务块状态TBLOCK_END;继续调用CommitTransaction函数提交事务,设置事务底层状态为TRANS_COMMIT,进行事务提交流程并且清理事务资源;清理后设置底层事务状态为TRANS_DEFAULT,返回CommitTansactionCommand函数;设置事务块上层状态为TBLOCK_DEFAULT,整个事务块结束。
图7 END执行流程
4. 事务状态转换相关函数简述
1) 事务处理子函数:根据当前事务上层状态机,对事务的资源进行相应的申请、回收及清理。
具体介绍如表2所示。
表2 事务处理子函数
子函数 |
说明 |
StartTransaction |
开启事务,对内存及变量进行初始化操作,完成后将底层事务状态置为TRANS_INPROGRESS |
CommitTransaction |
当前的底层状态机为TRANS_INPROGRESS,然后置为TRANS_COMMIT,本地持久化CLOG及XLOG日志,并清空相应的事务槽位信息,最后将底层状态机置为TRANS_DEFAULT |
PrepareTransaction |
当前底层状态机为TRANS_INPROGRESS,同前面描述的CommitTransaction函数类似处理,设置底层状态机为TRANS_PREPARE,构造两阶段GXACT结构并创建两阶段文件,加入dummy的槽位信息,将线程的锁信息转移到dummy槽位中,释放资源,最后将底层状态机置为TRANS_DEFAULT |
AbortTransaction |
释放LWLock、UnlockBuffers、LockErrorCleanup,当前底层状态为TRANS_INPROGRESS,设置为TRANS_ABORT,记录相应的CLOG日志,清空事务槽位信息,释放各类资源 |
CleanupTransaction |
当前底层状态机应为TRANS_ABORT,继续清理一些资源,一般紧接着AbortTransaction调用 |
FinishPreparedTransaction |
结束两阶段提交事务 |
StartSubTransaction |
开启子事务 |
CommitSubTransaction |
提交子事务 |
AbortSubTransaction |
回滚子事务 |
CleanupSubTransaction |
清理子事务的一些资源信息,类似于CleanupTransaction |
PushTransaction/PopTransaction |
子事务类似于一个栈式的信息,Push/Pop函数是开启和结束子事务时使用 |
2) 处理函数,根据相应的状态机调用子函数。
具体介绍如表3所示。
表3 事务执行函数
函数 |
说明 |
StartTransactionCommand |
事务开始时根据上层状态机调用相应的事务执行函数 |
CommitTransactionCommand |
事务结束时根据上层状态机调用相应的事务执行函数 |
AbortCurrentTransaction |
事务内部出错,长跳转longjump调用,提前清理掉相应的资源,并将事务上层状态机置为TBLOCK_ABORT |
3) 上层事务状态机控制函数
具体介绍如表4所示。
表4 上层事务状态机控制函数
函数 |
说明 |
BeginTransactionBlock |
开启一个显式事务时,将上层事务状态机变为TBLOCK_BEGIN |
EndTransactionBlock |
显式提交一个事务时,将上层事务状态机变为TBLOCK_END |
UserAbortTransactionBlock |
显式回滚一个事务时,将上层事务状态机变为TBLOCK_ABORT_PENDING/ TBLOCK_ABORT_END |
PrepareTransactionBlock |
显式执行PREPARE语句,将上层事务状态机变为TBLOCK_PREPARE |
DefineSavepoint |
执行savepoint语句,通过调用PushTransaction将子事务上层事务状态机变为TBLOCK_SUBBEGIN |
ReleaseSavepoint |
执行release savepoint语句,将子事务上层状态机转变为TBLOCK_SUBRELEASE |
RollbackToSavepoint |
执行“rollback to”语句,将所有子事务上层状态机转变为TBLOCK_SUBABORT_PENDING/ TBLOCK_SUBABORT_END,顶层事务的上层状态机转变为TBLOCK_SUBABORT_RESTART |
(二)事务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使用4个bit位来标识它的状态。CLOG定义代码如下:
LOG
页面的物理组织形式如图
10
所示
。
图10 CLOG页面的物理组织形式
图10标识事务1、4、5还在运行中,事务2已经提交,事务3已经回滚。
2) CSNLOG用于记录事务提交的序列号。openGauss为每个事务id分配8个字节uint64的CSN号,所以一个8kB页面能保存1k个事务的CSN号。CSNLOG达到一定大小后会分块,每个CSNLOG文件块的大小为256kB。同xid号类似,CSN号预留了几个特殊的号。CSNLOG定义代码如下:
C
LOG
相似
,
C
SNLOG
的物理结构体如图
11
所示
。
图11 CSNLOG的物理结构体
事务id 2048、2049、2050、2051、2052、2053的对应的CSN号依次是5、4、7、10、6、8;也就是说事务提交的次序依次是2049->2048->2052->2050->2053->2051。
3) 关键函数
64位xid页面xid_base的计算函数:
(1) Heap_page_prepare_for_xid函数:在对页面有写入操作时调用,用来调节xid_base。
(2) Freeze_single_heap_page函数:对该页面上较小的xid进行冻结操作。
(3) Heap_page_shift_base函数:更新xid_base,调整页面中各个元组头中的xmin/xmax。
(4) GetNewTransactionId函数:获取最新的事务id。
由于内容较多,关于事务并发控制MVCC可见性判断机制及进程内多线程管理机制方面的内容将在下篇图文进行分享,敬请期待!
本文分享自微信公众号 - Gauss松鼠会(gh_18130286cd60)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。