Postgresql源码(157)Redo系列MultiXact Redo (RM_MULTIXACT_ID = 6)

相关
《Postgresql源码(156)MultiXact机制分析》

总结

  • 一个行锁,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);
}
相关推荐
大妮哟11 分钟前
postgresql数据库日志量异常原因排查
数据库·postgresql·oracle
还是做不到嘛\.1 小时前
Dvwa靶场-SQL Injection (Blind)-基于sqlmap
数据库·sql·web安全
不写八个1 小时前
PHP教程004:php链接mysql数据库
数据库·mysql·php
Dylan~~~2 小时前
深度解析Cassandra:分布式数据库的王者之路
数据库·分布式
荒川之神2 小时前
Oracle HR 模式递归函数练习(基于 employees 表)
数据库·oracle
F1FJJ2 小时前
Shield CLI Postgres v0.3.10:当 142 张表挤在一张 ER 图里,我们做了什么
网络·vscode·网络协议·postgresql·开源软件
小陈工3 小时前
2026年3月31日技术资讯洞察:AI智能体安全、异步编程突破与Python运行时演进
开发语言·jvm·数据库·人工智能·python·安全·oracle
杨云龙UP3 小时前
Linux生产环境下Oracle RMAN 备份、核查、清理与验证常用命令整理_20260330
linux·运维·服务器·数据库·oracle
橙子家3 小时前
关于列式存储(Column-base Storage)的几个要点解读
数据库
Mr.45673 小时前
Spring Boot 集成 PostgreSQL 表级备份与恢复实战
java·spring boot·后端·postgresql