Oracle LogMiner 数据迁移实战

LogMiner 是什么

LogMiner 是Oracle官方提供的工具,可以解析 Redo log 和 Archived Redo log

LogMiner 可以做什么?

官方文档中列举了很多,大家可以自己去看下。

我们目前的项目在使用基于LogMiner 的 Debezium Oracle Connector 做数据迁移

Oracle LogMiner 数据迁移的原理是什么?

首先需要了解几个概念,这里简单介绍下

  • Redo log:Redo中记录了所有对数据块的更改,Oralce 要求至少有两个以上的Redo Log Group
  • Archived Redo log:当一个Redo Log 写满之后,会发生日志切换,数据的更改会记录到下一个Redo Log中(所以一定要有两个以上的Redo)。如果开启了归档模式,Oracle 会将写满的Redo Log 归档。
  • SCN (System Change Number):Oracle 内部逻辑时间戳
  • Flashback:通过闪回查询 SELECT ... AS OF SCN 可以查询Oracle某个时间点的全量数据

思路如下:

  1. 首先查询出一下当前的SCN
  2. 根据SCN 查询出这一时刻的全量数据
  3. 通过Logminer 指定Start_SCN,获取增量数据

安装与配置

想尝试却不太熟悉Oracle的同学,可以参考一下我整理的文档

小试牛刀

在准备好了环境之后,我们来开箱体验一下Logminer

logminer 用户登录 conn c##logminer/password

1. 构建数据字典

LogMiner使用数据字典将内部对象标识符和数据类型转换为正常字段和数据格式

1
2
3
4
5
# 这是一条常规的SQL
INSERT INTO HR.JOBS(JOB_ID, JOB_TITLE, MIN_SALARY, MAX_SALARY) VALUES('IT_WT','Technical Writer', 4000, 11000);
# 如果没有数据字典,数据会是这样的
insert into "UNKNOWN"."OBJ# 45522"("COL 1","COL 2","COL 3","COL 4")
values (HEXTORAW('45465f4748'),HEXTORAW('546563686e6963616c20577269746572'),HEXTORAW('c229'),HEXTORAW('c3020b'));

官方文档中提到三种方式:

  • 在线数据字典:当你可以访问创建Redo的源数据库并且表结构不会发生任何变动时。可以考虑使用在线数据字典。这是最简单有效的,也是Oracle的推荐选项
    由于在线数据字典永远存储的是最新的结构。如果发生了表结构变动,Logminer 捕获到旧版本的数据,SQL将会如上述代码块中那样
  • 提取数据字典到redo中:
    需要执行命令BEGIN DBMS_LOGMNR_D.BUILD (options => DBMS_LOGMNR_D.STORE_IN_REDO_LOGS); END;
    该操作会占用一定数据库资源
  • 提取数据字典到Flat File:Oracle维护该选项是为了兼容历史版本,本文并没有使用到该方式,不多做介绍

LogMiner 在启动时会通过指定的数据字典选项维护一个内部数据字典,当启动LogMiner时指定 DBMS_LOGMNR.DDL_DICT_TRACKING,LogMiner会自动捕获DDL来更新内部字典,这样即使发生了表结构变动时,也可以正确的解析DDL。注意:该选项不能和在线数据字典同时使用
更多解释参考Oracle文档:https://docs.oracle.com/en/database/oracle/oracle-database/12.2/sutil/oracle-logminer-utility.html#GUID-56743517-A0C0-4CCD-9D20-2883AFB5683B

这一步我选择在线数据字典,什么都不用做,直接进入下一步

2. 添加日志文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 查询目前的redol og
SQL> select member from v$logfile;

MEMBER
--------------------------------------------------------------------------------
/opt/oracle/oradata/ORCLCDB/redo03.log
/opt/oracle/oradata/ORCLCDB/redo02.log
/opt/oracle/oradata/ORCLCDB/redo01.log

# 添加redo log
SQL> EXECUTE DBMS_LOGMNR.ADD_LOGFILE( -
LOGFILENAME => '/opt/oracle/oradata/ORCLCDB/redo03.log', -
OPTIONS => DBMS_LOGMNR.NEW);
EXECUTE DBMS_LOGMNR.ADD_LOGFILE( -
LOGFILENAME => '/opt/oracle/oradata/ORCLCDB/redo02.log', -
OPTIONS => DBMS_LOGMNR.ADDFILE);
EXECUTE DBMS_LOGMNR.ADD_LOGFILE( -
LOGFILENAME => '/opt/oracle/oradata/ORCLCDB/redo01.log', -
OPTIONS => DBMS_LOGMNR.ADDFILE);

3. START_LOGMNR

1
2
3
# 使用在线数据字典进行log解析
SQL> EXECUTE DBMS_LOGMNR.START_LOGMNR(-
OPTIONS => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG);

然后执行一条INSERT

4. 查询结果

通过查询V$LOGMNR_CONTENTS 获取LogMiner捕获的结果。当执行该视图查询时,LogMiner会按照顺序解析Redo和Archived Log,所有执行时间会有一点慢

1
2
3
SELECT OPERATION, SQL_REDO, SQL_UNDO
FROM V$LOGMNR_CONTENTS
WHERE table_name='TEST_TAB';

结果如下,可以看到我们刚刚INSERT的SQL

实战

我们已经知道了迁移的思路和Logminer如何使用,现在可以动手搞一个demo了。

由于篇幅问题,这里我只讨论思路和我的一些想法。

完整代码参考👉 https://github.com/TavenYin/database-cdc/tree/master/oracle-logminer

1. 整体思路

相关实现思路参考自Debezium

image.png

需要解释一下第四步为什么,发生Redo发生切换时,需要重启Logminer流程,两点原因

  • Redo Log 切换后,会生成新的归档,我们需要Add新的归档日志
  • 长时间开启LogMiner会话,会导致PGA使用量一直上升无法释放,End LogMiner 可以解决这个问题。所以代码逻辑中需要找一个时机去重启LogMiner,而Redo 切换这个时间点确实也挺合适的。

写到这的时候,我突然有了一个疑问

我们刚刚已经说过了,只有在查询 V$LOGMNR_CONTENTS 时,LogMiner才会去解析Redo Log,然后动态的生成视图。

参考上图。如果在第四步和第六步之间,程序检查到没有RedoLog切换准备继续执行。突然插入了大量数据导致Current Redo Log 被覆盖(注意必须是已经被覆盖而不是切换)了,此时是不是我们再查询 V$LOGMNR_CONTENTS 岂不是会丢失一部分数据?

由于start_logminer时会指定,起始和结束SCN,所以即使下次执行时添加了新的Archived Log,由于SCN已经被跨过去了,所以一定不会读这部分数据

在我做了测试之后发现,如果情况真的如此极端,确实会这样。

那么Debezium为什么没有考虑这个问题呢?

个人理解,在生产环境通常Redo Log 不会频繁切换,并且一定会有多个Redo Group。这么短时间内被覆盖的情况几乎不可能发生。


2. 处理 V$LOGMNR_CONTENTS 结果集

最开始在看Debezium源码的时候,没仔细注意这个地方,在自己动手搞一遍之后,发现这个地方的逻辑有点麻烦

V$LOGMNR_CONTENTS 每一行可能是事务的提交、回滚,DDL,DML

上面提到了一个 TransactionalBuffer是什么?

我们在读取 V$LOGMNR_CONTENTS 会发生如下图的情况,因为每次只从startScn 读取到 当前Scn。而这中间可能发生的情况是,事务并没有Commit,但是我们拿到了其中一部分的DML,我们并不能确定这些DML是不是要Commit,所以需要将这些“一半”的事务暂时缓存在内存中

其实在调用 DBMS_LOGMNR.START_LOGMNR 时,可以指定一个选项 COMMITTED_DATA_ONLY,仅读出已提交的事务。这样就不必要这么麻烦的处理结果集了。但是为什么不选择 COMMITTED_DATA_ONLY?使用该策略会一直等待事务提交才会响应客户端,这很容易造成 “Out of Memory”,所以这个策略不适合我们的程序。


3. 迁移进程宕机处理

数据迁移必定是一个漫长的过程,如果在执行中遇到什么意外,导致Java进程挂了,那么一切都要从头开始吗?

如果我们能确定某个SCN之前的所有记录都已经被处理了,那么下次重启时从这个SCN开始处理即可

两处可以确定之前SCN已经被全部处理的地方,代码如下:

a. 当前TransactionalBuffer中没有数据,代表END_SCN之前所有的事务都已经被提交了

b. 提交事务时,如果当前要提交的事务的Start_SCN 早于TransactionalBuffer中的所有事务

4. SQL解析

如果你想将Oracle的数据同步到其他数据库(包含NoSQL)的话,最好的办法是将SQL解析成结构化的对象,让下游服务去消费这些对象。

Debezium的做法,我还没抽出空研究。目前的解决方法是用com.alibaba.druid.sql.SQLUtils,这个类可以将SQL解析成结构化对象,我们再对这些对象进行一些处理,即可让下游服务消费了。

DEMO

运行效果如下

DEMO运行效果

GitHub 👉 https://github.com/TavenYin/database-cdc/tree/master/oracle-logminer

参考