在PostgreSQL的数据目录下有pg_subtrans目录,这个目录借助SLRU记录事务ID的父子关系。目前,事务ID的长度为32位。在SLRU中,每个页面默认都是8KB,所以每个页面可以保存2048个事务的父子关系。顶层的事务保存的是InvalidTransactionId(也就是0),子事务保存的是自己上一层事务的事务ID。假如有多层的嵌套事务,当知道某个子事务的ID之后,就能很容易地向上追溯其父事务ID。这种追溯是单向的,由父事务ID无法获得子事务ID。

// 每个页面可以记录多少个事务ID
#define SUBTRANS_XACTS_PER_PAGE (BLCKSZ / sizeof(TransactionId))
// 根据子事务ID,确定该事务对应的父事务ID应该保持到那个SLRU页面
#define TransactionIdToPage(xid) ((xid) / (TransactionId)SUBTRANS_XACTS_PER_PAGE )
// 根据子事务ID,确定该事务在页面中的什么位置记录父事务的ID
#define TransactionIdToEntry(xid) ((xid) % (TransactionId)SUBTRANS_XACTS_PER_PAGE )

Simple Lru在共享内存中的布局:

PostgreSQL数据库事务系统——pg_subtrans日志_检查点

/* Link to shared-memory data structures for SUBTRANS control */
static SlruCtlData SubTransCtlData;
#define SubTransCtl (&SubTransCtlData)

设置父事务ID到pg_subtrans日志

在AssignTransactionId函数中,如果新获得的事务ID是子事务ID,那么就需要设置它的父事务ID到pg_subtrans日志中。

static void AssignTransactionId(TransactionState s) {
bool isSubXact = (s->parent != NULL);
...
if (isSubXact)
SubTransSetParent(XidFromFullTransactionId(s->fullTransactionId), XidFromFullTransactionId(s->parent->fullTransactionId));

SubTransSetParent函数通过形参xid取出pg_subtrans SLRU页面的槽,并将parent设置到该槽中。

/* Record the parent of a subtransaction in the subtrans log. */
void SubTransSetParent(TransactionId xid, TransactionId parent) {
int pageno = TransactionIdToPage(xid); // 页面寻址
int entryno = TransactionIdToEntry(xid); // 页内寻址
int slotno; TransactionId *ptr;
Assert(TransactionIdIsValid(parent)); Assert(TransactionIdFollows(xid, parent));

LWLockAcquire(SubtransControlLock, LW_EXCLUSIVE);
slotno = SimpleLruReadPage(SubTransCtl, pageno, true, xid);
ptr = (TransactionId *) SubTransCtl->shared->page_buffer[slotno];
ptr += entryno;

/* It's possible we'll try to set the parent xid multiple times but we shouldn't ever be changing the xid from one valid xid to another valid xid, which would corrupt the data structure. */
if (*ptr != parent) { // 槽内数据不定于父事务id,则设置
Assert(*ptr == InvalidTransactionId);
*ptr = parent;
SubTransCtl->shared->page_dirty[slotno] = true;
}

LWLockRelease(SubtransControlLock);
}

执行检查点

在关闭过程或数据库执行过程中,执行检查点,也就是flush SLRU到磁盘上。

/* Perform a checkpoint --- either during shutdown, or on-the-fly */
void CheckPointSUBTRANS(void) {
/* Flush dirty SUBTRANS pages to disk
* This is not actually necessary from a correctness point of view. We do
* it merely to improve the odds that writing of dirty pages is done by
* the checkpoint process and not by backends. */
TRACE_POSTGRESQL_SUBTRANS_CHECKPOINT_START(true);
SimpleLruFlush(SubTransCtl, true);
TRACE_POSTGRESQL_SUBTRANS_CHECKPOINT_DONE(true);
}

Extend SUBTRANS SLRU页面

ExtendSUBTRANS函数必须在持有XidGenLock锁时调用,在SLRU中没有空闲页需要将脏页刷回磁盘,然后再初始化Zero需要的SLRU页面时会有磁盘IO。

/* Make sure that SUBTRANS has room for a newly-allocated XID.
* NB: this is called while holding XidGenLock. We want it to be very fast
* most of the time; even when it's not so fast, no actual I/O need happen
* unless we're forced to write out a dirty subtrans page to make room
* in shared memory. */
void ExtendSUBTRANS(TransactionId newestXact) {
int pageno;
/* No work except at first XID of a page. But beware: just after
* wraparound, the first XID of page zero is FirstNormalTransactionId. */
if (TransactionIdToEntry(newestXact) != 0 && !TransactionIdEquals(newestXact, FirstNormalTransactionId))
return;
pageno = TransactionIdToPage(newestXact);
LWLockAcquire(SubtransControlLock, LW_EXCLUSIVE);
ZeroSUBTRANSPage(pageno); /* Zero the page */
LWLockRelease(SubtransControlLock);
}

Truncate SUBTRANS SLRU页面

在运行检查点时,所有运行事务最oldest的TransactionXmin所持有的SLRU页面。

/* Remove all SUBTRANS segments before the one holding the passed transaction ID
* oldestXact is the oldest TransactionXmin of any running transaction. This
* is called only during checkpoint. */
void TruncateSUBTRANS(TransactionId oldestXact) {
int cutoffPage;

/*
* The cutoff point is the start of the segment containing oldestXact. We
* pass the *page* containing oldestXact to SimpleLruTruncate. We step
* back one transaction to avoid passing a cutoff page that hasn't been
* created yet in the rare case that oldestXact would be the first item on
* a page and oldestXact == next XID. In that case, if we didn't subtract
* one, we'd trigger SimpleLruTruncate's wraparound detection.
*/
TransactionIdRetreat(oldestXact);
cutoffPage = TransactionIdToPage(oldestXact);
SimpleLruTruncate(SubTransCtl, cutoffPage);
}

PostgreSQL数据库事务系统——pg_subtrans日志_数据库_02