总结
- 一个行锁,xmax直接放xid。两个行锁,xmax放multixactid,然后multixactid=1 → offset,offset → 数据。双SLRU存储请参考[《Postgresql源码(156)MultiXact机制分析》]。(https://blog.csdn.net/jackgo73/article/details/158853458)
- 再来一个行锁的话,不会修改现有multixact记录,会新增一条multixactid=2,先把1的数据考过来,然后在追加新的。
- 行锁等的是什么锁?是从xmax位置读出来的xid,然后给事务id加锁,所以等的是事务id锁
XactLockTableWait。 - 四种multixact日志:
| WAL 类型 | 宏 | 值 | redo 核心动作 | 触发场景 |
|---|---|---|---|---|
| ZERO_OFF_PAGE | XLOG_MULTIXACT_ZERO_OFF_PAGE | 0x00 | ZeroMultiXactOffsetPage() |
MXact ID 跨入新 offsets 页面 |
| ZERO_MEM_PAGE | XLOG_MULTIXACT_ZERO_MEM_PAGE | 0x10 | ZeroMultiXactMemberPage() |
member 偏移量跨入新 members 页面 |
| CREATE_ID | XLOG_MULTIXACT_CREATE_ID | 0x20 | RecordNewMultiXact() + 推进计数器 |
两个以上事务对同一行持锁 |
| TRUNCATE_ID | XLOG_MULTIXACT_TRUNCATE_ID | 0x30 | PerformMembersTruncation() + PerformOffsetsTruncation() |
VACUUM 清理过时的 MultiXact |
multixact_redo 调试
调试SQL
sql
-- Session 1:
BEGIN;
SELECT * FROM test_multi WHERE id = 1 FOR SHARE;
-- 此时 xmax 还是普通 xid
-- Session 2:
BEGIN;
SELECT * FROM test_multi WHERE id = 1 FOR SHARE;
-- 此时 PostgreSQL 需要创建一个 MultiXact 来容纳两个事务的锁
-- 触发 MultiXactIdCreate() -> 写入 XLOG_MULTIXACT_CREATE_ID
-- 物理日志会立即发到备库,在备库上会触发multixact_redo
备库:

再来一个新会话对这一行加share锁,这里并没有修改mid=7的记录,而是直接新增mid=8:

现在有三个事务对这一行加了share锁,这时来一个update锁事务ID,开始等锁,这时不会产生WAL:

这时再来再来一个事务加share锁,会继续加进去,没有等锁队列。

multixact_redo代码分析
c
void
multixact_redo(XLogReaderState *record)
{
uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
/* MultiXact 记录不使用 backup block */
Assert(!XLogRecHasAnyBlockRefs(record));
if (info == XLOG_MULTIXACT_ZERO_OFF_PAGE)
{
/*
* ===== 处理 ZERO_OFF_PAGE: 零化 offsets SLRU 新页面 =====
* offsets SLRU 存储 MultiXactId -> member偏移量 的映射
*/
int pageno;
int slotno;
memcpy(&pageno, XLogRecGetData(record), sizeof(int));
/* 加锁后零化 offsets SLRU 页面, 然后立即写盘 */
LWLockAcquire(MultiXactOffsetSLRULock, LW_EXCLUSIVE);
slotno = ZeroMultiXactOffsetPage(pageno, false);
SimpleLruWritePage(MultiXactOffsetCtl, slotno);
Assert(!MultiXactOffsetCtl->shared->page_dirty[slotno]);
LWLockRelease(MultiXactOffsetSLRULock);
}
else if (info == XLOG_MULTIXACT_ZERO_MEM_PAGE)
{
/*
* ===== 处理 ZERO_MEM_PAGE: 零化 members SLRU 新页面 =====
* members SLRU 存储实际的 (xid, lock_mode) 成员列表
*/
int pageno;
int slotno;
memcpy(&pageno, XLogRecGetData(record), sizeof(int));
LWLockAcquire(MultiXactMemberSLRULock, LW_EXCLUSIVE);
slotno = ZeroMultiXactMemberPage(pageno, false);
SimpleLruWritePage(MultiXactMemberCtl, slotno);
Assert(!MultiXactMemberCtl->shared->page_dirty[slotno]);
LWLockRelease(MultiXactMemberSLRULock);
}
else if (info == XLOG_MULTIXACT_CREATE_ID)
{
/*
* ===== 处理 CREATE_ID: 重建 MultiXact 记录 =====
* 将 MultiXact 的 offset 和 members 写回 SLRU 文件
*/
xl_multixact_create *xlrec =
(xl_multixact_create *) XLogRecGetData(record);
TransactionId max_xid;
int i;
/*
* 核心动作: 将数据写回 SLRU 文件
* RecordNewMultiXact 会:
* 1. 在 offsets SLRU 中记录 mid -> moff 的映射
* 2. 在 members SLRU 中写入 nmembers 个 MultiXactMember
*/
RecordNewMultiXact(xlrec->mid, xlrec->moff, xlrec->nmembers,
xlrec->members);
/*
* 推进 nextMXact 和 nextOffset 计数器
* 确保它们不小于本记录中的值, 避免重复分配
*/
MultiXactAdvanceNextMXact(xlrec->mid + 1,
xlrec->moff + xlrec->nmembers);
/*
* 确保 nextXid 不小于记录中提到的任何 XID
* 这应该是多余的(这些 XID 在其他 WAL 中也有记录),
* 但作为安全措施仍然执行
*/
max_xid = XLogRecGetXid(record);
for (i = 0; i < xlrec->nmembers; i++)
{
if (TransactionIdPrecedes(max_xid, xlrec->members[i].xid))
max_xid = xlrec->members[i].xid;
}
AdvanceNextFullTransactionIdPastXid(max_xid);
}
else if (info == XLOG_MULTIXACT_TRUNCATE_ID)
{
/*
* ===== 处理 TRUNCATE_ID: 截断旧 MultiXact 数据 =====
*/
xl_multixact_truncate xlrec;
int pageno;
memcpy(&xlrec, XLogRecGetData(record), SizeOfMultiXactTruncate);
elog(DEBUG1, "replaying multixact truncation: "
"offsets [%u, %u), offsets segments [%x, %x), "
"members [%u, %u), members segments [%x, %x)",
xlrec.startTruncOff, xlrec.endTruncOff,
MultiXactIdToOffsetSegment(xlrec.startTruncOff),
MultiXactIdToOffsetSegment(xlrec.endTruncOff),
xlrec.startTruncMemb, xlrec.endTruncMemb,
MXOffsetToMemberSegment(xlrec.startTruncMemb),
MXOffsetToMemberSegment(xlrec.endTruncMemb));
/* 获取截断锁, 防止并发操作 */
LWLockAcquire(MultiXactTruncationLock, LW_EXCLUSIVE);
/*
* 推进 horizon 值(最老 MultiXact 限制), 确保恢复结束时是最新的
*/
SetMultiXactIdLimit(xlrec.endTruncOff, xlrec.oldestMultiDB, false);
/* 截断 members SLRU 段文件 */
PerformMembersTruncation(xlrec.startTruncMemb, xlrec.endTruncMemb);
/*
* 截断 offsets SLRU 段文件
* 在回放期间 latest_page_number 可能尚未设置,
* 需要手动插入一个合适的值来绕过 SimpleLruTruncate 的健全性检查
*/
pageno = MultiXactIdToOffsetPage(xlrec.endTruncOff);
MultiXactOffsetCtl->shared->latest_page_number = pageno;
PerformOffsetsTruncation(xlrec.startTruncOff, xlrec.endTruncOff);
LWLockRelease(MultiXactTruncationLock);
}
else
elog(PANIC, "multixact_redo: unknown op code %u", info);
}