物化视图的快速刷新(FAST REFRESH)依赖一张”变更日志表”——MLog(materialized view log)。基表每发生一次 DML,MLog 就追加一行带旧值/新值的变更记录。只写不删,这张表迟早会吃掉整个租户的存储空间,所以必须有人负责回收。OceanBase 4.3 里承担这个职责的是 storage/mview/ob_mlog_purge.cpp 中的 ObMLogPurger。这篇文章拆解它的三段式流程,以及最核心的那个问题:一次到底能删到哪个位点。
一、架构位置与三段式流程
调用方是 rootserver 一侧的物化视图后台任务(MLog 维护任务、日志裁剪任务),它们按阈值或按需触发;真正”怎么删”的逻辑全部收敛在 ObMLogPurger 里,最终通过内部事务 ObMViewTransaction 落到 SQL 层。
rootserver/mview 后台任务
↓ 按阈值 / 按需调度
ObMLogPurger::purge() ← 总编排
↓ trans_(内部表事务封装)
DELETE FROM __mv_log_xxx WHERE ... (purge_scn 之前的行)
↓
更新 __all_mlog:last_purge_scn / date / rows / trace_id
整个类只有四个成员函数,职责切得很干净:
init():只做入参校验和浅拷贝,不干重活(典型的两阶段构造)。get_and_check_mlog_database_schema():顺着基表找到所在 database,因为内部事务必须绑定 db 上下文。prepare_for_purge():加锁、算水位线、拼 SQL —— 全文件最烧脑的一段。do_purge():执行 DELETE、回写元信息、清优化器缓存。
二、核心数据结构
三个关键对象串起了整个流程:
ObMLogPurgeParam 输入参数(tenant_id_、master_table_id_、purge_log_parallel_)
ObMLogInfo 日志元信息(mlog_id、last_purge_scn、data_sync_scn)
ObMViewTransaction 内部表专用事务,写内部表必须裹守卫宏
purge_scn_ 本次清理的安全水位线
need_purge_ 提前退出的短路标志,避免空转事务
其中 purge_scn_ 是整个设计的题眼,下一节专门讲。need_purge_ 则是一个务实的优化:没有新日志可清时,连 MDS 注册和 DELETE 都跳过,省掉一次内部表写入。
三、purge_scn 怎么算:取所有下游 MV 的最小水位
这里是唯一真正需要权衡的地方。删得太狠,会删掉还没被物化视图消费的日志,导致快速刷新失败、退化成全量刷新;删得太保守,日志就会无限堆积。OceanBase 的答案是:
purge_scn = MIN(每个依赖该基表的物化视图的 refresh_scn)
但不是所有物化视图都参与这个最小值计算。代码里有一组很精确的筛选条件:
// 只挑"真的会读 MLog"的物化视图参与水位计算
} else if (mview_info.get_refresh_method() == ObMVRefreshMethod::FAST
|| mview_info.get_refresh_method() == ObMVRefreshMethod::FORCE
|| table_schema->mv_on_query_computation()) {
// for fast refresh mv or real-query mv
if (mview_info.get_is_synced()) {
// 已同步:日志已投递到 MV 侧,可用更激进的 data_sync_scn
min_mview_refresh_scn = MIN(min_mview_refresh_scn, mview_info.get_data_sync_scn());
} else {
// 未同步:只能用保守的 last_refresh_scn,慢一点但绝不误删
min_mview_refresh_scn = MIN(min_mview_refresh_scn, mview_info.get_last_refresh_scn());
}
}
三种情况被排除在外:COMPLETE 刷新方式的物化视图每次全量重算、根本不读日志;last_refresh_scn 为 0 或无效说明这张 MV 从未刷新过,若让它参与计算会把水位压到 0、导致永远清不掉;元信息缺失(很可能正在被 drop)时直接把错误吞掉,当作”无此依赖”。
还有一处细节值得留意:is_synced 为真时用 data_sync_scn(日志已投递到的位点),为假时退回 last_refresh_scn(刷新真正完成的位置)。前者更激进、吞吐更好,后者更保守、绝对安全,代码选择了”状态允许才激进”。
拿到最小值之后,决定 need_purge_ 的逻辑只有三行味道:
if (min_mview_refresh_scn != UINT64_MAX) {
if (min_mview_refresh_scn <= mlog_info_.get_last_purge_scn()) {
need_purge_ = false; // 上次已清到这个位置,本次无事可做
} else if (OB_FAIL(purge_scn_.convert_for_inner_table_field(min_mview_refresh_scn))) {
...
} else {
need_purge_ = true; // 有新增的可回收区间
}
} else {
purge_scn_ = current_scn; // 没有任何 MV 依赖 → 全量回收
need_purge_ = true;
}
那个 convert_for_inner_table_field 是容易踩坑的地方:SCN 的”事务视图值”和”内部表字段值”是两套编码,跨边界必须显式转换,否则会写出一个语义错乱的 SCN。
四、并发与事务的边界
purge() 里事务的开启位置很有讲究——它在 prepare_for_purge() 之前就启动,原因在 prepare 里:fetch_mlog_info(..., true /*for_update*/) 会对 __all_mlog 中那行元信息加行锁。这样两个并发的 purge 请求会在这把行锁上串行化,不会同时去删同一张日志表。
另一个顺序细节是”先注册 MDS、再 do_purge”。MDS 是物化视图侧的元数据操作日志,先把 MVIEW_OP_TYPE::PURGE_MLOG 的意图登记下来,再去真正删数据,便于下游感知”这些日志已经被清过了”。
事务收尾用了统一的一段:
if (trans_.is_started()) {
int tmp_ret = OB_SUCCESS;
if (OB_SUCCESS != (tmp_ret = trans_.end(OB_SUCC(ret)))) {
LOG_WARN("failed to commit trans", KR(ret), KR(tmp_ret));
ret = COVER_SUCC(tmp_ret); // 前面已失败时,不让提交错误覆盖原始错误码
}
}
COVER_SUCC 是 OceanBase 里非常常见的错误码优先级惯用法:只有当 ret 还是成功时才用新错误覆盖它,保证调用方看到的是最先发生的根因。
五、落地三件事与可观测性
do_purge() 做三件事,任何一件失败都整体回滚:
- 执行 DELETE:
trans_.write()直接执行拼好的 SQL 文本,拿到affected_rows。之所以用文本而非预编译,是因为表名和并行度都是运行期动态的。 - 回写元信息:把
last_purge_scn、last_purge_date、last_purge_time(纳秒转毫秒要除两次 1000)、last_purge_rows、last_purge_trace_id一并写回__all_mlog。这是一份相当完整的”清理账本”,DBA 靠它就能判断 GC 是否健康、有没有积压。 - 清动态采样缓存:日志被大量删除后,优化器手里基于旧数据采样出的统计(
ObOptDSStat)已经失效,必须显式 erase,否则可能选出坏计划。这一步刻意放在事务外——即使事务回滚,多清一次缓存最多多采一次样,代价极小。
此外所有写内部表的代码都被 WITH_MVIEW_TRANS_INNER_MYSQL_GUARD 包住。这个守卫宏把事务切到 inner-table mysql 模式,让内部表操作绕开用户态的审计、权限和会话变量路径——在 OceanBase 里,只要碰 __all_mlog 这类内部表,这个宏基本是标配。
六、小结
ob_mlog_purge.cpp 只有 300 多行,但它把一件事做得很完整:用一种保守而精确的水位线,在”日志不能误删”和”日志必须回收”之间取平衡。值得记住的几个设计点:
- 水位线的计算严格按”谁会读 MLog”来筛选,COMPLETE 刷新的 MV 完全不参与;
- 同步状态决定用激进还是保守的 SCN,让安全性和吞吐都各得其所;
- 行锁 + 单事务把 DELETE 与元信息回写绑成原子操作,崩溃不会留下”账实不符”;
- 清理完顺手让优化器的采样缓存失效,把”数据变了但统计没变”的隐患一并消掉。
理解了 purge_scn 的算法,再回头看物化视图的整条刷新链路——刷新器负责推进水位,清理器负责回收水位以下的空间——两个角色正好咬合成一个闭环。这也是 OceanBase 4.3 物化视图实现的骨架所在。