Postgresql源码(120)事务XID分配与主备XID同步

2024-02-05 16:50:40 浏览数 (1)

参考 《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时:

  1. 完全不关心子事务的关系,可见性判断交给快照和xid就足够了。
  2. 那怎么知道sub xid是不是提交了?
    • 情况1:sub xid的事务回滚了,那么主库会立即写clog,备库立即就知道了。
    • 情况2:sub xid的事务提交了,主库不会立即通知备库,直到顶层事务commit或release。所以这种情况下,把sub xid直接挂到顶层事务上是没有问题的,refer directly to the top-level transaction’s state。
代码语言:javascript复制
	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;
	
	...
}

0 人点赞