达梦数据库中的脏页与脏数据:从 WAL 到检查点的完整链路

📅 2026/8/2 10:25:33
达梦数据库中的脏页与脏数据:从 WAL 到检查点的完整链路
探究 数据页从干净 → 变脏 → 再变干净 这一生命周期。在达梦里有两层含义维度 脏页Dirty Page 脏数据 / 脏读Dirty Data / Dirty Read层面 物理/缓冲层 事务/隔离层定义 内存中的数据页已被修改且与磁盘上对应页不一致 事务修改了数据但尚未提交若其他事务读到这些未提交修改即脏读是否合法 正常运行时大量存在是性能设计的一部分 是否允许取决于隔离级别读未提交允许读已提交及以上禁止落盘关系 由检查点 / 缓冲淘汰 / 关闭等机制刷回磁盘 与「数据页是否已刷盘」无直接对应未提交事务也可能产生脏页脏页回答的是内存和磁盘是否一致脏数据脏读回答的是别的事务能不能看见我还没提交的修改两者可以同时存在但绝不能互相替代。一页数据的完整生命周期┌─────────┐ DML 修改 ┌─────────┐ Checkpoint / 淘汰 ┌─────────┐│ 干净页 │ ──────────► │ 脏页 │ ──────────────────► │ 干净页 ││ (Clean) │ │ (Dirty) │ │ (Clean) │└─────────┘ └────┬────┘ └─────────┘││ 同时写入 REDO 缓冲区▼┌─────────┐ 刷盘条件满足│ REDO缓冲 │ ──────────────────► REDO 文件落盘└─────────┘关键约束永远是日志先落盘数据页后落盘WAL, Write-Ahead Logging即便是未提交事务产生的脏页在被刷入数据文件之前系统也会强制检查并补刷相关 REDO保证「页上已有的修改日志里一定能重做出来」。达梦缓冲池脏页住在哪条链上DM Server 启动时按 dm.ini 中的缓冲参数向 OS 申请连续内存按页格式化后挂入管理链。数据缓冲区内有三条核心链表自由链Free尚未使用的内存页LRU 链已被使用的页含干净页与脏页按最近使用顺序排列脏链Dirty已被修改、尚未写回磁盘的页当自由链耗尽时从 LRU 链尾淘汰最近较少使用的页若被淘汰的是脏页必须先在满足 WAL 前提下写回磁盘再腾出缓冲槽位。缓冲池类型可通过 V$BUFFERPOOL 观察-- 查看各类型缓冲池及脏页数量SELECTNAME,PAGE_SIZE,N_PAGES,FREE,N_DIRTY,N_CLEAR,ROUND(RAT_HIT*100,2)ASHIT_RATIO_PCT,N_PHY_WRITEFROMV$BUFFERPOOLORDERBYN_DIRTYDESC;常见类型NORMAL主缓冲、KEEP尽量常驻、RECYCLE临时表空间、FAST、ROLL回滚相关等。日常脏页压力主要看 NORMAL 池的 N_DIRTY。WAL为什么「COMMIT 不等于数据页落盘」4.1 COMMIT 真正保证的是什么达梦与大多数 RDBMS 一致COMMIT 的核心作用确保该事务的 REDO含提交标记已落盘从而保证持久性Durability。COMMIT 不保证该事务修改过的数据页已经写入 .dbf 数据文件。因此会出现一种完全正常的状态事务 T1: UPDATE → 脏页在缓冲 → REDO 已落盘 → COMMIT 成功此时磁盘上的数据页可能仍是旧值崩溃后靠 REDO 重做即可恢复到已提交状态。4.2 日志刷盘的触发条件不止 COMMITCOMMIT 是日志刷盘的重要触发点但不是唯一触发点。常见还包括触发场景 说明日志缓冲区满 / 达阈值 缓冲写满前必须刷出否则无法继续写日志后台日志 FLUSH 线程定期工作 dm_redolog_thd 负责合并缓冲并顺序写日志检查点Checkpoint 刷脏页前必须先保证相关 REDO 已落盘脏页即将被淘汰写盘 WAL 强制「先日志后数据」伪代码刻画刷盘顺序procedure FlushDirtyPage(page): // 永远不允许数据页已在磁盘上更新而对应 REDO 还在内存ifpage.redo_lsnrlog.file_lsnthenFlushRedoUpTo(page.redo_lsn)-- 先把日志补到至少覆盖该页修改 endifWriteDataPageToDisk(page)page.dirty :false// 对应 REDO 之后可被检查点推进而回收 end procedure procedure Commit(trx): WriteCommitRecord(trx)-- 提交标记进入日志缓冲 EnsureRedoFlushed(trx.last_lsn)-- 若尚未落盘同步刷一次 // 注意此处不强制刷脏页returnSUCCESS end procedure事务最终 COMMIT 时系统会检查该事务相关 REDO 是否已在磁盘已在盘上提交几乎瞬时完成主要是写提交标记/确认尚未在盘上此刻触发一次同步刷日志确保落盘后再返回成功。线程协作谁写日志、谁写脏页结合达梦实例线程模型可以把责任拆开工作线程 (Worker)│ 修改缓冲页生成 REDO 到日志缓冲▼日志 FLUSH 线程 (dm_redolog_thd)│ 合并日志缓冲 → 顺序写 REDO 文件│ 若配置实时归档刷盘前可先发往备库▼调度线程 (dm_sched_thd) ← 每秒轮询│ 检查 CKPT_INTERVAL / 脏页数 / 日志量 → 触发检查点▼检查点线程 / 任务│ 决定刷多少脏页FLUSH_RATE / FLUSH_PAGES▼I/O 线程 (dm_io_thd)│ 真正把数据页写入磁盘▼数据文件 (.dbf)要点日志顺序写通常比数据页随机写更高效日志线程与 I/O 线程分离避免互相阻塞脏页刷盘前必须保证对应 REDO 已由 FLUSH 线程落盘。检查点让脏页「重新变干净」并推进日志环6.1 在线 REDO 是一个环达梦在线 REDO 默认两个物理文件可手工增加或扩容系统不会自动扩展。可把它想象成一个环┌────── free space ──────┐TAIL ●────────────────────────● HEAD│ 有效 REDO尚可能用于恢复│└────────────────────────┘HEAD新日志写入点TAIL有效日志起点检查点推进后右移HEAD 与 TAIL 之间的空隙是可复用的空闲空间检查点的本质工作把缓冲中的部分或全部脏页写入数据文件这些页对应的 REDO 不再需要保留推进 TAIL推进检查点释放日志空间供循环复用。副作用需要重做的日志更少 → 故障恢复时间缩短。代价是额外的数据页 I/O。6.2 触发检查点的条件以下条件可同时生效满足任一即可触发参数为 0 表示关闭该路触发# dm.ini 片段示例默认值以实际版本为准CKPT_INTERVAL300# 定时触发单位秒0关闭CKPT_RLOG_SIZE100# 已占用 REDO 达到该值(MB)则触发0关闭CKPT_DIRTY_PAGES10000# 缓冲脏页数超过该值则触发0关闭CKPT_FLUSH_RATE5.00# 每次刷「待刷相关」的约 5%CKPT_FLUSH_PAGES1000# 每次至少刷这么多页CKPT_WAIT_PAGES128# 单批串行写入页数上限分批推进 LSN另外还有「保底」路径即便上述条件未满足只要 REDO 可用空间不安全与 RLOG_SAFE_SPACE 等相关系统也会主动触发检查点甚至进入日志预留/等待状态避免日志环被写爆。手工触发-- 触发一次刷盘比例约 20% 的检查点SELECTCHECKPOINT(20)FROMDUAL;-- 控制台启动时也可使用 CKPT 命令视启动方式而定6.3 COMMIT vs Checkpoint 对照-- 概念对照注释说明非可执行断言-- COMMIT:-- 保证本事务 REDO含提交标记落盘-- 不保证脏数据页写入 .dbf---- CHECKPOINT:-- 保证按策略刷脏页 推进检查点 LSN / 日志 TAIL-- 同时刷脏页前仍会遵守 WAL必要时先刷 REDO操作 REDO 落盘 脏页落盘 主要目的COMMIT 是该事务相关 否不强制 事务持久性Checkpoint 视需要补刷 是按比例/页数 回收日志、缩短恢复、腾缓冲用 SQL「看见」脏页与日志进度7.1 缓冲脏页与命中率SELECTNAME,N_DIRTY,N_PAGES,FREE,ROUND(N_DIRTY*100.0/NULLIF(N_PAGES,0),2)ASDIRTY_PCT,ROUND(RAT_HIT*100,2)ASHIT_RATIO_PCT,N_PHY_READS,N_PHY_WRITEFROMV$BUFFERPOOLWHERENAMEIN(NORMAL,KEEP,RECYCLE,FAST,ROLL)ORDERBYN_DIRTYDESC;经验观察非绝对阈值N_DIRTY 长期接近池容量且伴随检查点频繁、写 I/O 飙高 → 考虑加大 BUFFER、优化大批量 DML 批次或审视 CKPT_* 是否过激/过缓命中率持续偏低 → 优先查 SQL 与缓冲大小而非一味加快刷脏。7.2 在线 REDO 空间与刷盘进度-- 日志空间概览SELECTCAST(SYSDATEASVARCHAR(20))ASCHECK_TIME,ROUND(TOTAL_SPACE/1024/1024,2)ASTOTAL_MB,ROUND(FREE_SPACE/1024/1024,2)ASFREE_MB,ROUND((TOTAL_SPACE-FREE_SPACE)/1024/1024,2)ASUSED_MBFROMV$RLOG;-- 更细的 LSN / 刷盘状态字段以实际版本为准SELECTCUR_LSN,FILE_LSN,FLUSHING_PAGES,ROUND(TOTAL_SPACE/1024/1024,2)ASTOTAL_MB,ROUND(FREE_SPACE/1024/1024,2)ASFREE_MBFROMV$RLOG;-- 日志文件清单SELECTCLIENT_PATHASLOG_NAME,PATH,ROUND(RLOG_SIZE/1024/1024,2)ASSIZE_MB,CREATE_TIMEFROMV$RLOGFILE;关注点FREE_MB 持续偏低 → 检查点跟不上写入或在线日志总容量偏小CUR_LSN 与 FILE_LSN 差距拉大 → 日志刷盘压力大COMMIT 可能变慢。7.3 检查点相关参数一览SELECTPARA_NAME,PARA_VALUEFROMV$DM_INIWHEREPARA_NAMEIN(CKPT_INTERVAL,CKPT_RLOG_SIZE,CKPT_DIRTY_PAGES,CKPT_FLUSH_RATE,CKPT_FLUSH_PAGES,CKPT_WAIT_PAGES,RLOG_SAFE_SPACE,RLOG_BUF_SIZE,RLOG_POOL_SIZE,BUFFER)ORDERBYPARA_NAME;部分环境也可对照检查点历史视图若版本支持-- 若存在该视图可观察检查点频率与耗时趋势SELECT*FROMV$CKPT_HISTORYORDERBY1DESC;事务层的「脏」脏读与隔离级别物理脏页与逻辑脏读要分开做实验认知。8.1 演示读未提交下的脏读会话 A-- 会话 ACREATETABLET_DIRTY_DEMO(IDINTPRIMARYKEY,VALVARCHAR(32));INSERTINTOT_DIRTY_DEMOVALUES(1,INIT);COMMIT;SETTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;UPDATET_DIRTY_DEMOSETVALDIRTY_UNCOMMITTEDWHEREID1;-- 注意此处故意不 COMMIT会话 B同为READUNCOMMITTEDsql -- 会话 B SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT ID, VAL FROM T_DIRTY_DEMO WHERE ID 1; -- 可能读到 DIRTY_UNCOMMITTED —— 这就是脏读 -- 若会话 A 随后 ROLLBACK会话 B 刚才读到的值在业务上是「从未真正存在」的默认隔离级别通常为读已提交下会话 B 不应看到 A 未提交的修改但 A 修改产生的脏页仍然可能存在于缓冲池中只是对其他会话不可见靠锁/MVCC/回滚段读一致性视图等机制实现。8.2对照实验COMMIT后脏页是否立刻消失sql -- 会话观察 COMMIT 前后脏页数变化示意 SELECT SUM(N_DIRTY) AS DIRTY_BEFORE FROM V$BUFFERPOOL; UPDATE T_DIRTY_DEMO SET VAL AFTER_COMMIT_TEST WHERE ID 1; COMMIT; SELECT SUM(N_DIRTY) AS DIRTY_AFTER_COMMIT FROM V$BUFFERPOOL; -- 通常 DIRTY_AFTER_COMMIT 不会因 COMMIT 而骤降到 0 -- 说明提交保证的是日志不是立刻把所有相关脏页刷盘 SELECT CHECKPOINT(100) FROM DUAL; -- 加大刷盘力度 SELECT SUM(N_DIRTY) AS DIRTY_AFTER_CKPT FROM V$BUFFERPOOL; -- 检查点后脏页数一般会明显下降不一定为 0取决于刷盘比例与并发写入这组对比能把笔记里那句「COMMIT确保日志落盘但不是日志刷盘的唯一条件也不是脏页刷盘条件」钉死在实验事实上。9.故障恢复视角脏页为什么「敢于」滞后落盘 假设时间线 t1Checkpoint完成ckpt_lsnL1此前脏页已落盘 t2 事务 T 修改页 PREDO 写入并在COMMIT时落盘LSNL2 t3 页 P 仍是脏页尚未写入数据文件 t4 实例崩溃 恢复时 从最近检查点约 L1之后的 REDO 开始重做 重做包含 T 的修改与提交记录 页 P 在内存重建后写回数据库回到崩溃前已提交状态。 若 t3 之前没有把 REDO 落到盘上就刷了页 P崩溃后可能出现「磁盘上有新页、却无法用日志证明/回滚」的不一致——这正是 WAL 禁止的情况。 因此恢复公式可以记为 持久性REDO 已落盘相对COMMIT 一致性窗口缩短检查点推进减少需重做的日志量 可恢复性WAL 顺序永远日志先于数据页10.调优与运维建议面向生产10.1参数取舍 目标 倾向 风险 缩短宕机恢复时间 更积极的检查点更小INTERVAL/更小 RLOG_SIZE/更敏感 DIRTY_PAGES 写 I/O 升高高峰期抖动 压榨峰值吞吐 略放宽检查点保证在线日志容量充足 恢复时间变长日志空间吃紧时仍会被迫猛刷 避免日志等待 扩大在线 REDO、合理 RLOG_SAFE_SPACE、保证检查点跟得上写入 磁盘占用增加 经验原则 先保证日志环不「卡死」再谈吞吐 CKPT_FLUSH_RATE 与 CKPT_FLUSH_PAGES 控制的是「每次刷多少」过小会「检查点很勤但清不干净」过大则单次 I/O 风暴 OLTP 高峰与大批量装载场景参数往往需要两套策略而不是一套吃遍天下。10.2巡检脚本骨架sql-- 一页纸巡检缓冲 日志 检查点参数SELECTBUFFER_DIRTYASITEM,NAME||: dirty||N_DIRTY||, hit||ROUND(RAT_HIT*100,2)||%ASINFOFROMV$BUFFERPOOLWHEREN_DIRTY0UNIONALLSELECTRLOG_SPACE,used_mb||ROUND((TOTAL_SPACE-FREE_SPACE)/1024/1024,2)||, free_mb||ROUND(FREE_SPACE/1024/1024,2)FROMV$RLOGUNIONALLSELECTCKPT_PARA,PARA_NAME||||PARA_VALUEFROMV$DM_INIWHEREPARA_NAMELIKECKPT_%ORDERBY1,2;同时建议结合实例日志中检查点刷盘记录判断业务写压力节奏作为调整 BUFFER 与 CKPT_* 的依据。回到最初那张周期图把全文收束成一条因果链DML└─► 缓冲页变脏进入脏链└─► 生成 REDO进日志缓冲│├─► 多种条件触发 REDO 刷盘缓冲满 / 后台线程 / Checkpoint / COMMIT 等│└─► COMMIT确保本事务 REDO 落盘脏页仍可滞后│▼Checkpoint / 淘汰 / 关闭│ 刷页前再次确认 WAL▼脏页写回数据文件 → 页变干净 → 推进日志 TAIL再记三句不易错的结论COMMIT ≠ 数据页落盘COMMIT ≈ 「该事务的 REDO 已安全」。永远是日志先落盘数据页后落盘未提交事务的脏页也不例外。脏页是性能机制脏读是隔离问题运维盯 N_DIRTY 与 REDO 空间开发盯隔离级别与事务边界。