引言MySQL与PG数据完整性机制对比DWB vs FPW 这篇文章有提到MySQL需要有flush list来保证刷脏时保证LSN全局有序。但明显PG和存算分离之后是没有flush list的本文来分析一下背后的原因。flush list 本身的作用核心功能维护脏页的LSN全局有序┌─────────────────────────────────────────┐ │ Buffer Pool │ │ │ │ LRU List淘汰管理 │ │ - 所有Page按访问时间排序 │ │ - 用于淘汰冷数据 │ │ - 不关心脏页、不关心LSN │ │ │ │ Flush List刷脏管理 │ │ - 仅脏页有未刷盘修改 │ │ - 严格按 oldest_modification LSN 排序 │ │ - tail 最老LSN优先刷 │ │ - head 最新LSN最后刷 │ │ │ └─────────────────────────────────────────┘ ↓ page_cleaner 线程从 tail 开始刷盘 ↓ 保证先刷 LSN100 的页再刷 LSN200 的页为什么必须有序B-tree 结构变化引入的跨Page依赖 页分裂操作 Step 1: 分配新页 P_new LSN100 Step 2: 初始化 P_new LSN110 Step 3: 更新父页 P_parent LSN120添加指向P_new的指针 依赖链P_parent 的内容依赖 P_new 的存在 刷盘顺序要求 必须先刷 P_new (LSN110)再刷 P_parent (LSN120) 否则崩溃后P_parent 指向不存在的页 → 数据损坏 flush list 保证 LSN110 在链表中位于 LSN120 之前 page_cleaner 必然先刷 110再刷 120为什么 PG 不需要这点其实MySQL与PG数据完整性机制对比DWB vs FPW 文中已经说了很多了这里再额外总结一下。核心原因操作语义 vs 字节修改对比一下同样的页分裂操作InnoDB redo字节修改有依赖场景页分裂创建新页 P_new父页 P_parent 添加指针 redo[1] LSN100: MLOG_PAGE_CREATE page_id: P_new // 只记录创建页页内容还是空的 redo[2] LSN110: MLOG_WRITE_STRING page_id: P_new, offset: 0, len: 100, data: 页头部分记录 // 依赖P_new 必须已存在且已初始化 redo[3] LSN120: MLOG_COMP_REC_INSERT page_id: P_parent, offset: 200, rec: {key50, ptrP_new} // 依赖P_new 必须已有有效内容否则指针指向垃圾 关键特性 - 每条 redo 是字节级补丁 - redo[3] 的物理内容包含指针值P_new 的地址 - 如果先 apply redo[3]P_new 还未初始化 → 崩溃后数据损坏PG WAL操作语义无依赖场景同样的页分裂 WAL[1] LSN100: XLOG_BTREE_SPLIT_L 内容{ node: 左页分裂, left_blk: 1234, // 原页 right_blk: 5678, // 新分配的右页仅记录块号 firstright: 50, // 分裂点 key newitem: {key50, ptr5678} // 插入父项的信息 } // 注意不包含 5678 页的物理内容 WAL[2] LSN110: 可能是其他操作或同一事务的后续 关键特性 - 每条 WAL 是操作描述不是字节补丁 - 恢复时PG 从 WAL 重新执行分裂操作 - 执行时会分配 5678 → 初始化内容 → 更新父页 - 不依赖磁盘上 5678 的当前状态依赖差异的本质维度InnoDB redoPG WAL记录内容在偏移 X 处写入字节 Y执行分裂操作参数如下恢复行为直接修改磁盘页的指定字节重新执行操作逻辑上下文依赖必须基于正确的当前页状态自带完整操作语义顺序敏感乱序 apply 导致字节级错误乱序重放仍能得到正确结构为什么存算分离之后不需要主要讨论的是log is database之后大部分情况下的做法。核心转变redo 从操作指令变为状态快照┌─────────────────────────────────────────┐ │ 传统 InnoDB需要 flush list │ │ │ │ redo 格式生理日志 │ │ 在 Page P 偏移 200 处插入记录 │ │ │ │ 特点 │ │ - 非自包含依赖执行上下文 │ │ - 恢复时必须按序 apply │ │ - 刷盘必须保证 LSN 顺序确保依赖满足 │ │ │ │ 需要 flush list强制全局有序刷脏 │ └─────────────────────────────────────────┘ ↓ 存算分离 ┌─────────────────────────────────────────┐ │ 存算分离架构无需 flush list │ │ │ │ redo 格式自包含的页镜像 │ │ Page P 的完整内容变为 [16KB 数据] │ │ │ │ 特点 │ │ - 完全自包含无执行依赖 │ │ - 直接覆盖即可无需 apply 过程 │ │ - 每个 Page 的 redo 独立无跨页依赖 │ │ │ │ 无需 flush listPer-Page 独立刷盘/恢复 │ └─────────────────────────────────────────┘存算分离的具体实现计算节点 MTR 提交时 - 生成该 MTR 涉及的所有 Page 的完整镜像 - LSN 仅用于标记版本不用于排序依赖 - 发送到存储节点任意顺序并行发送 存储节点 接收 Page A LSN1000 → 直接写入存储key(A, 1000), value完整镜像 → 无需关心其他 Page 接收 Page B LSN800 → 直接写入存储key(B, 800), value完整镜像 → 与 Page A 完全独立 读取 Page A → 返回 LSN 最大的版本即可 → 无需 apply 任何 redo关键消解传统InnoDB需要全局顺序的原因存算分离如何消解B-tree 页分裂的跨页指针依赖redo 包含完整页指针是结果的一部分页分配依赖空间管理页页分配也记录为完整状态redo 是增量必须基于正确基础 applyredo 是快照直接覆盖恢复时需要按序重放存储层直接保存各版本无需重放总结flush list 是 InnoDB 物理 redo 架构下为保证 B-tree 结构变化的刷盘时序依赖而设计的。InnoDB 的 redo 记录的是物理步骤步骤之间有字节级依赖必须按序 applyPG 的 WAL 记录的是操作结果恢复时重新执行完整操作不依赖磁盘页的中间状态因此可独立重放。PostgreSQL 的 WAL 记录操作语义而非字节修改恢复时重新执行操作不依赖磁盘页状态配合 Full Page Write 处理部分写损坏从而无需保证刷盘全局顺序无需 flush list。存算分离通过 redo 自包含完整页镜像彻底消解了结构依赖从而也无需 flush list。关键区分redo 自包含 vs 完整页概念含义InnoDB存算分离redo 自包含不依赖上下文即可应用❌ 否需磁盘页apply✅ 是直接覆盖redo 有完整页包含 16KB 页内容⚠️ 偶尔初始化等✅ 通常优化后