Postgresql源码(155)Redo系列CLOG Redo (RM_CLOG_ID = 3)
总结RM_CLOG_ID处理的是PG提交日志的修改。CLOG记录每个事务的最终状态(in_progress/committed/aborted/sub_committed)存储在pg_xact/目录下的 SLRU 文件中。CLOG WAL只有两种:初始化页面(ZEROPAGE)和truncate旧页面(TRUNCATE)。事务提交流程是顺序是 先WAL落盘、在CLOG和数据落盘CLOG可以看做数据redo的时候会用wal覆盖CLOG的状态所以CLOG和数据一样需要后写需要永远落后于WAL。CLOG特殊之处: 事务状态的设置(COMMITTED/ABORTED)不通过 CLOG 自己的 WAL而是通过wal的 commit/abort 记录中的TransactionIdSetTreeStatus()函数直接写入CLOG页面。CLOG自己的wal记录只负责页面生命周期管理(创建销毁)。CommitTransaction() │ ▼ XactLogCommitRecord() │ 写一条 XACT 类型的 WAL 记录 (XLOG_XACT_COMMIT) │ 这条记录包含: 事务ID、提交时间戳、子事务列表等 ▼ TransactionIdCommitTree() │ ▼ TransactionIdSetTreeStatus() │ 直接修改 CLOG 共享内存中的页面 │ 把对应事务的 2-bit 状态从 IN_PROGRESS 改为 COMMITTED ▼ CLOG 页面变脏, 后续由 checkpoint 刷盘clog两类redo总结WAL 类型宏值redo 核心动作触发条件ZEROPAGECLOG_ZEROPAGE0x00ZeroCLOGPage()SimpleLruWritePage()nextXid 跨越到新 CLOG 页面(每 32768 个 xid)TRUNCATECLOG_TRUNCATE0x10AdvanceOldestClogXid()SimpleLruTruncate()VACUUM 清理不再需要的旧事务状态1 WAL Record 类型与结构体事务状态定义/* src/include/access/clog.h *//* * 事务的四种可能状态 -- 注意: 全零(0x00)是初始状态(IN_PROGRESS) * 每个事务占用 2 bit, 一个 8KB CLOG 页面可以存储 8192*4 32768 个事务 */typedefintXidStatus;#defineTRANSACTION_STATUS_IN_PROGRESS0x00/* 进行中 */#defineTRANSACTION_STATUS_COMMITTED0x01/* 已提交 */#defineTRANSACTION_STATUS_ABORTED0x02/* 已中止 */#defineTRANSACTION_STATUS_SUB_COMMITTED0x03/* 子事务已提交(父事务尚未提交) */数据结构/* src/include/access/clog.h *//* * xl_clog_truncate: CLOG truncate操作的 WAL 记录数据 * 当 VACUUM 清理旧事务时, 不再需要的 CLOG 页面可以被truncate */typedefstructxl_clog_truncate{intpageno;/* truncate到的页面编号(保留此页面及之后的) */TransactionId oldestXact;/* 最老的仍需保留的事务 ID */Oid oldestXactDb;/* 该最老事务所属的数据库 OID */}xl_clog_truncate;CLOG_ZEROPAGE 的数据: 仅一个int pageno(不需要结构体直接写)。2 GDB触发 SQL 示例CLOG_ZEROPAGE (0x00) – 新 CLOG 页面初始化-- CLOG_ZEROPAGE 在事务 ID 耗尽当前 CLOG 页面范围时自动触发-- 每个 CLOG 页面覆盖 32768 个事务(8KB * 4 xid/byte)-- 当 nextXid 跨越到需要新页面时, ExtendCLOG() 会零化新页面并写入 WAL-- 通常通过大量事务来触发:-- (在测试中难以直接触发, 因为需要恰好跨越 32K xid 边界)BEGIN;INSERTINTOtVALUES(1);COMMIT;-- ... 重复直到 xid 跨越 CLOG 页面边界CLOG_TRUNCATE (0x10) – VACUUM 清理旧 CLOG 页面-- 当 autovacuum 或手动 VACUUM 运行时, 如果发现很老的事务已不再需要,-- 会调用 TruncateCLOG() truncate过时的 CLOG 段文件VACUUM;-- 也可以通过 pg_controldata 查看当前 oldestXid,-- 确认哪些 CLOG 页面可以安全truncate3 Redo 核心代码 (带中文注释)/* src/backend/access/transam/clog.c:984-1019 *//* * CLOG 资源管理器的 redo 入口函数 */voidclog_redo(XLogReaderState*record){uint8 infoXLogRecGetInfo(record)~XLR_INFO_MASK;/* CLOG 记录不使用 backup block(全页写) */Assert(!XLogRecHasAnyBlockRefs(record));if(infoCLOG_ZEROPAGE){/* * 处理 ZEROPAGE: 零化新 CLOG 页面 * 当事务 ID 增长到需要新 CLOG 页面时, * 必须先将该页面清零(因为 0x00 IN_PROGRESS 是正确的初始状态) */intpageno;intslotno;/* 从 WAL data 中取出页面编号 */memcpy(pageno,XLogRecGetData(record),sizeof(int));/* 获取 SLRU 排他锁, 防止并发访问 */LWLockAcquire(XactSLRULock,LW_EXCLUSIVE);/* 零化指定页面: false 表示不写 WAL(因为我们正在回放 WAL) */slotnoZeroCLOGPage(pageno,false);/* 立即将零化后的页面写入磁盘, 确保持久化 */SimpleLruWritePage(XactCtl,slotno);Assert(!XactCtl-shared-page_dirty[slotno]);LWLockRelease(XactSLRULock);}elseif(infoCLOG_TRUNCATE){/* * 处理 TRUNCATE: truncate旧 CLOG 段文件 * 将不再需要的旧 CLOG 页面从磁盘删除 */xl_clog_truncate xlrec;memcpy(xlrec,XLogRecGetData(record),sizeof(xl_clog_truncate));/* 更新全局的 oldestClogXid, 表示更老的事务不再需要查看 CLOG */AdvanceOldestClogXid(xlrec.oldestXact);/* 执行 SLRU truncate: 删除 pageno 之前的所有段文件 */SimpleLruTruncate(XactCtl,xlrec.pageno);}elseelog(PANIC,clog_redo: unknown op code %u,info);}4 GDB 调试在本次测试中CLOG_ZEROPAGE 未被触发。这是正常的因为 ZEROPAGE 只在事务 ID 增长到需要分配新 CLOG 页面时才会产生每 32768 个事务一次。未触发原因分析:CLOG 每页存储 32768 个事务的状态(8KB page, 每个 xid 占 2 bit)只有当 nextXid 跨越到新页面范围时ExtendCLOG()才会调用ZeroCLOGPage()并写入 CLOG_ZEROPAGE WAL 记录在常规小规模测试中事务 ID 通常不会跨越页面边界如果要在 GDB 中捕获 CLOG_ZEROPAGE可以:# 在 clog_redo 设置断点 break clog_redo # 或者在写入端 break clog.c:ZeroCLOGPage5 官方文档src/backend/access/transam/READMEpg_xact and pg_subtrans are permanent (on-disk) storage of transaction relatedinformation. There is a limited number of pages of each kept in memory, soin many cases there is no need to actually read from disk. However, ifthere’s a long running transaction or a backend sitting idle with an opentransaction, it may be necessary to be able to read and write this informationfrom disk.CLOG (pg_xact) 是事务状态的持久化存储使用 SLRU(简单 LRU)机制管理内存中的有限页面。大部分读取来自内存缓存只有长事务或空闲事务才需要磁盘 I/O。异步提交与 CLOG LSNFirst, for each page of CLOG we must remember the LSN of the latest commitaffecting the page, so that we can enforce the same flush-WAL-before-writerule that we do for ordinary relation pages. Otherwise the record of thecommit might reach disk before the WAL record does.CLOG 页面也遵守 WAL-first 规则。每个 CLOG 页面都有关联的 LSN确保提交记录的 WAL 在 CLOG 数据刷盘之前先持久化。子事务与 CLOG 的两阶段协议The main role of marking transactions as sub-committed is to provide anatomic commit protocol when transaction status is spread across multiple clogpages. Whenever transaction status spreads across multiple pages we must usea two-phase commit protocol: the first phase is to mark the subtransactionsas sub-committed, then we mark the top level transaction and all itssubtransactions committed (in that order).当一个事务树的状态跨越多个 CLOG 页面时使用两阶段提交协议:第一阶段: 将子事务标记为 SUB_COMMITTED (0x03)第二阶段: 将顶层事务和所有子事务标记为 COMMITTED (0x01)这确保了即使在跨页面时事务提交的原子性也能得到保证。Recovery 期间的 CLOG 处理Not all transactional behaviour is emulated, for example we do not inserta transaction entry into the lock table, nor do we maintain the transactionstack in memory. Clog, multixact and commit_ts entries are made normally.在 WAL 恢复期间CLOG 条目的写入与正常运行时完全一致。恢复进程会正常维护 CLOG确保 Hot Standby 查询能获取正确的事务可见性信息。附录redo调用的SimpleLruTruncate函数分析注意: 这里的truncate不是清空文件而是删除文件。CLOG 数据存储在pg_xact/目录下的多个段文件中(如0000,0001,0002…),每个段文件包含 32 个页面(32 × 8KB 256KB)。当 VACUUM 判定某些旧事务的状态不再需要时,SimpleLruTruncate()会扫描目录并unlink(删除) 那些完全落在 cutoffPage 之前的段文件。直观理解: pg_xact/ 目录下的段文件: ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ 0000 │ │ 0001 │ │ 0002 │ │ 0003 │ │ 0004 │ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ page 0-31 32-63 64-95 96-127 128-159 假设 cutoffPage 70 (即页面70之前的都不需要了) 段 0000 (page 0-31): 31 70 → unlink 删除 ✓ 段 0001 (page 32-63): 63 70 → unlink 删除 ✓ 段 0002 (page 64-95): 64 70 但 95 70 → 保留 ✗ (cutoff 落在段内部) 段 0003 (page 96-127): 96 70 → 保留 ✗ 段 0004 (page 128-159):128 70 → 保留 ✗ 结果: 文件 0000 和 0001 被 unlink 删除, 0002 及之后的文件完整保留(不做 ftruncate)源码/* src/backend/access/transam/slru.c */voidSimpleLruTruncate(SlruCtl ctl,intcutoffPage){SlruShared sharedctl-shared;intslotno;/* 更新 truncate 统计计数器 */pgstat_count_slru_truncate(shared-slru_stats_idx);/* * 第一阶段: 清理共享内存中的 SLRU 缓冲区 * * 扫描共享内存中的所有 SLRU buffer slot, * 将属于 cutoffPage 之前的页面从缓冲区中驱逐出去, * 防止后续意外将旧页面重新写回磁盘。 * * (这通常发生在 checkpoint 之后, 脏页应该已经落盘, * 这里只是做额外的安全保障。) */LWLockAcquire(shared-ControlLock,LW_EXCLUSIVE);restart:/* * 安全检查: 最新页面不能被 truncate。 * 如果出现这种情况, 说明可能发生了 XID 回绕异常。 */if(ctl-PagePrecedes(shared-latest_page_number,cutoffPage)){LWLockRelease(shared-ControlLock);ereport(LOG,(errmsg(could not truncate directory \%s\: apparent wraparound,ctl-Dir)));return;}for(slotno0;slotnoshared-num_slots;slotno){if(shared-page_status[slotno]SLRU_PAGE_EMPTY)continue;/* 只处理 cutoffPage 之前的页面 */if(!ctl-PagePrecedes(shared-page_number[slotno],cutoffPage))continue;/* * 如果页面是干净的(VALID且不脏), 直接标记为 EMPTY。 * 这是最常见的情况。 */if(shared-page_status[slotno]SLRU_PAGE_VALID!shared-page_dirty[slotno]){shared-page_status[slotno]SLRU_PAGE_EMPTY;continue;}/* * 如果页面正在进行 I/O 或者是脏页: * - 脏页: 先写回磁盘再驱逐 * - I/O中: 等待 I/O 完成 * 然后重新开始扫描(因为释放锁期间状态可能变化)。 */if(shared-page_status[slotno]SLRU_PAGE_VALID)SlruInternalWritePage(ctl,slotno,NULL);elseSimpleLruWaitIO(ctl,slotno);gotorestart;}LWLockRelease(shared-ControlLock);/* * 第二阶段: 扫描磁盘目录, 删除旧的段文件 * * 扫描 pg_xact/ 目录下的所有文件, * 对每个文件调用 SlruScanDirCbDeleteCutoff 回调, * 判断该段文件是否完全在 cutoffPage 之前, * 如果是则调用 unlink() 删除。 */(void)SlruScanDirectory(ctl,SlruScanDirCbDeleteCutoff,cutoffPage);}函数调用SimpleLruTruncate(ctl, cutoffPage) │ ├── 第一阶段: 清理共享内存缓冲区 │ └── 遍历 shared-page_status[0..num_slots-1] │ ├── 干净页面 → page_status SLRU_PAGE_EMPTY (驱逐) │ ├── 脏页面 → SlruInternalWritePage() → 写回磁盘后驱逐 │ └── I/O中 → SimpleLruWaitIO() → 等待完成后重试 │ └── 第二阶段: 删除磁盘文件 └── SlruScanDirectory(ctl, SlruScanDirCbDeleteCutoff, cutoffPage) │ ├── AllocateDir(ctl-Dir) // 打开 pg_xact/ 目录 │ ├── 对目录中每个合法文件名 (如 0000, 001F): │ │ segno strtol(filename, 16) // 十六进制解析段号 │ │ segpage segno * 32 // 段的首页编号 │ │ │ └── SlruScanDirCbDeleteCutoff(ctl, filename, segpage, cutoffPage) │ │ │ └── SlruMayDeleteSegment(ctl, segpage, cutoffPage) │ │ // 判断条件: 段的首页和尾页都在 cutoffPage 之前 │ │ seg_last_page segpage 31 │ │ return PagePrecedes(segpage, cutoffPage) │ │ PagePrecedes(seg_last_page, cutoffPage) │ │ │ ├── true → SlruInternalDeleteSegment(ctl, segno) │ │ │ │ │ ├── RegisterSyncRequest(SYNC_FORGET_REQUEST) │ │ │ // 取消该段的待 fsync 请求 │ │ │ │ │ └── unlink(path) ← 真正删除文件! │ │ │ └── false → 跳过, 保留该段文件 │ └── FreeDir()SlruInternalDeleteSegment – 最终unlink删除/* src/backend/access/transam/slru.c */staticvoidSlruInternalDeleteSegment(SlruCtl ctl,intsegno){charpath[MAXPGPATH];/* 取消该段的待 fsync 请求(文件都要删了, 不需要再 fsync) */if(ctl-sync_handler!SYNC_HANDLER_NONE){FileTag tag;INIT_SLRUFILETAG(tag,ctl-sync_handler,segno);RegisterSyncRequest(tag,SYNC_FORGET_REQUEST,true);}/* 构造文件路径并调用 unlink() 删除文件 */SlruFileName(ctl,path,segno);ereport(DEBUG2,(errmsg_internal(removing file \%s\,path)));unlink(path);/* ← 这里! 不是 ftruncate, 是 unlink, 直接删除文件 */}

相关新闻

【论文】Agentic NL2SQL to Reduce Computational Costs

【论文】Agentic NL2SQL to Reduce Computational Costs

Agentic NL2SQL to Reduce Computational Costs arxiv 下面是在原有框架基础上,按你要求“最开头附上链接并给出文章名”整理后的完整分析。 一. 翻译摘要原文 将自然语言查询转换为SQL查询(NL2SQL 或 Text-to-SQL)最近得益于大型语言模型&…

2026/5/17 11:06:10 阅读更多 →
【量化工具推荐】期货量化交易调试工具推荐:10款工具深度分析

【量化工具推荐】期货量化交易调试工具推荐:10款工具深度分析

一、前言 做期货量化交易,调试是开发过程中不可或缺的环节。好的调试工具能帮你快速定位问题、验证逻辑、优化策略。不同的调试工具有不同的特点——有的集成在IDE中,有的支持断点,有的适合异步和事件驱动,有的专注性能分析。 本…

2026/7/3 0:50:54 阅读更多 →
浙江大学团队:AI实现真人级实时直播互动能力

浙江大学团队:AI实现真人级实时直播互动能力

对话已经成为我们数字生活的核心部分。当你在直播间看到主播娓娓道来,在游戏解说中听到精彩点评,或在学习时获得及时指导,这些看似简单的互动背后,其实隐藏着巨大的技术挑战。现在,一项来自浙江大学、深圳大学、华南理…

2026/5/17 6:55:52 阅读更多 →

最新新闻

太玄经二十七部(3位三进制符号表(27组))

太玄经二十七部(3位三进制符号表(27组))

太玄经二十七部(3位三进制符号表(27组)) 三元九宫图: ​​​​​​​ 1. 天部(第一位为 ,共9组) 序号 符号组合 三进制值 太玄部名 核心含义 1 (1,1,1) 天部中天 纯阳至极&am…

2026/7/3 2:52:38 阅读更多 →
医疗预测建模实战:从临床共识到可行动预警

医疗预测建模实战:从临床共识到可行动预警

医疗预测建模这件事,我干了整整十二年——从三甲医院信息科借调支援的“临时工”,到后来牵头搭建省级慢病风险预警平台,再到如今帮基层社区卫生服务中心落地轻量化AI辅助决策工具。说实话,第一次看到“Predictive Modeling in Hea…

2026/7/3 2:50:37 阅读更多 →
终极B站视频下载指南:解锁大会员4K和充电专属内容

终极B站视频下载指南:解锁大会员4K和充电专属内容

终极B站视频下载指南:解锁大会员4K和充电专属内容 【免费下载链接】bilibili-downloader B站视频下载,支持下载大会员清晰度4K,持续更新中 项目地址: https://gitcode.com/gh_mirrors/bil/bilibili-downloader 你是否曾经想要永久保存…

2026/7/3 2:44:33 阅读更多 →
Loki MCP Server -支持Claude Desktop/Claude Code/Cursor 等客户端通过自然语言查询日志

Loki MCP Server -支持Claude Desktop/Claude Code/Cursor 等客户端通过自然语言查询日志

MCP定位,技术栈,架构,项目结构,基础框架搭建,开发部署及常见问题 # Loki MCP Server - CLAUDE.md> Go 实现的 MCP Server,集成 Grafana Loki 日志查询。支持 Claude Desktop / Claude Code / Cursor 等…

2026/7/3 2:42:31 阅读更多 →
嵌套 H5 的跨端通信:iOS / Android / 小程序 / 浏览器

嵌套 H5 的跨端通信:iOS / Android / 小程序 / 浏览器

一、为什么要做“统一桥接层”? “Write once, run anywhere” 对于纯展示型 H5 是成立的。但只要涉及到业务交互,比如:调起原生登录、保存图片到相册、修改系统状态栏颜色、分享到朋友圈,浏览器标准的 Web API 根本无能为力。 …

2026/7/3 2:40:31 阅读更多 →
交叉熵损失函数实战指南:原理、陷阱与工业级调优

交叉熵损失函数实战指南:原理、陷阱与工业级调优

1. 项目概述:为什么交叉熵损失函数不是“又一个公式”,而是模型精度的隐形操盘手在机器学习项目里,你调用model.compile(losscategorical_crossentropy)可能只需要0.3秒,但背后这个看似简单的函数,却直接决定了模型是“…

2026/7/3 2:38:31 阅读更多 →

日新闻

Nginx防御TLS重协商攻击实战:从原理到配置与监控

Nginx防御TLS重协商攻击实战:从原理到配置与监控

1. 项目概述:为什么TLS重协商攻击至今仍需警惕十多年前的CVE-2011-1473,一个关于TLS/SSL协议重协商机制的漏洞,现在提起来还有必要吗?很多运维和开发朋友可能会觉得,这都老掉牙了,现代服务器和客户端不都默…

2026/7/3 0:03:59 阅读更多 →
华为防火墙双通道远程管理实战:Web与SSH配置详解

华为防火墙双通道远程管理实战:Web与SSH配置详解

1. 项目概述:为什么需要双通道远程管理防火墙?在任何一个稍具规模的企业网络里,防火墙都是那个默默守护在边界的关键角色。作为网络工程师,我们不可能每次都跑到机房,插上console线去配置它。远程管理能力,…

2026/7/3 0:03:59 阅读更多 →
AD74413R与PIC18F65K40的高精度工业数据采集方案

AD74413R与PIC18F65K40的高精度工业数据采集方案

1. 项目概述:AD74413R与PIC18F65K40的协同工作在工业自动化和精密测量领域,同时实现高精度模数转换(ADC)和数模转换(DAC)功能是许多复杂系统的核心需求。AD74413R作为一款四通道可配置模拟输入/输出器件,与PIC18F65K40微控制器的组合&#xf…

2026/7/3 0:05:59 阅读更多 →

周新闻

月新闻