参考 《Postgresql源码(25)子事务可见性判断和性能问题》
XID获取顶层入口
函数:AssignTransactionId
代码语言:javascript复制static void
AssignTransactionId(TransactionState s)
{
...
优先给没有事务ID的父事务分配
确保父事务有 XID,以便子事务总是拥有一个比其父事务更新的 XID。这里不能递归调用,否则如果我们处于一个巨大的子事务堆栈的底部,而这些子事务还没有分配 XID,可能会遇到栈溢出的问题。
下面虽然递归了,但永远不会超过1层。
代码语言:javascript复制 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 中。
代码语言:javascript复制 if (isSubXact && XLogLogicalInfoActive() &&
!TopTransactionStateData.didLogXid)
log_unknown_top = true;
生成一个新的 FullTransactionId 并在 PGPROC 和 pg_subtrans 中记录其 xid。
注意:必须在 Xid 出现在 PGPROC 之外的任何共享存储中之前,先创建 subtrans 条目;因为如果 PGPROC 中没有空间容纳它,subtrans 条目是必需的,以确保其他后端将 Xid 视为“正在运行”。参见 GetNewTransactionId。
代码语言:javascript复制 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必须用事务的。
代码语言:javascript复制 XactLockTableInsert(XidFromFullTransactionId(s->fullTransactionId));
CurrentResourceOwner = currentOwner;
在每个顶级事务中,每分配 PGPROC_MAX_CACHED_SUBXIDS(64)个事务 id,就为分配发出一个 WAL 记录。
这里看最后一章的分析:
代码语言:javascript复制 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;
}
}
}
xid具体获取流程
函数:GetNewTransactionId
32位事务ID:
代码语言:javascript复制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来使用:
代码语言:javascript复制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,主要是兼容性问题不好解决。
代码语言:javascript复制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;
子事务ID维护
函数:SubTransSetParent
《Postgresql源码(25)子事务可见性判断和性能问题》
主机为什么每64个subxid就给备机发送一批?
结论:
- 主库给备库同步正在运行的xid列表,缓冲64个一批发过去,避免频率太高。
- 备库收到后会挑一个最大的xid做记录RecordKnownAssignedTransactionIds。
- 备库会把除了最大的都删了,因为备库可以根据最大值推测其他运行中的xid。
分析
在备机redo时,ProcArrayApplyXidAssignment负责处理收到的xids日志。
代码语言:javascript复制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);
}
包含了两部分:
- xlrec->xtop:顶层事务ID
- xlrec->xsub:子事务IDs
ProcArrayApplyXidAssignment函数负责处理:
代码语言:javascript复制ProcArrayApplyXidAssignment(TransactionId topxid,
int nsubxids, TransactionId *subxids)
{
...
max_xid = TransactionIdLatest(topxid, nsubxids, subxids);
最大的子事务ID记一下就好啦,其他的备库能推测出来。
代码语言:javascript复制 RecordKnownAssignedTransactionIds(max_xid);
注意这里,主库的标记逻辑是
SubTransSetParent(XidFromFullTransactionId(s->fullTransactionId), XidFromFullTransactionId(s->parent->fullTransactionId));
父子事务会形成链式结构,需要查询子事务最终状态需要遍历到最后一个。
原因是要实现rollback to的逻辑,必须将事务维护成chain的结构,rollback to会按顺序影响多个子事务。
而备库的逻辑是直接全部挂在顶层事务上,因为备库redo时:
- 完全不关心子事务的关系,可见性判断交给快照和xid就足够了。
- 那怎么知道sub xid是不是提交了?
- 情况1:sub xid的事务回滚了,那么主库会立即写clog,备库立即就知道了。
- 情况2:sub xid的事务提交了,主库不会立即通知备库,直到顶层事务commit或release。所以这种情况下,把sub xid直接挂到顶层事务上是没有问题的,refer directly to the top-level transaction’s state。
for (i = 0; i < nsubxids; i )
SubTransSetParent(subxids[i], topxid);
...
记录完了最大的,里面其他的就可以删了。
代码语言:javascript复制 KnownAssignedXidsRemoveTree(InvalidTransactionId, nsubxids, subxids);
if (TransactionIdPrecedes(procArray->lastOverflowedXid, max_xid))
procArray->lastOverflowedXid = max_xid;
...
}