暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

openGauss数据库源码解析系列文章——事务机制源码解析(一)

Gauss松鼠会 2021-07-03
1109

Gauss松鼠会

学习 探索 分享数据库知识 共建数据库技术交流圈

关注
事务是数据库操作的执行单位,需要满足最基本的ACID(原子性、一致性、隔离性、持久性)属性。
(1) 原子性:一个事务提交之后要么全部执行,要么全部不执行。
(2) 一致性:事务的执行不能破坏数据库的完整性和一致性。
(3) 隔离性:事务的隔离性是指在并发中,一个事务的执行不能被其他事务干扰。
(4) 持久性:一旦事务完成提交,那么它对数据库的状态变更就会永久保存在数据库中。
本篇主要从事务整体架构和代码概览事务并发控制两方面展开讨论,其中事务并发控制从事务状态机事务ID分配及CLOG/CSNLOG两方面进行详细介绍。

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

事务模块总体结构如图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. 事务上层状态机

事务块上层状态机结构体代码如下:
    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定义代码如下:
          #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所示

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

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

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

            3) 关键函数

            64位xid页面xid_base的计算函数:

            (1) Heap_page_prepare_for_xid函数:在对页面有写入操作时调用,用来调节xid_base。
            ①新来xid在“xid_base + FirstNormalxid”与“xid_base + MaxShortxid(0xFFFFFFFF)”之间时,当前的xid_base不需要调整。
            ② 新来xid在“xid_base + FirstNormalxid”左侧(xid小于该值)时,需要减小xid_base。
            ③新来xid在“xid_base + MaxShortxid”右侧(xid大于该值)时,需要增加xid_base。
            ④特殊情况下,由于页面的xid跨度大于32位能表示的范围时,就需要冻结掉本页面上较小的xid,即将提交的xid设为FrozenTransactionId(2),该值对所有事务均可见;将回滚的xid设为InvalidTransactionId(0),该值对所有的事务均不可见。
            (2) Freeze_single_heap_page函数:对该页面上较小的xid进行冻结操作。
            ①计算oldestxid,比该值小的事务已经无任何事务访问更老的版本,此时可以将提交的xid直接标记为FrozenTransactionId,即对所有事务可见;将回滚的xid标记为InvalidTransactionId,即对所有事务不可见。
            ②页面整理,清理hot update链,重定向itemid,整理页面空间。
            ③根据oldestxid处理各个元组。
            (3) Heap_page_shift_base函数:更新xid_base,调整页面中各个元组头中的xmin/xmax。
            (4) GetNewTransactionId函数:获取最新的事务id。

            由于内容较多,关于事务并发控制MVCC可见性判断机制及进程内多线程管理机制方面的内容将在下篇图文进行分享,敬请期待!

            END
            Gauss松鼠会
            汇集数据库从业人员及爱好者
            互助解决问题 共建数据库技术交流圈
            PC端阅读,请点击“阅读原文”
            文章转载自Gauss松鼠会,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

            评论