函数:AssignTransactionId
static void
AssignTransactionId(TransactionState s)
{
...
优先给没有事务ID的父事务分配
确保父事务有 XID,以便子事务总是拥有一个比其父事务更新的 XID。这里不能递归调用,否则如果我们处于一个巨大的子事务堆栈的底部,而这些子事务还没有分配 XID,可能会遇到栈溢出的问题。
下面虽然递归了,但永远不会超过1层。
if (isSubXact && !FullTransactionIdIsValid(s->parent->fullTransactionId))
{
TransactionState p = s->parent;
TransactionState *parents;
size_t parentOffset = 0;
parents = palloc(sizeof(TransactionState) * s->nestingLevel);
while (p != NULL && !FullTransactionIdIsValid(p->fullTransactionId))
{
parents[parentOffset++] = p;
p = p->parent;
}
while (parentOffset != 0)
AssignTransactionId(parents[--parentOffset]);
pfree(parents);
}
当 wal_level=logical 时,确保只有在其顶级事务的 xid 已经被记录到 WAL 中之后,子事务的 xid 才能在 WAL 流中被看到。如果必要,会记录一个包含少于PGPROC_MAX_CACHED_SUBXIDS(64个) xact_assignment 记录。
请注意,即使一个事务的didLogXid 没有被设置,而它出现在 WAL 记录中,这也是可以的,我们可能会多余地记录一些内容。当一个 xid 被包含在 wal 记录的内部某处,但不在 XLogRecord->xl_xid 中时,就会发生这种情况,如在 xl_standby_locks 中。
if (isSubXact && XLogLogicalInfoActive() &&
!TopTransactionStateData.didLogXid)
log_unknown_top = true;
生成一个新的 FullTransactionId 并在 PGPROC 和 pg_subtrans 中记录其 xid。
注意:必须在 Xid 出现在 PGPROC 之外的任何共享存储中之前,先创建 subtrans 条目;因为如果 PGPROC 中没有空间容纳它,subtrans 条目是必需的,以确保其他后端将 Xid 视为“正在运行”。参见 GetNewTransactionId。
s->fullTransactionId = GetNewTransactionId(isSubXact);
if (!isSubXact)
XactTopFullTransactionId = s->fullTransactionId;
if (isSubXact)
SubTransSetParent(XidFromFullTransactionId(s->fullTransactionId),
XidFromFullTransactionId(s->parent->fullTransactionId));
...
currentOwner = CurrentResourceOwner;
CurrentResourceOwner = s->curTransactionOwner;
加xid锁,实现mvcc的功能(事务之间修改同一行数据时用到这个);resowner必须用事务的。
XactLockTableInsert(XidFromFullTransactionId(s->fullTransactionId));
CurrentResourceOwner = currentOwner;
在每个顶级事务中,每分配 PGPROC_MAX_CACHED_SUBXIDS(64)个事务 id,就为分配发出一个 WAL 记录。
这里看最后一章的分析:
if (isSubXact && XLogStandbyInfoActive())
{
unreportedXids[nUnreportedXids] = XidFromFullTransactionId(s->fullTransactionId);
nUnreportedXids++;
...
if (nUnreportedXids >= PGPROC_MAX_CACHED_SUBXIDS ||
log_unknown_top)
{
xl_xact_assignment xlrec;
xlrec.xtop = GetTopTransactionId();
Assert(TransactionIdIsValid(xlrec.xtop));
xlrec.nsubxacts = nUnreportedXids;
XLogBeginInsert();
XLogRegisterData((char *) &xlrec, MinSizeOfXactAssignment);
XLogRegisterData((char *) unreportedXids,
nUnreportedXids * sizeof(TransactionId));
(void) XLogInsert(RM_XACT_ID, XLOG_XACT_ASSIGNMENT);
nUnreportedXids = 0;
TopTransactionStateData.didLogXid = true;
}
}
}
函数:GetNewTransactionId
32位事务ID:
uint32
0 1 2 3 4 5 ...... 4294967295 →(回卷)
0 1 2 3 4 5 ...... 4294967295 →(回卷)
0 1 2 3 4 5 ...... 4294967295 →(回卷)
0 1 2 3 4 5 ...... 4294967295 →(回卷)
0 1 2 3 4 5 ...... 4294967295 →(回卷)
fullxid使用64b,分成两个32b来使用:
uint64
[epoch 32bit][xid 32bit]
[0][0] [0][1] [0][2] [0][3] ..... [0][4294967295] →(回卷,不在溢出,只需要把epoch加一)
[1][0] [1][1] [1][2] [1][3] ..... [1][4294967295] →(回卷,不在溢出,只需要把epoch加一)
[2][0] [2][1] [2][2] [2][3] ..... [2][4294967295] →(回卷,不在溢出,只需要把epoch加一)
但是,在目前版本中(PG15),落盘的表和XLOG里面还是记录的32位XLOG,主要是兼容性问题不好解决。
typedef uint32 TransactionId;
typedef struct XLogRecord
{
uint32 xl_tot_len; /* total len of entire record */
TransactionId xl_xid; /* xact id */
XLogRecPtr xl_prev; /* ptr to previous record in log */
uint8 xl_info; /* flag bits, see below */
RmgrId xl_rmid; /* resource manager for this record */
/* 2 bytes of padding here, initialize to zero */
pg_crc32c xl_crc; /* CRC for this record */
/* XLogRecordBlockHeaders and XLogRecordDataHeader follow, no padding */
} XLogRecord;
函数:SubTransSetParent
《Postgresql源码(25)子事务可见性判断和性能问题》
结论:
分析
在备机redo时,ProcArrayApplyXidAssignment负责处理收到的xids日志。
xact_redo
...
else if (info == XLOG_XACT_ASSIGNMENT)
{
xl_xact_assignment *xlrec = (xl_xact_assignment *) XLogRecGetData(record);
if (standbyState >= STANDBY_INITIALIZED)
ProcArrayApplyXidAssignment(xlrec->xtop,
xlrec->nsubxacts, xlrec->xsub);
}
包含了两部分:
ProcArrayApplyXidAssignment函数负责处理:
ProcArrayApplyXidAssignment(TransactionId topxid,
int nsubxids, TransactionId *subxids)
{
...
max_xid = TransactionIdLatest(topxid, nsubxids, subxids);
最大的子事务ID记一下就好啦,其他的备库能推测出来。
RecordKnownAssignedTransactionIds(max_xid);
注意这里,主库的标记逻辑是
SubTransSetParent(XidFromFullTransactionId(s->fullTransactionId), XidFromFullTransactionId(s->parent->fullTransactionId));
父子事务会形成链式结构,需要查询子事务最终状态需要遍历到最后一个。
原因是要实现rollback to的逻辑,必须将事务维护成chain的结构,rollback to会按顺序影响多个子事务。
而备库的逻辑是直接全部挂在顶层事务上,因为备库redo时:
for (i = 0; i < nsubxids; i++)
SubTransSetParent(subxids[i], topxid);
...
记录完了最大的,里面其他的就可以删了。
KnownAssignedXidsRemoveTree(InvalidTransactionId, nsubxids, subxids);
if (TransactionIdPrecedes(procArray->lastOverflowedXid, max_xid))
procArray->lastOverflowedXid = max_xid;
...
}