PostgreSQL两阶段提交事务源码分析

源码版本:PG 13.3

PG 支持两阶段提交事务(2PC),本文基于 PG 13.3 源码,简单分析 2PC 事务处理相关的逻辑。

1. 两阶段事务提交的处理过程

2PC 各阶段的命令调用主要包含如下三个:

  • prepare trancation xxx
  • commit prepared xxx
  • rollback prepared xxx

prepare transaction xxx 命令表示让名称为 xxx 的事务就绪,此时事务并不可见,其主要是调用如下两个函数:

  • StartPrepare() 函数,把事务相关的数据全部存储到内存变量 static struct xllist records。
  • EndPrepare() 函数,把 StartPrepare() 函数中保存到 records 变量中的数据写入 wal。

看一个例子,prepare transaction 'T200' 的调用堆栈:

exec_simple_query
    PortalRun
        PortalRunMulti
            PortalRunUtility
                ProcessUtility
                    standard_ProcessUtility
                        PrepareTransactionBlock //设置gid,此处为T200
    finish_xact_command
        CommitTransactionCommand
            PrepareTransaction
                StartPrepare
                EndPrepare

commit prepared 和 rollback prepared 命令用于将已经就绪(prepared)的 2PC 事务提交或者回滚,两者最终都是调用 FinishPreparedTransaction() 函数,通过参数 isCommit 来区别是提交还是回滚操作。

举个例子 commit prepared 'T200' 的调用堆栈,如下:

exec_simple_query
    PortalRun
        PortalRunMulti
            PortalRunUtility
                ProcessUtility
                    standard_ProcessUtility
                        FinishPreparedTransaction
                            RecordTransactionCommitPrepared //提交
                            RecordTransactionAbortPrepared  //回滚
  • RecordTransactionCommitPrepared 函数的主要逻辑就是将事务相关的数据写到 wal 日志,然后在 clog 中将 xid 对应的事务状态设置为 commited。
  • RecordTransactionAbortPrepared 函数的主要逻辑是将事务 abort 相关的数据写入 wal 日志,然后在 clog 中将 xid 对应的事务状态设置为 aborted。

2. 两阶段事务相关数据结构

两阶段事务相关的信息会存储在 TwoPhaseState 指向的共享内存里,该变量定义如下:

static TwoPhaseStateData *TwoPhaseState;

  • TwoPhaseShmemSize() 函数计算需要的共享内存大小。
  • TwoPhaseShmemInit() 函数初始化共享内存。

每一个 prepared 的事务对应一个 GlobalTransactionData 类型的元素,它包含事务号 xid,gid 等信息,它的详细定义如下:

typedef struct GlobalTransactionData
{
    GlobalTransaction next;		/* list link for free list */
    int			pgprocno;		/* ID of associated dummy PGPROC */
    BackendId	dummyBackendId; /* similar to backend id for backends */
    TimestampTz prepared_at;	/* time of preparation */

    /*
     * Note that we need to keep track of two LSNs for each GXACT. We keep
     * track of the start LSN because this is the address we must use to read
     * state data back from WAL when committing a prepared GXACT. We keep
     * track of the end LSN because that is the LSN we need to wait for prior
     * to commit.
     */
    XLogRecPtr	prepare_start_lsn;	/* XLOG offset of prepare record start */
    XLogRecPtr	prepare_end_lsn;	/* XLOG offset of prepare record end */
    TransactionId xid;			/* The GXACT id */

    Oid			owner;			/* ID of user that executed the xact */
    BackendId	locking_backend;	/* backend currently working on the xact */
    bool		valid;			/* true if PGPROC entry is in proc array */
    bool		ondisk;			/* true if prepare state file is on disk */
    bool		inredo;			/* true if entry was added via xlog_redo */
    char		gid[GIDSIZE];	/* The GID assigned to the prepared xact */
}			GlobalTransactionData;

TwoPhaseStateData 结构相当于是申请的共享内存的头,在共享内存内部会通过链表把 GlobalTransactionData 组织起来,通过 TwoPhaseStateData.freeGXacts 快速找到一个可用的 GlobalTransactionData 元素。TwoPhaseStateData 结构的详细定义如下:

typedef struct TwoPhaseStateData
{
    /* Head of linked list of free GlobalTransactionData structs */
    GlobalTransaction freeGXacts;

    /* Number of valid prepXacts entries. */
    int			numPrepXacts;

    /* There are max_prepared_xacts items in this array */
    GlobalTransaction prepXacts[FLEXIBLE_ARRAY_MEMBER];
} TwoPhaseStateData;

GlobalTransactionData.pgprocno 成员与 PreparedXactProcs.pgprocno 对应,PreparedXactProcs 在 InitProcGlobal() 函数中初始化,初始化 ProcGlobal->allProcs 的时候,为 2PC 事务预留了 max_prepared_xacts 个数量的 PGPROC 元素。因此可以理解为每个 prepared 的 2PC 事务,它都有一个对应的 backend 结构 PGPROC 与之对应,直到该 prepared 的事务提交或者回滚才会释放 PGPROC。max_prepared_xacts 参数不能动态修改,需要重启才能生效。

在计算 PGPROC 总数时,将 max_prepared_xacts 计算在内,如下:

uint32		TotalProcs = MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts;

PreparedXactProcs 指向为 2PC 预留的 PGPROC 元素,如下:

PreparedXactProcs = &procs[MaxBackends + NUM_AUXILIARY_PROCS];

3. 两阶段事务的持久化

prepared 的事务会在两个地方进行持久化,第一个是 wal 日志,第二个是 pg_twophase 目录,每个 xid 对应一个文件。

对于 prepared 的事务,应对崩溃恢复的场景同样是写入 wal 日志。在 prepare transaction 时写 wal 日志,记录 gxact->prepare_start_lsn 和 gxact->prepare_end_lsn,崩溃恢复时能够从 wal 日志里面恢复 2PC 事务相关的信息。通过 pg_waldump 可以解析 wal 日志,看到 2PC 事务相关的 wal record 信息。

在执行 checkpoint 之后,2PC 事务信息会被写入 pg_twophase 目录下的文件,事务最终提交或者回滚,pg_twophase 目录中的信息将被清除。pg_twophase 目录下的文件格式为 "pg_twophase/xid",xid 为 16 进制格式,每一个事务号对应一个文件,文件末尾有 crc32 校验。具体的文件格式如下:

  • xl_xact_prepare,别名 TwoPhaseFileHeader
  • gid,字符串,比如 T100,长度由 hdr.gidlen 记录
  • subxids,数组,大小由 hdr.nsubxacts 记录
  • RelFileNode 数组,表示事务提交要删除的文件,大小由 hdr->ncommitrels 记录
  • RelFileNode 数组,表示事务回滚要删除的文件,大小由 hdr->nabortrels 记录
  • SharedInvalidationMessage 数组,大小由 hdr->ninvalmsgs 记录
  • 其他,详见函数 StartPrepare()

4. 两阶段事务相关函数

  • restoreTwoPhaseData(),恢复二阶段事务信息,从 pg_twophase 目录中遍历所有文件,将文件中的内容加载到共享内存中。
  • CheckPointTwoPhase(),二阶段事务 checkpoint 逻辑,遍历 TwoPhaseState->prepXacts 数组,从 wal 日志里面读取 gxact 对应的 wal 记录,将记录写到 pg_twophase 目录下的文件中。RecreateTwoPhaseFile() 函数用于将二阶段事务信息写入文件中。执行完成后,gxact->ondisk 设置为 true。
  • XlogReadTwoPhaseData() 从 wal 日志里读取指定 lsn 开始的记录,该记录必须是一个有效 2PC 的 wal 记录。

2PC 事务 checkpoint 操作函数调用关系如下:

CreateCheckPoint
    CheckPointGuts
        CheckPointTwoPhase
            XlogReadTwoPhaseData    //从wal日志中读取2PC事务数据
            RecreateTwoPhaseFile    //将2PC事务数据写入pg_twophase目录下的文件中

5. 两阶段事务相关 GUC 参数

max_prepared_transactions,默认值为 0,表示禁用 2PC 事务,取值范围 0 ~ 0x3FFFF,不能动态修改。

文章评论

0条评论