Oracle 在线重定义卡了一个多小时:查不到阻塞会话

昨天一位朋友碰到个问题:用 DBMS_REDEFINITION 把一张表在线改成分区表,前面几步都很顺,到了最后一步 finish_redef_table,会话卡在 WAIT FOR TABLE LOCK 不动了。

业务会话全杀了,索引也确认复制齐了,vsession里查不到阻塞者,vsession 里查不到阻塞者,vsession里查不到阻塞者,vaccess 里也没人占着这张表。就这么耗了一个多小时,差点直接 abort。后来翻到一篇 MOS 文档,才查到 SYS.MLOG$ 里还有一条没清掉的 PURGE_JOB 记录。

把这次的排查过程记下来,也顺带整理几个做在线重定义时容易碰到的问题。

FINISH 会做哪些操作

finish_redef_table 卡住后,我们先查了读写源表的会话。要继续往下排查,还得看 FINISH 内部的操作顺序:

  1. 调用一次 SYNC_INTERIM_TABLE,同步增量数据;
  2. 锁定源表,此后源表数据不再变化,这是整个流程里唯一的锁表窗口;
  3. 再同步一次增量数据;
  4. 交换源表与中间表的表名(数据字典层面的指针切换);
  5. 删除物化视图及物化视图日志。

第 2 步要拿源表的独占锁,拿不到就一直等,等待事件就是 WAIT FOR TABLE LOCK。

坑一:找不到持有者的表锁,MLOG$ 里残留的 PURGE_JOB

现象: FINISH 卡在 WAIT FOR TABLE LOCK,vsession.blockingsession为空,vsession.blocking_session 为空,vsession.blockingsession为空,vaccess 查不到任何会话占用这张表,杀光业务会话也没用。

原因: 在线重定义的增量同步靠的是物化视图日志(MLOG)。朋友这个库前一天厂商做数据同步时卡住了,DBA顺手kill掉了一个清理物化视图日志的purgejob,结果在SYS.MLOG)。朋友这个库前一天厂商做数据同步时卡住了,DBA 顺手 kill 掉了一个清理物化视图日志的 purge job,结果在 SYS.MLOG)。朋友这个库前一天厂商做数据同步时卡住了,DBA顺手kill掉了一个清理物化视图日志的purgejob,结果在SYS.MLOG 的 PURGE_JOB 字段里留下了一条残留记录。

FINISH 拿独占锁之前会检查 MLOG 上的 PURGE_JOB。字段非空,Oracle 就认为还有一个物化视图日志清理任务在用这张表,于是继续等待。那个 job 已经被 kill,vsession 里没有对应会话,查会话也就找不到阻塞者。

排查: 需要 SYSDBA 权限,MLOG$ 是基表。

sql 复制代码
select mowner, master, purge_job from SYS.MLOG$ where purge_job is not null;

查到 PURGE_JOB 非空的记录后,要确认它对应哪张表,相关任务是否还在运行。这次确认任务已经结束,才按残留记录处理。查表锁时,可以先看 v$locked_object、dba_dml_locks、dba_waiters。

解决:

注意:下面第 2 步直接改数据字典基表,不在 Oracle 常规支持范围内。生产库建议在 Oracle Support 指导下操作,动手前做好备份,而且只改排查时查到的那一行,不要整表 update。

sql 复制代码
-- 1. 先中断本次重定义
exec dbms_redefinition.abort_redef_table('SCHEMA','SRC_TABLE','INTERIM_TABLE');

-- 2. 只清掉排查中确认的那条残留记录
update SYS.MLOG$
   set purge_job = null
 where mowner = 'OWNER_FOUND'     -- 替换为排查结果中的 mowner
   and master = 'MASTER_FOUND';   -- 替换为排查结果中的 master
-- 确认只更新了预期的行数,再提交
commit;

-- 3. 重新走一遍重定义流程,FINISH 即可顺利完成

参考: My Oracle Support 文章 KB145797,标题是《DBMS_REDEFINITION.FINISH_REDEF_TABLE hangs on this event "WAIT FOR TABLE LOCK"》。文档记录了 PURGE_JOB 非空、清理后重跑成功的案例,还提到类似问题的 Bug 24554467(FINISH_REDEF_TABLE IS HANGING),状态为 closed as not a bug。文档没有说明该 Bug 的关闭理由。

预防: 开工前先查一遍 SYS.MLOG$;生产库上 kill 过物化视图日志相关的 job,事后记得检查有没有残留。12.2 及以上版本,finish_redef_table 支持 dml_lock_timeout 参数,等待拿锁超过设定时间后会返回:

sql 复制代码
exec dbms_redefinition.finish_redef_table('SCHEMA','SRC_TABLE','INTERIM_TABLE', dml_lock_timeout => 300);

坑二:表没有主键,can_redef_table 直接报错

在线重定义默认要求表有主键,用来关联源表和中间表的行。没有主键,can_redef_table 会直接报不支持。

可以先给表补上主键,或者改用 ROWID 方式(DBMS_REDEFINITION.CONS_USE_ROWID)。ROWID 方式不能用于索引组织表(IOT),还会在新表上加一个隐藏列 M_ROW$$。老版本需要完成后手动 set unused 或删除,新版本由 FINISH 自动置为 unused。完成后查一下 dba_unused_col_tabs 确认。

重定义后会使用新段,两种方式都要考虑 ROWID 变化。下游应用要是存了 ROWID,两种方式都会出问题。生产环境优先用主键方式。

坑三:中间表结构对不上,START 失败

不传 col_mapping 时,start_redef_table 按列名匹配源表和中间表,列顺序不同没关系。容易出问题的是这几种:列名写错或大小写不一致、数据类型不兼容、中间表新增列带了 NOT NULL 约束。分区改造时中间表是手写 DDL 建的,要留意这几处。

中间表建好后,用 dba_tab_columns 按列名把列名、类型、长度、是否可空逐列比一遍,再执行 start,别靠肉眼看。

坑四:漏了 COPY_TABLE_DEPENDENTS,索引、触发器、授权没过来

群里有人接着问:"索引有没有漏?"copy_table_dependents 负责把源表的索引、触发器、约束、授权复制到中间表,它有个 num_errors 出参,很多人调完就不管了。

示例:

sql 复制代码
DECLARE
  num_errors PLS_INTEGER;
BEGIN
  DBMS_REDEFINITION.COPY_TABLE_DEPENDENTS(
    uname            => 'SCHEMA',
    orig_table       => 'SRC_TABLE',
    int_table        => 'INTERIM_TABLE',
    copy_indexes     => DBMS_REDEFINITION.CONS_ORIG_PARAMS,
    copy_triggers    => TRUE,
    copy_constraints => TRUE,
    copy_privileges  => TRUE,
    ignore_errors    => FALSE,
    num_errors       => num_errors,
    copy_statistics  => TRUE);
  DBMS_OUTPUT.PUT_LINE('errors=' || num_errors);
END;
/

调完看一下 num_errors,再查 dba_redefinition_errors 确认。漏了索引,切换后原来走索引的业务 SQL 可能改走全表扫描。

坑五:空间没评估,跑到一半表空间或归档满了

start_redef_table 的初始装载是直接路径插入(INSERT 带 APPEND 提示),表数据本身几乎不产生 undo。不能按表的大小直接估算 undo 用量。开工前要评估这四项:

  1. 目标表空间:中间表要再装一份完整数据,至少预留和源表差不多大的空间,加了列还要更多;
  2. redo 和归档:库开了 FORCE LOGGING(有 Data Guard 基本都开)的话,全量装载产生的 redo 和数据量相当,归档目录要留足;
  3. 临时表空间:copy_table_dependents 建索引时排序会用到,要估算大表建索引需要的排序空间;
  4. undo:主要来自重定义期间的业务 DML 和索引维护,一般不是瓶颈,长事务多的库要留意。

中途空间不足,可能需要 abort 清理后重跑。

开工前把这四项逐一算清楚,执行过程中盯着表空间使用率、归档目录和 dba_temp_free_space,大表放在维护窗口做。

坑六:FINISH 赶上业务高峰,短暂锁表变成业务中断

最后切换阶段的锁表时间,跟待同步的增量数据量有关。如果 START 到 FINISH 之间隔了几个小时,增量很大,第 1 步同步就要跑很久;第 2 步的独占锁一旦拿到,所有业务 DML 都会被堵住。

我建议把 FINISH 放在业务低峰期,执行前先手动跑一次 sync_interim_table,减少切换时需要同步的增量。12.2 及以上版本再带上 dml_lock_timeout,限制等锁时间。具体要锁多久,得在自己的库上测。

坑七:收尾不收集统计信息,执行计划跑偏

切换完成后,新表的统计信息可能是空的,也可能是旧的。就算坑四里带了 copy_statistics => TRUE,分区改造后表结构变了,原来的统计信息也不一定适用。CBO 使用这些统计信息估算行数,估错了就可能选错执行计划。

FINISH 后收集统计信息:

sql 复制代码
exec DBMS_STATS.GATHER_TABLE_STATS( -
  OWNNAME          => 'SCHEMA', -
  TABNAME          => 'SRC_TABLE', -
  ESTIMATE_PERCENT => DBMS_STATS.AUTO_SAMPLE_SIZE, -
  METHOD_OPT       => 'for all columns size repeat', -
  DEGREE           => 8, -
  GRANULARITY      => 'ALL', -
  CASCADE          => TRUE);

这里用 GRANULARITY => 'ALL' 收集全局和分区级统计信息。采样率用 AUTO_SAMPLE_SIZE,大分区表手工指定 1% 这种低采样率,NDV 这类统计很容易失真。

操作检查清单

开工前:

  • can_redef_table 校验通过(主键就绪,或确认走 ROWID 方式)
  • 中间表与源表按列名逐列比对一致
  • 评估目标表空间、redo/归档、临时表空间、undo
  • 查 SYS.MLOG$ 无残留 PURGE_JOB
  • 选定业务低峰窗口

执行中:

  • copy_table_dependents 后检查 num_errors 和 dba_redefinition_errors
  • FINISH 前手动 sync_interim_table 压增量
  • 12.2+ 的 FINISH 带上 dml_lock_timeout

收尾:

  • 检查约束状态,复制过来的约束可能是 NOVALIDATE,业务允许时再 enable validate
  • ROWID 方式确认 M_ROW$$ 已 unused
  • 收集统计信息(含分区级)
  • 抽查业务 SQL 的执行计划

这次清掉 PURGE_JOB 残留后,朋友重新跑了一遍重定义,FINISH 通过了。前面一个多小时都花在查会话、杀会话上,直到查 SYS.MLOG$ 才定位到原因。以后再碰到 WAIT FOR TABLE LOCK、又查不到阻塞会话,我会接着检查物化视图日志相关任务有没有留下记录。

参考:My Oracle Support KB145797;Bug 24554467(closed as not a bug)。

相关推荐
꯭自꯭闭꯭1 小时前
达梦守护集群手工切换及故障切换
linux·运维·服务器·数据库
j7~1 小时前
【Redis初阶】(篇二)《一文吃透 Redis:特性、应用场景、版本演进与安装配置全解析》
数据库·redis·缓存·redis的特性·redis的主要应用场景·redis重要文件及其作用·安装并启动redis
Elastic 中国社区官方博客1 小时前
使用 NVIDIA cuVS 在 Elasticsearch 中实现 GPU 加速的向量索引:在 10 分钟内处理 1.38 亿个向量
大数据·数据库·elasticsearch·搜索引擎·全文检索·安全威胁分析
我们从未走散2 小时前
AI 生图平台如何避免资损:用“扣款与凭据同事务”构建三态账本
开发语言·数据库·php
Shadow(⊙o⊙)2 小时前
MySQL复合查询
数据库·mysql
L·S·P8 小时前
25MB 管理 100+ 种数据库:开源轻量客户端 DBX 介绍与上手
数据库·mysql·开源·database·dbx
笃行35010 小时前
KingbaseES KSQL Developer 的连接管理、数据处理与对象管理
数据库
ha_lydms10 小时前
MaxCompute中加密与解密函数
大数据·数据库·数据仓库·hadoop·dataworks·maxcompute·odps
Shadow(⊙o⊙)10 小时前
MySQL内置函数
数据库·mysql