在 OceanBase 4.x 的物化视图(Materialized View)体系里,有一个不起眼但很关键的定时任务:ObMViewMdsOpTask。它的职责是周期性扫描系统日志流(SYS_LS)上的事务上下文,把仍活跃事务携带的 mview 相关 MDS(Multi-Data Source,表级事务性元数据管理框架)操作收集到内存 Map 中,同时清理那些对应事务已经消失、且记录本身已过期的条目。简单说,它是”刷新任务读取 mds 记录”与”事务真实状态”之间的同步桥梁——没有它,刷新任务可能拿到早已回滚事务的脏数据,内存 Map 也会随时间无限膨胀。
这个任务运行在 rootserver 的 mview 模块中,是 ObMViewMaintenanceService 的配套组件。它继承自定时任务基类,以固定周期注册调度,但真正执行收集动作前还有一层节流判断,整体设计遵循 OB 后台任务一贯的”轻初始化、可重复调度、两阶段清理”风格。
一、任务生命周期:init / start / stop / destroy
生命周期四个接口都非常标准。构造时全部给安全默认值,is_stop_ 初始为 true;init 只做幂等保护并记录当前租户 ID(MTL_ID()),不分配大资源;start 里以 MVIEW_MDS_OP_INTERVAL 周期注册为可重复执行的定时任务,in_sched_ 标志防止重复调度;stop 除了取消调度,还会顺手清空维护服务里的 mview mds op Map——任务停了意味着不再同步事务状态,旧缓存留着只会误导下游。
int ObMViewMdsOpTask::start()
{
int ret = OB_SUCCESS;
if (IS_NOT_INIT) {
ret = OB_NOT_INIT;
LOG_WARN("ObMViewMdsOpTask not init", KR(ret), KP(this));
} else {
is_stop_ = false;
if (!in_sched_ && OB_FAIL(schedule_task(MVIEW_MDS_OP_INTERVAL, true /*repeat*/))) {
LOG_WARN("fail to schedule ObMViewMdsOpTask", KR(ret));
} else {
in_sched_ = true;
}
}
return ret;
}
destroy 与 stop 的区别在于它还会 wait_task() 等待正在执行的任务体退出——保证析构时不会有线程还在跑,这是 OB 后台任务避免 use-after-free 的通用手法。
二、核心流程 update_mview_mds_op:收集 + 清理两步走
真正干活的是 update_mview_mds_op(),分两大步。
步骤一:收集。拿到 SYS_LS 的 LS 句柄,构造事务上下文迭代器,逐个遍历该日志流上的分区事务上下文(ObPartTransCtx),调用 collect_mview_mds_op() 让事务自己上报是否携带 mview 相关的 mds 操作;需要收集的则以 <事务ID, ObMViewOpArg> 写入服务层的 Map,同时记入本轮活跃事务集合 tx_set。每个 tx_ctx 用完必须 revert_tx_ctx 归还(引用计数管理):
transaction::ObPartTransCtx *tx_ctx = nullptr;
while (OB_SUCC(ret)) {
ObMViewOpArg arg;
bool need_collect = false;
if (OB_FAIL(ls_tx_ctx_iter.get_next_tx_ctx(tx_ctx))) {
if (OB_ITER_END == ret) { ret = OB_SUCCESS; break; }
} else if (OB_FAIL(tx_ctx->collect_mview_mds_op(need_collect, arg))) {
LOG_WARN("collect mview mds failed", KR(ret));
} else if (need_collect && OB_FAIL(mview_mds_map.set_refactored(
tx_ctx->get_trans_id(), arg, 1))) {
LOG_WARN("set mview mds failed", KR(ret));
}
if (OB_NOT_NULL(tx_ctx)) {
ls_tx_ctx_iter.revert_tx_ctx(tx_ctx);
tx_ctx = nullptr;
}
}
步骤二:清理。遍历容器时不能边遍历边删,所以用两阶段:CollectNeedDeleteMdsFunctor 仿函数遍历 Map,凡是事务 ID 不在 tx_set 里的(OB_HASH_NOT_EXIST),先收集到待删列表;随后统一处理删除。删除还有一个时间保险——只有当前时间超过 arg.start_ts_ + MVIEW_MDS_OP_EXPIRE_TIME 才真正 erase,即”事务不在了且记录也已过期”双条件,避免竞态下误删仍在使用的记录:
} else if (ObTimeUtil::current_time() >= arg.start_ts_ + MVIEW_MDS_OP_EXPIRE_TIME) {
if (OB_FAIL(mview_mds_map.erase_refactored(del_tx_id.at(idx)))) {
LOG_WARN("erase tx_id failed", KR(ret), K(del_tx_id.at(idx)));
}
}
整轮结束会打印耗时与统计信息(活跃事务数、待删数),方便线上排查”mview mds 记录为什么还在/为什么被清掉”这类问题。
三、runTimerTask 的双重节流设计
定时回调 runTimerTask 并不是每次到点都执行,而是带两重节流判断,任一满足才真正干活:
void ObMViewMdsOpTask::runTimerTask()
{
...
if (last_sched_ts_ <= MTL(ObMViewMaintenanceService*)->get_mview_mds_ts()) {
need_sched = true; // 有新数据需要立刻同步
} else if (curr_ts - last_sched_ts_ > 60 * 1000 * 1000) {
need_sched = true; // 兜底周期:60 秒
}
...
}
第一个条件是事件驱动:服务层的 mview_mds_ts 被别处(如新注册的刷新任务)更新过,说明有新数据,立即同步;第二个条件是 60 秒兜底周期,保证 Map 与事务状态最终一致。这是”事件驱动 + 周期兜底”的经典组合——有变化快刷,没变化慢刷,既不浪费 CPU 也不落后于业务。
小结
ObMViewMdsOpTask 全文不足 200 行,却体现了 OB 后台任务的几个典型设计模式:轻初始化 + 幂等保护、引用计数管理的迭代器遍历、先收集后删除的两阶段清理、双条件删除保险,以及事件驱动与周期兜底结合的节流策略。配合前几篇讲过的刷新任务、MLog 维护任务、快照清洁任务,物化视图后台任务家族的拼图又完整了一块。