在 OceanBase 4.3 的物化视图(MV)体系里,”大版本刷新”(major refresh mv)是一种重量级操作:它不是在事务里实时计算刷新,而是依赖存储层的多版本快照,把刷新推迟到一次 Major Compaction 中批量完成。这就带来一个关键问题——刷新期间所依赖的最低快照版本(min_refresh_scn)必须被系统”保住”,否则事务的多版本数据一旦被回收,刷新就会读到不完整的数据。本文要解读的 ob_mview_push_snapshot_task.cpp(209 行)正是解决这个问题的组件:一个由 RootService 定时驱动的”快照推进任务” ObMViewPushSnapshotTask,它周期性地把所有大版本刷新物化视图所需的最小快照 SCN 推送到快照表,为刷新链路兜底。
架构位置:MV 后台任务家族的一员
ObMViewPushSnapshotTask 位于 rootserver/mview 目录下,与我们之前解读过的几个任务构成一个完整的后台调度体系:
- ObMViewTimerTask:物化视图定时刷新的统一调度入口,按 refresh_interval 触发各 MV 的刷新;
- ObMViewCleanSnapshotTask:清理已不再需要的 MV 快照版本;
- ObMViewPushSnapshotTask(本文):反向操作——把刷新依赖的最小快照”推进”下去,保证它不被回收;
- ObMViewUpdateDepsTask / ObMviewUpdateCacheTask / ObMViewMdsOpTask:分别负责依赖关系维护、刷新信息缓存与 MDS 操作记录同步。
一个”推”、一个”清”,两者共同管理快照版本的生命周期:推进任务保证刷新依赖的版本不被 GC,清洁任务在刷新完成后释放空间。
生命周期:标准的 init / start / stop / destroy 四件套
ObMViewPushSnapshotTask 遵循 OceanBase 后台任务的经典生命周期模式。构造函数里 is_stop_ 初始为 true,任务未启动前不会执行任何业务逻辑;init() 只做幂等检查并记录当前租户 ID(通过 MTL_ID() 获取,这是租户级单例的标准获取方式):
int ObMViewPushSnapshotTask::init()
{
int ret = OB_SUCCESS;
if (IS_INIT) {
ret = OB_INIT_TWICE;
LOG_WARN("ObMViewPushSnapshotTask init twice", KR(ret), KP(this));
} else {
tenant_id_ = MTL_ID();
is_inited_ = true;
}
return ret;
}
start() 则把任务注册进定时器,第二个参数 true 表示周期性重复执行,间隔由 MVIEW_PUSH_SNAPSHOT_INTERVAL 宏定义;in_sched_ 标志防止重复注册。而 destroy() 在撤销调度后还要调用 wait_task() 等待正在执行的一轮跑完,避免析构后定时器线程仍在访问成员变量——这是后台任务实现里最容易踩的坑,OceanBase 的统一封装把它处理得很干净。
核心流程:runTimerTask 的四个步骤
定时器每个周期调用一次 runTimerTask(),这是整个文件的核心。流程可以概括为四步:
void ObMViewPushSnapshotTask::runTimerTask()
{
// ... 前置检查略 ...
} else if (OB_FAIL(trans.start(sql_proxy, tenant_id_))) {
LOG_WARN("fail to start trans", KR(ret), K(tenant_id_));
} else {
share::ObGlobalStatProxy stat_proxy(trans, tenant_id_);
share::SCN major_refresh_mv_merge_scn;
const int64_t snapshot_for_tx = mgr->get_min_reserved_snapshot_for_tx();
share::SCN min_refresh_scn;
const bool select_for_update = true;
// 1) select for update 锁住全局的 major_refresh_mv_merge_scn
if (OB_FAIL(stat_proxy.get_major_refresh_mv_merge_scn(select_for_update,
major_refresh_mv_merge_scn))) {
// ...
}
// 2) 计算所有 major refresh mview 的最小刷新 SCN
else if (OB_FAIL(ObMViewInfo::get_min_major_refresh_mview_scn(
trans, tenant_id_, snapshot_for_tx, min_refresh_scn))) {
if (OB_ERR_NULL_VALUE == ret) {
ret = OB_SUCCESS; // 租户没有 major refresh mview,正常路径
}
}
// 3) 排除备份作业冲突
else if (OB_FAIL(share::ObBackupJobOperator::get_jobs(...))) {
// ...
}
// 4) 推送快照
else if (OB_FAIL(snapshot_proxy.push_snapshot_for_major_refresh_mv(trans, tenant_id_,
min_refresh_scn))) {
// ...
}
// 事务收尾:无论成败都要 end()
if (trans.is_started()) {
int tmp_ret = OB_SUCCESS;
if (OB_SUCCESS != (tmp_ret = trans.end(OB_SUCC(ret)))) {
ret = OB_SUCC(ret) ? tmp_ret : ret;
}
}
}
}
有几个细节值得展开:
第一,为什么整个过程要包在一个事务里? “读取合并 SCN → 计算最小刷新 SCN → 推送快照”这三步必须原子完成。如果不加事务保护,两个周期(或两个节点)可能并发推进快照,产生竞态。而事务里的 select_for_update = true 更是双重保险:它直接锁住 __all_core_table 里的 major_refresh_mv_merge_scn 记录,与备份流程形成互斥——备份操作也会检查并推进这个 SCN,双方通过行锁保证串行化。
第二,snapshot_for_tx 的自洽设计。 读取 __all_mview 时用的是事务级最低保留快照 get_min_reserved_snapshot_for_tx(),而不是直接用当前时间戳。注释里写得很清楚:这是为了确保并发查询不会返回 OB_SNAPSHOT_DISCARDED 错误——如果用一个可能已被 GC 的快照版本去做快照读,查询本身就可能失败。先拿到系统保证保留的最低快照,再用它做读基准,这是多版本回收与快照推进之间典型的自洽设计。
第三,错误码的精细处理。 get_min_major_refresh_mview_scn 返回 OB_ERR_NULL_VALUE 时表示租户下没有任何大版本刷新物化视图,这是正常路径而非错误,所以代码把它”吞掉”重置为成功。事务收尾处 tmp_ret 的处理也很讲究:只有当主流程成功而提交失败时才覆盖 ret,避免”提交失败”这个次生错误抹掉更早的真实错误码。
与备份作业的空间博弈:check_space_occupy_
如果租户正在执行备份作业,是否还要推进快照?这个任务给出了一个有趣的三分支决策:
} else if (!backup_jobs.empty() && OB_FAIL(check_space_occupy_(space_danger))) {
LOG_WARN("backup jobs exist, check space occupy failed", KR(ret), K(tenant_id_));
} else if (!backup_jobs.empty() && !space_danger) {
LOG_INFO("backup jobs exist, space is not in danger just skip push snapshot", ...);
} else if (OB_FAIL(snapshot_proxy.push_snapshot_for_major_refresh_mv(...))) {
// 推送快照
}
决策逻辑是:没有备份作业 → 直接推进;有备份作业且磁盘空间不紧张 → 跳过本次推进,避免和备份抢 IO;有备份作业且空间紧张 → 反而要推进。最后这个分支看起来违反直觉,实则合理:空间紧张意味着多版本数据堆积已经到了危险边缘,此时保住 MV 刷新链路(让 Compaction 尽快推进、回收旧版本)优先级高于备份体验,否则可能陷入”空间不够 → 备份/合并更慢 → 版本堆积更多”的恶性循环。
check_space_occupy_ 的实现也很直观:查询 gv$ob_servers 视图,取所有服务器上 DATA_DISK_IN_USE / DATA_DISK_ALLOCATED 的最大占比,超过租户配置 _datafile_usage_upper_bound_percentage 则判定空间危险。此外文件里还留了一个 ERRSIM 故障注入点,可以在测试中模拟空间检查失败,验证快照推进在极端情况下的行为。
小结
ob_mview_push_snapshot_task.cpp 是一个只有 209 行的小文件,但它串起了 OceanBase MV 大版本刷新链路里最关键的一环:快照版本的生命周期管理。它的设计有三个值得借鉴的点:一是把”读 SCN、算最小值、推快照”包在单个事务里并用 select for update 与备份流程互斥,用最朴素的方式消灭了并发竞态;二是用事务级保留快照做读基准,避免了”为了推快照而读快照、结果快照已被回收”的自指困境;三是空间检查与备份作业的三分支决策,体现了存储引擎里”保链路优先于保体验”的取舍思路。至此,rootserver/mview 目录下的后台任务家族(timer / clean_snapshot / mds_op / update_deps / update_cache / push_snapshot)已全部解读完毕,物化视图后台调度体系的拼图基本完整。