引言
在上一篇博客MVCC(6)中,我们分析了MVCC数据可见性机制的元组可见性判断函数SetXact2CommitInProgress
,给对应XID设置相应的CSNLOG的函数CSNLogSetCommitSeqNo
本篇博客将会分析MVCC机制可见性判断的主体函数中的最后一个函数RecordTransactionCommit
。 文件路径 D:\opengauss-server\src\gausskernel\storage\access\transam\csnlog.cpp 记录事务提交,主要是写CLOG,CSNLOG以及他们的XLOG
一、名词介绍
IsPostmasterEnvironment
IsPostmasterEnvironment是一个全局变量,用于判断当前进程是否在Postmaster环境中。在PostgreSQL中,Postmaster是主进程,负责监听和接收客户端连接请求,为其派生服务进程。
GTM_FREE_MODE
GTM_FREE_MODE则是GaussDB数据库中的一个模式。GaussDB提供了两种不同的GTM模式,GTM-Lite和GTM-Free。在GTM-Free模式下,中心事务管理节点不再参与事务管理,消除了GTM单点瓶颈,可以达到更高的事务处理性能。但在一致性方面,支持所有事务运行完后,保证读的外部一致性,不支持分布式事务强一致性读,不支持依赖于查询结果的事务一致性。
Paxos协议
Paxos协议是一种基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一,其解决的问题就是在分布式系统中如何就某个值(决议)达成一致。 Paxos协议运行在允许消息重复、丢失、延迟或乱序,但没有拜占庭式错误的网络环境中,它利用“大多数 (Majority)机制”保证了 2F+1 的容错能力,即 2F+1 个节点的系统最多允许 F 个节点同时出现故障。 在Paxos协议中,有三种角色:提出者(Proposer)、接受者(Acceptor)和学习者(Learner)。提出者提交一个提案,等待大家批准为结案;接受者负责对提案进行投票,提议超过半数的接受者投票及被选中;学习者被告知提案结果,并与之统一,不参与投票过程。
二、函数介绍
RecordTransactionCommit
RecordTransactionCommit
是openGauss数据库中的一个函数,它用于记录事务的提交。这个函数包括写入Clogs(事务提交日志)、CSNlog(事务提交序列号日志)和Xlogs(事务日志)。Clogs仅记录事务执行结果,CSNlogs记录提交日志的顺序以确定可见性,Xlogs是用于数据恢复和持久性的重做日志。这个函数是openGauss事务模块实现数据库事务基本属性的一部分,确保用户数据不丢失,正确修改,并正确查询。在并发执行事务时,事务并发控制机制用于确保openGauss的ACID属性。总的来说,RecordTransactionCommit
函数在openGauss中起着关键的作用,它确保了数据库事务的正确性和一致性。
这个函数涉及到Clogs(事务提交日志)、CSNlog(事务提交序列号日志)和Xlogs(事务日志)。下面是对这三个概念的简介:
Clogs
- Clogs(事务提交日志):事务提交日志(CLOG)记录所有事务的状态,是并发控制机制的一部分。它分配给共享内存并在整个事务处理过程中使用。如果系统发生故障,您将需要该日志将数据库恢复到一致状态。
CSNlogs
- CSNlog(事务提交序列号日志):Commit Sequence Number (CSN) 是 Oracle GoldenGate 构造的标识符,用于标识事务,以维护事务一致性和数据完整性。它唯一地标识了事务提交到数据库的时间点。任何两个 CSN 数字的比较,每个数字都绑定到同一日志流中的事务提交记录,可靠地指示了两个事务完成的顺序。
Xlogs
- Xlogs(事务日志):每个数据库都有一个记录所有事务和每个事务所做的数据库修改的事务日志。如果系统发生故障,您将需要该日志将数据库恢复到一致状态。
这些日志在数据库管理中起着至关重要的作用,确保数据的一致性和完整性。
相关结构体、预定义
文件路径:opengauss-server\src\include\access\clog.h
#define CLOG_XID_STATUS_IN_PROGRESS 0x00
#define CLOG_XID_STATUS_COMMITTED 0x01
#define CLOG_XID_STATUS_ABORTED 0x02
#define CLOG_XID_STATUS_SUB_COMMITTED 0x03
这里定义的宏分别表示了CLOG四种状态:分别表示在进程中,已经提交,禁止,子事务提交。这样定义状态的好处是只需要两个bit就可以存储事务状态
#define TransactionIdToPage(xid) ((xid) / (TransactionId)CLOG_XACTS_PER_PAGE)
#define TransactionIdToPgIndex(xid) ((xid) % (TransactionId)CLOG_XACTS_PER_PAGE)
#define TransactionIdToByte(xid) (TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE)
#define TransactionIdToBIndex(xid) ((xid) % (TransactionId)CLOG_XACTS_PER_BYTE)
这里的宏定义分别给出了XID是如何映射到文件上的,是一种典型的哈希表的应用,对于事务定位效率有着极高的提升。
- Xid到页面位置
- 页面上的偏移量
- 页面上第几个字节
- 字节上哪一个bit
#define CLOG_BITS_PER_XACT 2
#define CLOG_XACTS_PER_BYTE 4
#define CLOG_XACTS_PER_PAGE (BLCKSZ * CLOG_XACTS_PER_BYTE)
#define CLOG_XACT_BITMASK ((1 << CLOG_BITS_PER_XACT) - 1)
此处BLCKSZ默认值为8k,即8192
三、代码分析
我们还是同以前一样,先对函数做简单的代码块划分,然后再对每个模块进行详细分析
我们将会对其上划分的的变量初始化简单分析,事务主体处理部分逐一分析。事务主体部分涵盖了大量的预定义处理,对于全局数据库,内存引擎进行了仔细的检查,但是不是本文分析的主体,且笔者能力有限,为了防止本篇博客过于冗杂,所以将会跳过这些预定义处理,默认为#else下的情况进行分析
变量初始化
TransactionId xid = GetTopTransactionIdIfAny();
bool markXidCommitted = TransactionIdIsValid(xid);
TransactionId latestXid = InvalidTransactionId;
int nrels;
int temp_nrels = 0;
ColFileNodeRel *rels = NULL;
int nchildren;
TransactionId *children = NULL;
int nmsgs = 0;
SharedInvalidationMessage *invalMessages = NULL;
int nlibrary = 0;
char *library_name = NULL;
int library_length = 0;
bool RelcacheInitFileInval = false;
bool wrote_xlog = false;
bool isExecCN = (IS_PGXC_COORDINATOR && !IsConnFromCoord());
XLogRecPtr globalDelayDDLLSN = InvalidXLogRecPtr;
XLogRecPtr commitRecLSN = InvalidXLogRecPtr;
nrels = smgrGetPendingDeletes(true, &rels, false, &temp_nrels);
nchildren = xactGetCommittedChildren(&children);
将上面的变量做一个简单的概括如下图所示:
可以看到除了简单的赋值初始化变量,简单的宏函数调用以外,变量初始化还用到了两个函数: xactGetCommittedChildren()–将失效消息添加到提交记录中。 返回值由nchildren接收,是失效的子事务数量
smgrGetPendingDeletes() – 获取待删除的非临时关系列表。 返回值由nrels接受,是预定的删除事务数量, 通过传入实参,rels被设置为指向一个新分配的RelFileNodes数组,temp_nrels被设置为临时关系的数量
事务处理主体
检查是否有提交XID标记
if (!markXidCommitted) {
if (nrels != 0)
ereport(ERROR, (errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot commit a transaction that deleted files but has no xid")));
Assert(nchildren == 0);
#ifdef ENABLE_MOT
CallXactCallbacks(XACT_EVENT_RECORD_COMMIT);
if (t_thrd.xlog_cxt.XactLastRecEnd != 0) {
wrote_xlog = true;
}
#endif
if (!wrote_xlog) {
goto cleanup;
}
}
检查是否有提交XID标记,对于没有标记提交XID,但是nrels!=0的的情况进行报错,因为不能提交一项没有XID却还要删除文件的事项。如果提交了,将会对数据库文件恢复造成困难,事务回滚操作找不到对应的 xid
如果没有定义内存引擎的使用,此时将会直接跳转到函数结尾——clean up,而此时的reles指针若为NULL(),否则会释放它所指向的文件
else {
if (TransactionIdDidAbort(xid))
ereport(PANIC, (errcode(ERRCODE_INVALID_TRANSACTION_STATE),
errmsg("cannot commit transaction %lu, it was already aborted", xid)));
BufmgrCommit();
START_CRIT_SECTION();
t_thrd.pgxact->delayChkpt = true;
if (useLocalXid || !IsPostmasterEnvironment || GTM_FREE_MODE) {
#ifndef ENABLE_MULTIPLE_NODES
CommitSeqNo csn = SetXact2CommitInProgress(xid, 0);
XLogInsertStandbyCSNCommitting(xid, csn, children, nchildren);
#else
SetXact2CommitInProgress(xid, 0);
#endif
setCommitCsn(getLocalNextCSN());
} else {
if (TransactionIdIsNormal(xid) &&
(!(useLocalXid || !IsPostmasterEnvironment || GTM_FREE_MODE || GetForceXidFromGTM())) &&
(GetCommitCsn() == 0)) {
CommitSeqNo csn = SetXact2CommitInProgress(xid, 0);
XLogInsertStandbyCSNCommitting(xid, csn, children, nchildren);
END_CRIT_SECTION();
PG_TRY();
{
ereport(LOG, (errmsg("Set a new csn from gtm for auto commit transactions.")));
setCommitCsn(CommitCSNGTM(true));
}
PG_CATCH();
{
t_thrd.pgxact->delayChkpt = false;
PG_RE_THROW();
}
PG_END_TRY();
START_CRIT_SECTION();
}
}
#ifdef ENABLE_MOT
CallXactCallbacks(XACT_EVENT_RECORD_COMMIT);
#endif
UpdateNextMaxKnownCSN(GetCommitCsn());
SetCurrentTransactionStopTimestamp();
#ifdef ENABLE_MULTIPLE_NODES
bool hasOrigin = false;
#else
bool hasOrigin = u_sess->reporigin_cxt.originId != InvalidRepOriginId &&
u_sess->reporigin_cxt.originId != DoNotReplicateId;
#endif
对于没有标记XID提交的,先对事务ID是否已经取消,不能对取消的事务进行操作。 然后调用BufmgrCommit()为提交XLOG做准备,这里是一个空函数,所有对于bufmgr层面的提交事务都需要调用这个空函数,给系统确认的时间。
然后将自己标记为处于提交关键区域内这强制任何并发的检查点等待,直到我们更新pg_clog。如果没有这个,检查点可能会在XLOG记录之后设置REDO,但未能将pg_clog更新刷新到磁盘,如果系统稍后崩溃,可能会导致事务提交丢失。注意:我们可以,但不需要在 RecordTransactionAbort中设置此标志。这是因为丢失一个事务中止是非关键的;假设它反正已经中止了。 在不持有ProcArrayLock的情况下,更改我们自己后台的delayChkpt标志是安全的, 因为我们是唯一修改它的人。 这使得检查点确定哪些xacts是delayChkpt有点模糊, 但这并不重要。
事务环境检查
if (nrels > 0 || nmsgs > 0 || RelcacheInitFileInval || t_thrd.xact_cxt.forceSyncCommit ||
XLogLogicalInfoActive() || hasOrigin) {
xl_xact_commit xlrec;
xl_xact_origin origin;
xlrec.xinfo = 0;
if (RelcacheInitFileInval)
xlrec.xinfo |= XACT_COMPLETION_UPDATE_RELCACHE_FILE;
if (t_thrd.xact_cxt.forceSyncCommit)
xlrec.xinfo |= XACT_COMPLETION_FORCE_SYNC_COMMIT;
#ifdef ENABLE_MOT
if (IsMOTEngineUsed() || IsMixedEngineUsed()) {
xlrec.xinfo |= XACT_MOT_ENGINE_USED;
}
#endif
if (hasOrigin) {
xlrec.xinfo |= XACT_HAS_ORIGIN;
}
xlrec.dbId = u_sess->proc_cxt.MyDatabaseId;
xlrec.tsId = u_sess->proc_cxt.MyDatabaseTableSpace;
xlrec.csn = GetCommitCsn();
#ifdef PGXC
xlrec.xact_time = t_thrd.xact_cxt.xactStopTimestamp + t_thrd.xact_cxt.GTMdeltaTimestamp;
#else
xlrec.xact_time = t_thrd.xact_cxt.xactStopTimestamp;
#endif
xlrec.nrels = nrels;
xlrec.nsubxacts = nchildren;
xlrec.nmsgs = nmsgs;
xlrec.nlibrary = nlibrary;
XLogBeginInsert();
XLogRegisterData((char *)(&xlrec), MinSizeOfXactCommit);
if (nrels > 0) {
XLogRegisterData((char *)rels, nrels * sizeof(ColFileNodeRel));
(void)LWLockAcquire(DelayDDLLock, LW_SHARED);
}
if (nchildren > 0) {
XLogRegisterData((char *)children, nchildren * sizeof(TransactionId));
}
if (nmsgs > 0) {
XLogRegisterData((char *)invalMessages, nmsgs * sizeof(SharedInvalidationMessage));
}
#ifndef ENABLE_MULTIPLE_NODES
XLogRegisterData((char *) &u_sess->utils_cxt.RecentXmin, sizeof(TransactionId));
#endif
if (nlibrary > 0) {
XLogRegisterData((char *)library_name, library_length);
}
if (hasOrigin) {
origin.origin_lsn = u_sess->reporigin_cxt.originLsn;
origin.origin_timestamp = u_sess->reporigin_cxt.originTs;
XLogRegisterData((char*)&origin, sizeof(xl_xact_origin));
}
XLogIncludeOrigin();
commitRecLSN = XLogInsert(RM_XACT_ID, XLOG_XACT_COMMIT);
if (nrels > 0) {
globalDelayDDLLSN = GetDDLDelayStartPtr();
if (!XLogRecPtrIsInvalid(globalDelayDDLLSN) && XLByteLT(globalDelayDDLLSN, commitRecLSN))
t_thrd.xact_cxt.xactDelayDDL = true;
else
t_thrd.xact_cxt.xactDelayDDL = false;
LWLockRelease(DelayDDLLock);
}
if (hasOrigin) {
replorigin_session_advance(u_sess->reporigin_cxt.originLsn, t_thrd.xlog_cxt.XactLastRecEnd);
}
} else {
xl_xact_commit_compact xlrec;
xlrec.xact_time = t_thrd.xact_cxt.xactStopTimestamp;
xlrec.csn = GetCommitCsn();
xlrec.nsubxacts = nchildren;
XLogBeginInsert();
XLogRegisterData((char *)(&xlrec), MinSizeOfXactCommitCompact);
if (nchildren > 0) {
XLogRegisterData((char *)children, nchildren * sizeof(TransactionId));
}
#ifndef ENABLE_MULTIPLE_NODES
XLogRegisterData((char *) &u_sess->utils_cxt.RecentXmin, sizeof(TransactionId));
#endif
XLogIncludeOrigin();
(void)XLogInsert(RM_XACT_ID, XLOG_XACT_COMMIT_COMPACT);
}
}
这段代码涉及到数据库系统中的事务处理、日志记录、缓存管理等多个方面,在这一部分首先对如下关系进行检查
变量名 | 含义 |
---|---|
nrels | 需要处理的关系 |
nmsgs | 无效消息 |
RelcacheInitFileInval | 关系缓存无效标志 |
t_thrd.xact_cxt.forceSyncCommi | 强制同步提交标志 |
XLogLogicalInfoActive() | 逻辑日志信息活动 |
hasOrigin | 原始事务 |
如果满足任一条件,然后它会执行以下操作:
- 初始化一个
xl_xact_commit
结构体,用于记录事务提交的信息。 - 根据不同的条件设置
xlrec.xinfo
的不同位。 - 设置数据库ID、表空间ID和提交序列号(CSN)。
- 设置事务时间戳。
- 设置关系数、子事务数、无效消息数和库数。
- 调用
XLogBeginInsert()
开始一个新的日志记录。 - 调用
XLogRegisterData()
将各种数据注册到日志记录中。 - 如果有原始事务,还会将原始事务的LSN和时间戳注册到日志记录中。
- 调用
XLogInsert()
将日志记录插入到WAL中。 - 如果有关系需要处理,还会更新全局的DDL延迟LSN,并根据需要设置事务的DDL延迟标志。
- 如果有原始事务,还会更新会话的原始LSN。
if ((wrote_xlog && u_sess->attr.attr_storage.guc_synchronous_commit > SYNCHRONOUS_COMMIT_OFF) ||
t_thrd.xact_cxt.forceSyncCommit || nrels > 0) {
if (!IsInitdb && g_instance.attr.attr_storage.dcf_attr.enable_dcf) {
SyncPaxosWaitForLSN(t_thrd.xlog_cxt.XactLastRecEnd);
} else {
XLogWaitFlush(t_thrd.xlog_cxt.XactLastRecEnd);
if (wrote_xlog && u_sess->attr.attr_storage.guc_synchronous_commit > SYNCHRONOUS_COMMIT_LOCAL_FLUSH) {
SyncRepWaitForLSN(t_thrd.xlog_cxt.XactLastRecEnd, !markXidCommitted);
g_instance.comm_cxt.localinfo_cxt.set_term = true;
}
}
if (markXidCommitted) {
t_thrd.pgxact->needToSyncXid |= SNAPSHOT_UPDATE_NEED_SYNC;
TransactionIdCommitTree(xid, nchildren, children, GetCommitCsn());
}
} else {
if (markXidCommitted) {
t_thrd.pgxact->needToSyncXid |= SNAPSHOT_UPDATE_NEED_SYNC;
TransactionIdAsyncCommitTree(xid, nchildren, children, t_thrd.xlog_cxt.XactLastRecEnd, GetCommitCsn());
}
}
它首先检查是否满足以下任一条件:写入了XLOG、设置了强制同步提交标志,或者有关系需要处理。
变量名 | 含义 |
---|---|
wrote_xlo,guc_synchronous_commit > SYNCHRONOUS_COMMIT_OFF | 写入了XLOG,设置了强制同步提交标志 |
t_thrd.xact_cxt.forceSyncCommit | 设置了强制同步提交标志 |
nrels | 有关系需要处理 |
- 如果不是在初始化数据库,并且启用了DCF(Distributed Consensus Framework),则调用
SyncPaxosWaitForLSN()
等待Paxos协议同步。 - 否则,调用
XLogWaitFlush()
等待XLOG刷新。如果写入了XLOG并且设置了同步提交标志,则调用SyncRepWaitForLSN()
等待同步复制,并设置本地信息上下文的set_term
标志。 - 如果需要标记事务ID为已提交,则设置进程的
needToSyncXid
标志,并调用TransactionIdCommitTree()
提交事务。
如果不满足上述任一条件,但需要标记事务ID为已提交,则设置进程的needToSyncXid
标志,并调用TransactionIdAsyncCommitTree()
异步提交事务。
清理工作
if (markXidCommitted) {
t_thrd.pgxact->delayChkpt = false;
END_CRIT_SECTION();
}
latestXid = TransactionIdLatest(xid, nchildren, children);
t_thrd.xlog_cxt.XactLastCommitEnd = t_thrd.xlog_cxt.XactLastRecEnd;
t_thrd.xlog_cxt.XactLastRecEnd = 0;
cleanup:
if (rels != NULL)
pfree(rels);
return latestXid;
最后进行处理数据库事务提交后的清理工作。它首先检查是否需要标记事务ID为已提交。如果需要,它会将进程的delayChkpt
标志设置为假,并结束关键区域。然后,它会计算最新的事务ID,并将最后一次提交的结束位置保存到XactLastCommitEnd
中,同时将XactLastRecEnd
重置为0。
在cleanup:
标签下,它会检查是否有关系需要处理。如果有,它会释放关系数组的内存。最后,它返回最新的事务ID。
四、小结
RecordTransactionCommit是一个非常复杂的函数,它调用的函数之多,涉及的数据库概念之综合,是我之前解读的函数所远不能及的。刚开始他让我无从下手,因为他实在是太长了,调用函数太多了,然我陷入了一个陷阱——无止境的分析其调用函数,后面我决定不求甚解,只去了解调用函数的作用,而非实现,终于走出了怪圈。诚然如此,他的逻辑却并不显得复杂,对事务的标记与否,阻塞与否,同步提交与否每一个进行了逐一判断,庞杂而不凌乱,在这里我们可以看到数据库事务提交的全而精。笔者能力有限,本次分析没有对预定义的条件分支进行详尽解读,希望以后有机会能够更精细的学习。如有错漏,烦请指正。