# 海山数据库(He3DB)源码详解:两阶段提交(2PC)事务块内核函数执行过程
本文介绍了两阶段提交(2PC)事务执行过程中,主要讲解事务的prepare阶段和commit阶段的内核函数执行过程。
前言
两阶段提交(2PC)是一种用于分布式系统的事务处理协议,旨在确保事务的原子性。以下是2PC从事务准备到提交或放弃的执行流程图:
流程说明:
- 开始:事务发起者(协调者)开始一个新的分布式事务。
- 事务发起者请求所有参与者准备:协调者向所有参与者发送准备请求,询问它们是否准备好提交事务。
- 所有参与者是否准备好?:这是一个决策点,需要等待所有参与者的响应。
- 是:如果所有参与者都准备好了,协调者将发送提交请求。
- 否:如果任何一个参与者没有准备好,协调者将发送放弃请求。
- 事务发起者发送提交请求:协调者向所有参与者发送提交事务的请求。
- 所有参与者是否同意提交?:这是另一个决策点,需要等待所有参与者的响应。
- 是:如果所有参与者都同意提交,它们将执行提交操作。
- 否:如果任何一个参与者拒绝提交,所有参与者将放弃事务。
- 所有参与者提交事务:参与者执行实际的提交操作,使事务永久生效。
- 所有参与者放弃事务:参与者执行回滚操作,撤销所有事务相关的更改。
- 事务提交完成:事务要么成功提交,要么被放弃,分布式事务到此结束。
- 结束:2PC过程完成。
处理TRANS_STMT_PREPARE
当终端输入PREPARE TRANSACTION xxx语句时,这时一个会发起2PC的事务提交准备过程。处理过程首先会调用PrepareTransactionBlock()函数,并根据返回值判断是否时错误结束发生回滚。
PrepareTransactionBlock()函数执行过程:
- 首先,该函数会调用EndTransactionBlock()函数,获得一个事务结束的返回值;
TransactionState s;
bool result;
/* Set up to commit the current transaction */
result = EndTransactionBlock(false);
- 然后,判断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()函数执行过程
- 调用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)));
- 调用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)));
- 检查全局
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(两阶段提交)之间的冲突主要体现在以下几个方面:
- 检查是否为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)));
- 检查当前事务块状态:
if (CurrentTransactionState->blockState != TBLOCK_DEFAULT &&
CurrentTransactionState->blockState != TBLOCK_STARTED)
elog(FATAL, "cannot prevent transaction chain");
- 将全局
int
变量MyXactFlag的与XACT_FLAGS_NEEDIMMEDIATECOMMIT做位或操作
MyXactFlags |= XACT_FLAGS_NEEDIMMEDIATECOMMIT;
FinishPreparedTransaction()函数执行过程
- 首先,申请执行过程所需要的所有变量:
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;
- 这是一个指针,指向共享无效消息,用于在事务处理过程中通知其他节点或进程某些数据已失效。
- 验证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,在同一时间内只有一个后端可以进行提交操作。
- 检查gxact->ondisk,如果成立就从磁盘上读取2PC状态数据,如果不成立就从Xlog日志中读取2PC状态数据。
if (gxact->ondisk)
buf = ReadTwoPhaseFile(xid, false);
else
XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, NULL);
- 从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));
- 获取主xact及其子项中的最新XID,并关闭中断。
latestXid = TransactionIdLatest(xid, hdr->nsubxacts, children);
HOLD_INTERRUPTS();
- 判断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,表示该事务不在后端进程中。
- 判断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
函数的具体步骤通常包括:- 从共享缓冲池中移除所有相关的数据页。
- 关闭所有
SMgrRelation
对象的所有分支。 - 注册 SMgrRelation 对象与物理文件的无效映射信息,并告知其他进程。
- 删除所有
SMgrRelation
对象的所有分支所对应的物理文件。
- 根据isCommit的值,调用
pgstat_execute_transactional_drops
函数执行计划内的删除,主要是一些被标记为删除的对象(如表、索引、视图等)能够从实际统计信息中移除,具体来说,pgstat_execute_transactional_drops
会执行以下操作:- 在事务提交时,它会删除那些在事务中被删除的对象的统计信息。
- 在事务回滚时,它会撤销对统计信息的任何修改,因为事务中的更改没有实际发生。
- 这个函数确保了统计信息的一致性,避免了因为事务中的删除操作而导致的统计信息不一致的问题。
- 当事务提交时,确保所有相关的缓存和共享数据结构都被正确地更新或失效,以反映事务中所做的更改。
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)来传递,这个队列被所有数据库进程共享。当一个事务提交时,它会发送这些失效消息到其他后端进程,以便它们可以更新自己的缓存。这种机制确保了在高并发环境下,不同事务对同一数据的修改不会导致缓存不一致的问题。
- 加锁处理事务提交或者放弃阶段的回调函数过程,释放事务所持有的谓词锁以及清理全局事务的共享内存。
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
回调函数可能执行以下类型的操作:- 数据修改:将事务中对数据的修改应用到数据库中,或者在回滚时撤销这些修改。
- 锁处理:在事务提交时释放锁,或者在回滚时保持锁的状态。
- 存储管理器操作:处理与物理存储相关的操作,如文件的创建、删除等。
- 系统目录更新:更新系统目录信息,以反映事务对数据库结构的更改。
PredicateLockTwoPhaseFinish
处理和释放一个准备好的事务在提交或中止时持有的谓词锁,无论是成功提交还是由于某种原因中止,都需要确保之前设置的谓词锁被释放,以避免死锁和其他事务的不一致性问题。RemoveGXact
会处理共享内存中的全局事务,注意:这里的全局事务必须以及从Proc Array中移除了。
- 更新和清理事务的统计信息,处理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的提交过程类似,所以这里不做过多的讲解。