海山数据库(He3DB)源码解读:2PC

# 海山数据库(He3DB)源码详解:两阶段提交(2PC)事务块内核函数执行过程

本文介绍了两阶段提交(2PC)事务执行过程中,主要讲解事务的prepare阶段和commit阶段的内核函数执行过程。

前言

两阶段提交(2PC)是一种用于分布式系统的事务处理协议,旨在确保事务的原子性。以下是2PC从事务准备到提交或放弃的执行流程图:

在这里插入图片描述

流程说明:

  1. 开始:事务发起者(协调者)开始一个新的分布式事务。
  2. 事务发起者请求所有参与者准备:协调者向所有参与者发送准备请求,询问它们是否准备好提交事务。
  3. 所有参与者是否准备好?:这是一个决策点,需要等待所有参与者的响应。
    • :如果所有参与者都准备好了,协调者将发送提交请求。
    • :如果任何一个参与者没有准备好,协调者将发送放弃请求。
  4. 事务发起者发送提交请求:协调者向所有参与者发送提交事务的请求。
  5. 所有参与者是否同意提交?:这是另一个决策点,需要等待所有参与者的响应。
    • :如果所有参与者都同意提交,它们将执行提交操作。
    • :如果任何一个参与者拒绝提交,所有参与者将放弃事务。
  6. 所有参与者提交事务:参与者执行实际的提交操作,使事务永久生效。
  7. 所有参与者放弃事务:参与者执行回滚操作,撤销所有事务相关的更改。
  8. 事务提交完成:事务要么成功提交,要么被放弃,分布式事务到此结束。
  9. 结束:2PC过程完成。

处理TRANS_STMT_PREPARE

当终端输入PREPARE TRANSACTION xxx语句时,这时一个会发起2PC的事务提交准备过程。处理过程首先会调用PrepareTransactionBlock()函数,并根据返回值判断是否时错误结束发生回滚。

PrepareTransactionBlock()函数执行过程:

  1. 首先,该函数会调用EndTransactionBlock()函数,获得一个事务结束的返回值;
    TransactionState s;
	bool		result;

	/* Set up to commit the current transaction */
	result = EndTransactionBlock(false);
  1. 然后,判断result的值,如果为true,修改其他block的状态;如果为false,表示事务出错,准备进行回滚;
	if (result)
	{
		s = CurrentTransactionState;

		while (s->parent != NULL)
			s = s->parent;

		if (s->blockState == TBLOCK_END)
		{
			/* Save GID where PrepareTransaction can find it again */
			prepareGID = MemoryContextStrdup(TopTransactionContext, gid);

			s->blockState = TBLOCK_PREPARE;
		}
		else
		{
			/*
			 * ignore case where we are not in a transaction;
			 * EndTransactionBlock already issued a warning.
			 */
			Assert(s->blockState == TBLOCK_STARTED ||
				   s->blockState == TBLOCK_IMPLICIT_INPROGRESS);
			/* Don't send back a PREPARE result tag... */
			result = false;
		}
	}
  • 首先,获得当前事务的状态结构体,并找到事务的根节点;
  • 之后,判断节点的状态是否为END,如果是,则将当前的gid深拷贝一份,交给全局静态变量prepareGID,并修改事务块的状态为TBLOCK_PREPARE;否则,将返回结构result改为false,准备进行回滚。

根据PrepareTransactionBlock()返回值对事务进行标记

如果PrepareTransactionBlock()函数的返回值为false,则会将事务标记为CMDTAG_ROLLBACK,这意味着当前的事务将被中止,并且事务所作的所有更新都会被丢弃,回到事务开始之前的状态。调用SetQueryCompletion()函数对传入参数qc进行修改,完成事务的标记。

处理TRANS_STMT_COMMIT_PREPARED

当终端输入COMMIT PREPARED xxx语句时,这时会发起一个2PC的事务提交过程。处理过程首先会调用PreventInTransactionBlock()函数,检查当前分布式事务和事务块的状态。然后调用FinishPreparedTransaction()函数,执行分布式事务提交操作。

PreventInTransactionBlock()函数执行过程

  1. 调用IsTransactionBlock()检查全局结构体变量CurrentTransactionState中的blockState成员值,如果blockState等于TBLOCK_DEFAULT或者TBLOCK_STARTED,表示不存在事务块,返回false,其他返回true:
	if (IsTransactionBlock())
		ereport(ERROR,
				(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
		/* translator: %s represents an SQL statement name */
				 errmsg("%s cannot run inside a transaction block",
						stmtType)));
  1. 调用IsSubTransaction()检查全局结构体变量CurrentTransactionState中的nestingLevel成员值,如果≥2表示有子事务嵌套,返回true,否则返回false:
    if (IsSubTransaction())
		ereport(ERROR,
				(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
		/* translator: %s represents an SQL statement name */
				 errmsg("%s cannot run inside a subtransaction",
						stmtType)));
  1. 检查全局int变量MyXactFlag的值,如果等于XACT_FLAGS_PIPELINING,表示事务处于管道模式。
	if (MyXactFlags & XACT_FLAGS_PIPELINING)
		ereport(ERROR,
				(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
		/* translator: %s represents an SQL statement name */
				 errmsg("%s cannot be executed within a pipeline",
						stmtType)));
  • 补充: 管道模式和2PC(两阶段提交)之间的冲突主要体现在以下几个方面:
  1. 检查是否为TopLevel:
	if (!isTopLevel)
		ereport(ERROR,
				(errcode(ERRCODE_ACTIVE_SQL_TRANSACTION),
		/* translator: %s represents an SQL statement name */
				 errmsg("%s cannot be executed from a function", stmtType)));
  1. 检查当前事务块状态:
	if (CurrentTransactionState->blockState != TBLOCK_DEFAULT &&
		CurrentTransactionState->blockState != TBLOCK_STARTED)
		elog(FATAL, "cannot prevent transaction chain");
  1. 将全局int变量MyXactFlag的与XACT_FLAGS_NEEDIMMEDIATECOMMIT做位或操作
    MyXactFlags |= XACT_FLAGS_NEEDIMMEDIATECOMMIT;

FinishPreparedTransaction()函数执行过程

  1. 首先,申请执行过程所需要的所有变量:
	GlobalTransaction gxact;
	PGPROC	   *proc;
	TransactionId xid;
	char	   *buf;
	char	   *bufptr;
	TwoPhaseFileHeader *hdr;
	TransactionId latestXid;
	TransactionId *children;
	RelFileNode *commitrels;
	RelFileNode *abortrels;
	RelFileNode *delrels;
	int			ndelrels;
	xl_xact_stats_item *commitstats;
	xl_xact_stats_item *abortstats;
	SharedInvalidationMessage *invalmsgs;
  • GlobalTransaction gxact;

    • 这是一个结构体,用于表示全局事务数据的类型,主要跟踪prepared transactions(即那些已经被准备好但尚未提交的事务)。
  • PGPROC *proc;

    • PGPROC 是 PostgreSQL 中的一个结构体,代表一个后端进程。这个指针用于指向当前事务的后端进程结构。
  • TransactionId xid;

    • TransactionId 是 PostgreSQL 中用于唯一标识一个事务的类型。这个变量存储了当前事务的 ID。
  • char *buf;

    • 这是一个字符指针,用于存储事务日志记录的缓冲区。
  • char *bufptr;

    • 这是另一个字符指针,用作 buf 的游标或指针,用于在缓冲区中移动和处理数据。
  • TwoPhaseFileHeader *hdr;

    • TwoPhaseFileHeader 是一个结构体,用于存储两阶段提交协议的文件头信息。这个指针可能指向一个这样的结构体实例,包含有关两阶段提交的元数据。
  • TransactionId latestXid;

    • 这个变量存储了最新的事务 ID,用于跟踪事务日志中的最新事务。
  • TransactionId *children;

    • 这是一个指向 TransactionId 的指针数组,用于存储当前事务的子事务 ID。
  • RelFileNode *commitrels;

    • 这是一个指向 RelFileNode 的指针数组,用于存储需要在事务提交时处理的关联关系文件节点。
  • RelFileNode *abortrels;

    • 类似于 commitrels,这个指针数组用于存储需要在事务中止时处理的关联关系文件节点。
  • RelFileNode *delrels;

    • 这个指针数组用于存储需要删除的关联关系文件节点。
  • int ndelrels;

    • 这个整数变量表示 delrels 数组中的元素数量。
  • xl_xact_stats_item *commitstats;

    • 这是一个指针,指向存储提交事务统计信息的结构体。
  • xl_xact_stats_item *abortstats;

    • 类似于 commitstats,这个指针指向存储中止事务统计信息的结构体。
  • SharedInvalidationMessage *invalmsgs;

    • 这是一个指针,指向共享无效消息,用于在事务处理过程中通知其他节点或进程某些数据已失效。
  1. 验证GID并锁定GXACT,获取对应GXACT的后端proc,获取GXACT的xid。
	/*
	 * Validate the GID, and lock the GXACT to ensure that two backends do not
	 * try to commit the same GID at once.
	 */
	gxact = LockGXact(gid, GetUserId());
	proc = &ProcGlobal->allProcs[gxact->pgprocno];
	xid = gxact->xid;
  • 验证GID: GID 通常指的是全局事务标识符(Global Transaction Identifier)。验证 GID 意味着检查这个标识符是否有效,是否存在于系统中,以及是否对应于一个已经准备好但尚未提交的事务。这是为了确保你正在处理的是一个有效的、待提交的事务。

  • 锁定GXACT: GXACT 可能是指全局事务数据结构(Global Transaction Actor)。锁定 GXACT 是为了防止多个后端(backend)进程或线程同时尝试提交同一个 GID 对应的事务。这种锁定机制是必要的,因为在一个分布式系统中,可能同时有多个节点或进程参与事务的处理。

  • 保两个后端不会尝试一次提交相同的 GID: 这句话的意思是确保不会有两次提交操作同时对同一个 GID 进行。在分布式系统中,不同的后端可能指的是不同的数据库节点、不同的事务管理器或不同的服务实例。它们可能都参与了同一个全局事务的处理,但是由于网络分区、节点故障恢复或其他原因,可能会出现多个节点尝试提交同一个事务的情况。为了防止数据不一致或其他问题,需要确保对于任何一个给定的 GID,在同一时间内只有一个后端可以进行提交操作。

  1. 检查gxact->ondisk,如果成立就从磁盘上读取2PC状态数据,如果不成立就从Xlog日志中读取2PC状态数据。
	if (gxact->ondisk)
		buf = ReadTwoPhaseFile(xid, false);
	else
		XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, NULL);
  1. 从buf中读取数据并初始化申请的变量。
	hdr = (TwoPhaseFileHeader *) buf;
	Assert(TransactionIdEquals(hdr->xid, xid));
	bufptr = buf + MAXALIGN(sizeof(TwoPhaseFileHeader));
	bufptr += MAXALIGN(hdr->gidlen);
	children = (TransactionId *) bufptr;
	bufptr += MAXALIGN(hdr->nsubxacts * sizeof(TransactionId));
	commitrels = (RelFileNode *) bufptr;
	bufptr += MAXALIGN(hdr->ncommitrels * sizeof(RelFileNode));
	abortrels = (RelFileNode *) bufptr;
	bufptr += MAXALIGN(hdr->nabortrels * sizeof(RelFileNode));
	commitstats = (xl_xact_stats_item *) bufptr;
	bufptr += MAXALIGN(hdr->ncommitstats * sizeof(xl_xact_stats_item));
	abortstats = (xl_xact_stats_item *) bufptr;
	bufptr += MAXALIGN(hdr->nabortstats * sizeof(xl_xact_stats_item));
	invalmsgs = (SharedInvalidationMessage *) bufptr;
	bufptr += MAXALIGN(hdr->ninvalmsgs * sizeof(SharedInvalidationMessage));
  1. 获取主xact及其子项中的最新XID,并关闭中断。
    latestXid = TransactionIdLatest(xid, hdr->nsubxacts, children);
    HOLD_INTERRUPTS();
  1. 判断isCommit值,根据判断结果选择执行提交(commit)还是放弃(abort)。
	if (isCommit)
		RecordTransactionCommitPrepared(xid,
										hdr->nsubxacts, children,
										hdr->ncommitrels, commitrels,
										hdr->ncommitstats,
										commitstats,
										hdr->ninvalmsgs, invalmsgs,
										hdr->initfileinval, gid);
	else
		RecordTransactionAbortPrepared(xid,
									   hdr->nsubxacts, children,
									   hdr->nabortrels, abortrels,
									   hdr->nabortstats,
									   abortstats,
									   gid);
    ProcArrayRemove(proc, latestXid);
    gxact->valid = false;
  • 当isCommit为true时,会调用RecordTransactionCommitPrepared函数记录事务提交日志;
  • 当isCommit为false时,会调用RecordTransactionAbortPrepared函数记录事务放弃日志;
  • 从全局 ProcArray 中移除 PGPROC,移除 PGPROC 条目意味着该事务不再被视为活跃,TransactionIdIsInProgress函数将不再报告该事务正在进行中;
  • 将全局事务的valid成员值改为false,表示该事务不在后端进程中。
  1. 判断isCommit值,根据判断结果对变量delrels和ndelrels进行赋值,然后进行对应Relation文件的的删除,以及执行计划内的删除。
	if (isCommit)
	{
		delrels = commitrels;
		ndelrels = hdr->ncommitrels;
	}
	else
	{
		delrels = abortrels;
        ndelrels = hdr->nabortrels;
    }
    DropRelationFiles(delrels, ndelrels, false);
    
	if (isCommit)
		pgstat_execute_transactional_drops(hdr->ncommitstats, commitstats, false);
	else
		pgstat_execute_transactional_drops(hdr->nabortstats, abortstats, false);
  • 当isCommit为true时,调用commitrels和hdr的ncommitrels成员两个值对变量进行赋值,表示进行提交的节点删除;
  • 当isCommit为false时,调用abortrels和hdr的nabortrels成员两个值对变量进行赋值,表示进行放弃的节点删除;
  • DropRelationFiles函数会调用smgrdounlinkall函数,作用是将给定的所有SMgrRelation对象的所有分支(forks)从磁盘中移除,确保数据库系统中不再需要的文件被彻底清除,以释放存储空间,并保持文件系统的整洁。执行 smgrdounlinkall 函数的具体步骤通常包括:
    1. 从共享缓冲池中移除所有相关的数据页。
    2. 关闭所有 SMgrRelation 对象的所有分支。
    3. 注册 SMgrRelation 对象与物理文件的无效映射信息,并告知其他进程。
    4. 删除所有 SMgrRelation 对象的所有分支所对应的物理文件。
  • 根据isCommit的值,调用pgstat_execute_transactional_drops函数执行计划内的删除,主要是一些被标记为删除的对象(如表、索引、视图等)能够从实际统计信息中移除,具体来说,pgstat_execute_transactional_drops会执行以下操作:
    • 在事务提交时,它会删除那些在事务中被删除的对象的统计信息。
    • 在事务回滚时,它会撤销对统计信息的任何修改,因为事务中的更改没有实际发生。
    • 这个函数确保了统计信息的一致性,避免了因为事务中的删除操作而导致的统计信息不一致的问题。
  1. 当事务提交时,确保所有相关的缓存和共享数据结构都被正确地更新或失效,以反映事务中所做的更改。
	if (isCommit)
	{
		if (hdr->initfileinval)
			RelationCacheInitFilePreInvalidate();
		SendSharedInvalidMessages(invalmsgs, hdr->ninvalmsgs);
		if (hdr->initfileinval)
			RelationCacheInitFilePostInvalidate();
	}

  • 检查是否需要初始化文件失效(initfileinval)

    • hdr->initfileinval 检查是否需要进行初始化文件失效的操作。如果为真,表示需要在事务提交前处理相关的缓存失效。
  • 执行初始化文件预失效(PreInvalidate)操作

    • RelationCacheInitFilePreInvalidate() 函数被调用,以处理在发送共享失效消息(Shared Invalid Messages)之前需要进行的缓存失效操作。这通常涉及到关系缓存(relation cache)的初始化文件,确保在事务提交过程中,相关的缓存信息是最新的。
  • 发送共享失效消息

    • SendSharedInvalidMessages(invalmsgs, hdr->ninvalmsgs) 函数被调用,发送共享失效消息。这些消息通知其他数据库进程,某些数据或结构已经改变,需要更新它们的缓存。invalmsgs 是一个包含失效消息的数组,hdr->ninvalmsgs 是这个数组中的元素数量。
  • 执行初始化文件后失效(PostInvalidate)操作

    • 如果 hdr->initfileinval 为真,RelationCacheInitFilePostInvalidate() 函数被调用,以处理在发送共享失效消息之后需要进行的缓存失效操作。这确保了在事务提交后,相关的缓存信息也是最新的。

共享失效消息(Shared Invalidation Messages)是 PostgreSQL 中用于维护缓存一致性的一种机制。当数据库中的某些数据被修改时,这些修改可能会影响到其他数据库进程中的缓存数据。为了确保所有进程中的缓存数据保持一致,PostgreSQL 通过共享失效消息来通知其他进程,某些缓存数据已经不再有效,需要被刷新或更新。

这些失效消息通过一个共享内存中的队列(shmInvalBuffer)来传递,这个队列被所有数据库进程共享。当一个事务提交时,它会发送这些失效消息到其他后端进程,以便它们可以更新自己的缓存。这种机制确保了在高并发环境下,不同事务对同一数据的修改不会导致缓存不一致的问题。

  1. 加锁处理事务提交或者放弃阶段的回调函数过程,释放事务所持有的谓词锁以及清理全局事务的共享内存。
	LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);

	/* And now do the callbacks */
	if (isCommit)
		ProcessRecords(bufptr, xid, twophase_postcommit_callbacks);
	else
		ProcessRecords(bufptr, xid, twophase_postabort_callbacks);

	PredicateLockTwoPhaseFinish(xid, isCommit);

	/* Clear shared memory state */
	RemoveGXact(gxact);

	/*
	 * Release the lock as all callbacks are called and shared memory cleanup
	 * is done.
	 */
	LWLockRelease(TwoPhaseStateLock);
  • ProcessRecords 回调函数的作用是处理事务日志中记录的各种操作。这些操作可能包括数据的修改、锁的获取和释放、存储管理器(storage manager)的操作等。ProcessRecords 回调函数会被调用来遍历事务日志中的每一条记录,并执行相应的操作来提交或回滚事务。
    具体来说,ProcessRecords 回调函数可能执行以下类型的操作:
    1. 数据修改:将事务中对数据的修改应用到数据库中,或者在回滚时撤销这些修改。
    2. 锁处理:在事务提交时释放锁,或者在回滚时保持锁的状态。
    3. 存储管理器操作:处理与物理存储相关的操作,如文件的创建、删除等。
    4. 系统目录更新:更新系统目录信息,以反映事务对数据库结构的更改。
  • PredicateLockTwoPhaseFinish处理和释放一个准备好的事务在提交或中止时持有的谓词锁,无论是成功提交还是由于某种原因中止,都需要确保之前设置的谓词锁被释放,以避免死锁和其他事务的不一致性问题。
  • RemoveGXact会处理共享内存中的全局事务,注意:这里的全局事务必须以及从Proc Array中移除了。
  1. 更新和清理事务的统计信息,处理2PC文件,将该进程的全局事务指针置空,开放终端,释放内存,结束函数。
	/* Count the prepared xact as committed or aborted */
	AtEOXact_PgStat(isCommit, false);

	/* And now we can clean up any files we may have left. */
	if (gxact->ondisk)
		RemoveTwoPhaseFile(xid, true);

	MyLockedGxact = NULL;

	RESUME_INTERRUPTS();

	pfree(buf);
  • AtEOXact_PgStat主要负责更新统计信息,这些统计信息包括但不限于表和索引的访问统计、函数调用统计等,具体来说会执行以下操作:
    • 更新表和索引的统计信息,如访问次数、行数变化等。
    • 更新函数调用的统计信息,如调用次数和执行时间。
    • 清理事务中产生的临时统计信息。
    • 确保统计信息的准确性和及时性,以便查询优化器可以使用这些信息来优化查询计划。
      pgstat_execute_transactional_drops函数的区别:
    • AtEOXact_PgStat 函数更关注于更新和清理事务中的统计信息
    • pgstat_execute_transactional_drops 函数则专注于处理事务中对象的创建和删除对统计信息的影响
  • 调用RemoveTwoPhaseFile函数处理2PC文件。每个事务在进行两阶段提交时,会创建一个临时文件来记录事务的状态,直到事务最终提交或中止。这个文件被称为两阶段提交文件(2PC 文件),它包含了完成两阶段提交协议所需的信息。
  • 当前由本进程锁定的全局事务条目指针MyLockedGxact置为空;
  • 放开中断,并释放buf对应的内存空间。

处理TRANS_STMT_ROLLBACK_PREPARED

tat_execute_transactional_drops`函数的区别:**

  • AtEOXact_PgStat 函数更关注于更新和清理事务中的统计信息
  • pgstat_execute_transactional_drops 函数则专注于处理事务中对象的创建和删除对统计信息的影响
  • 调用RemoveTwoPhaseFile函数处理2PC文件。每个事务在进行两阶段提交时,会创建一个临时文件来记录事务的状态,直到事务最终提交或中止。这个文件被称为两阶段提交文件(2PC 文件),它包含了完成两阶段提交协议所需的信息。
  • 当前由本进程锁定的全局事务条目指针MyLockedGxact置为空;
  • 放开中断,并释放buf对应的内存空间。

处理TRANS_STMT_ROLLBACK_PREPARED

当终端输入ABORT PREPARED xxx语句时,这时会发起一个2PC的事务放弃过程。整体流程和2PC的提交过程类似,所以这里不做过多的讲解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值