聊Oracle日志解析,绕不开两个词:前镜像(Before Image) 和后镜像(After Image) 。
第一次听到这两个词的时候,很多人会以为是什么高深的概念。其实说白了很简单:
- 前镜像:数据修改之前长什么样
- 后镜像:数据修改之后长什么样
但问题在于------这两个东西在redo log里到底是怎么存的?CDC解析的时候到底该用哪个?
这篇文章把这件事聊透。
前镜像和后镜像,到底存在哪?
redo log里,每个Change Vector的Body部分,存的就是变更的具体数据。
以一条UPDATE为例:
sql
UPDATE employees SET salary = 8000 WHERE employee_id = 1001;
假设修改前salary是5000。这个Change Vector的Body里,会记录:
- 前镜像:employee_id=1001这一行的原始数据(salary=5000,以及其他列的值)
- 后镜像:修改后的数据(salary=8000,以及其他列的值)
但这里有一个很容易被忽略的细节:前镜像和后镜像不一定都完整。
Oracle的redo log里,前镜像的完整性取决于补充日志(Supplemental Logging) 的配置。
补充日志:前镜像完整性的开关
Oracle默认不记录完整的前镜像。它只记录修改涉及的列的前后值。
比如你执行UPDATE employees SET salary = 8000 WHERE employee_id = 1001,默认情况下:
- 前镜像:只记录了employee_id和salary的旧值
- 后镜像:只记录了employee_id和salary的新值
其他列(比如name、department、hire_date)不记录。
这意味着什么?
如果你只靠redo log来还原数据变更,你拿到的是一个"残缺"的前镜像------只知道哪一行的哪个字段被改了,但不知道这一行的完整数据长什么样。
要拿到完整的前镜像,必须开启补充日志。
补充日志有几个级别:
- 最小补充日志(Minimal) :默认级别,只记录必要的信息
- 主键补充日志(Primary Key) :额外记录主键列的值
- 唯一键补充日志(Unique Key) :额外记录唯一键列的值
- 全列补充日志(All Columns) :记录所有列的前镜像
ALL_COLUMNS级别的补充日志,会让redo log里记录每一行的完整前镜像。但这会显著增加redo log的大小------可能增加20%-50%的日志量。
这就是为什么CDC方案通常要求开启补充日志。 LogMiner需要前镜像来重构完整的行数据,Debezium和Flink CDC的文档里也都明确要求开启ALL_COLUMNS补充日志。
前镜像和后镜像,CDC到底用哪个?
答案是:两个都用,但用途不同。
后镜像用于同步。
CDC的核心目标是"把源库的变更同步到目标端"。后镜像是修改之后的值------目标端需要的就是这个值。你拿到后镜像,就知道目标端应该把这一行改成什么样。
前镜像用于判断和回滚。
前镜像虽然不直接同步到目标端,但在解析过程中有非常重要的作用:
第一,判断事务是否回滚。
如果一个事务最终回滚了,redo里会有OP 5.1(撤销修改)的Change Vector。这个Change Vector的Body里存的就是前镜像------Oracle要把数据恢复到修改之前的状态。解析器看到OP 5.1,就知道这个事务要回滚,所有变更都不应该输出。
第二,处理UPDATE的WHERE条件。
LogMiner在重构SQL的时候,需要用前镜像来构造WHERE条件。比如UPDATE employees SET salary = 8000 WHERE employee_id = 1001,LogMiner需要从Change Vector里提取employee_id的旧值来拼出WHERE子句。
第三,处理链式变更。
如果同一行数据在一个事务里被修改了多次(UPDATE→UPDATE→UPDATE),前镜像和后镜像会形成一条链。解析器需要按顺序应用这些变更,才能得到最终结果。
第四,处理行迁移和行链接。
如果一行数据太大,跨了多个数据块,前镜像可以帮助解析器识别和还原完整的行数据。
实际解析中,前镜像和后镜像会遇到哪些坑?
第一个坑:前镜像不完整。
如果没有开启补充日志,前镜像只包含修改涉及的列,其他列是空的。这时候你拿到的UPDATE变更,只知道salary从5000改成了8000,但不知道这一行的其他字段是什么。
对于CDC来说,这通常不是问题------你只需要同步变更的字段。但如果目标端需要完整的行数据(比如做全量比对),前镜像不完整就会导致数据缺失。
第二个坑:大字段的前镜像被截断。
对于BLOB、CLOB这些大字段,前镜像和后镜像可能不是完整的------redo log里可能只记录了locator的变更,实际数据在别的数据块里。解析器需要跟踪这些数据块的变更,把完整的数据拼出来。
第三个坑:LONG类型的前镜像不记录。
Oracle的LONG类型在redo log里不记录前镜像------这是一个历史遗留问题。LogMiner不支持LONG类型,很大程度上就是因为前镜像不完整。
第四个坑:虚拟列没有前镜像。
虚拟列的值是计算出来的,redo log里不记录虚拟列本身的前后镜像。解析器需要拿到依赖列的值,然后按照虚拟列的定义自己计算。
第五个坑:行迁移导致的前镜像跨块。
如果一行数据被迁移到了新的数据块,前镜像和后镜像可能分布在不同的块里。解析器需要能跨块关联,把完整的数据拼出来。
LogMiner怎么处理前镜像和后镜像?
LogMiner在处理前镜像和后镜像的时候,有一个关键的限制:它需要连接数据库,依赖数据字典。
具体来说,LogMiner拿到Change Vector之后,需要:
- 从Change Vector里提取前镜像和后镜像
- 根据对象ID去数据字典里查这个表的结构------有哪些列、什么类型、什么顺序
- 按列的顺序和类型,把前镜像和后镜像解码成可读的值
- 拼成SQL语句(
SQL_REDO和SQL_UNDO)
这个过程有几个问题:
第一,必须连接数据库。 没有数据字典,LogMiner只能返回内部对象ID和十六进制数据,根本没法用。
第二,字典必须是最新的。 如果表结构变了(加了列、改了类型),LogMiner的字典必须同步更新,否则解析出来的数据就是错的。
第三,某些类型处理不了。 前面说的BLOB、CLOB、XMLTYPE这些,LogMiner拿到前镜像和后镜像之后,不知道怎么解码,直接标记为NULL或UNSUPPORTED。
裸日志解析怎么处理前镜像和后镜像?
直接解析二进制的时候,前镜像和后镜像的处理逻辑是自己控制的。
核心思路:不依赖数据字典,按二进制格式直接解码。
具体来说:
第一,识别Change Vector的类型。 通过OP Code判断这是INSERT、UPDATE还是DELETE。INSERT没有前镜像,DELETE没有后镜像,UPDATE两个都有。
第二,解析Body里的二进制数据。 Change Vector的Body里,前镜像和后镜像按列顺序排列。每一列的编码方式由数据类型决定------NUMBER有NUMBER的编码,VARCHAR2有VARCHAR2的编码。
第三,自己维护类型映射。 不依赖Oracle的数据字典,自己维护表和列的元数据。列的顺序、类型、长度,全部自己管理。这样就不会受LogMiner字典翻译层的限制。
第四,按类型逐列解码。 NUMBER类型按Oracle的NUMBER编码规则解码,VARCHAR2按字符集解码,DATE按7字节格式解码,BLOB/CLOB按locator关联实际数据块解码。
第五,处理回滚判断。 读到OP 5.1(撤销修改)时,把对应的前镜像应用到已经解析的变更上,把数据恢复到修改前的状态。如果整个事务最终回滚,所有变更都不输出。
我们团队在做TLA的时候,前镜像和后镜像的解析就是按这个思路实现的。自己维护列的类型和顺序,自己按二进制格式解码,不依赖LogMiner的字典翻译层。所以BLOB、CLOB、XMLTYPE这些LogMiner不支持的类型,我们都能处理。流式解析,不缓存整个事务,大事务也不会OOM。
一个容易搞混的点
前镜像不是"上一行的值",后镜像不是"下一行的值"。
前镜像和后镜像是针对同一个数据块、同一行数据的修改前后状态。不是两个不同行的值。
前镜像不一定完整。 取决于补充日志的配置。默认情况下只记录修改涉及的列。
后镜像也不一定完整。 对于大字段,后镜像可能只是locator,不是完整数据。
前镜像和后镜像可能不在同一个Change Vector里。 一个大的UPDATE可能被拆成多个Change Vector,前镜像和后镜像分散在不同的record里。
说句实在话
前镜像和后镜像,名字听起来很学术,其实本质很简单------就是修改前和修改后的数据。
但在实际解析中,这两个东西涉及的问题很多:补充日志的配置、数据类型的处理、大字段的关联、回滚的判断、行迁移的处理......
套壳LogMiner的方案,把这些事情交给了LogMiner。代价是:需要连接数据库、依赖数据字典、不支持某些类型、大事务OOM。
裸日志解析的方案,自己处理前镜像和后镜像的解析。难走,但走通了就不受任何人限制。
这是"LogMiner vs 裸日志解析"系列的第三篇。后面会继续聊其他几个坑------大事务OOM、SCN追不上、在线日志读写冲突,等等。
欢迎交流。
补充:文中提到的补充日志配置和数据类型处理方式基于Oracle主流版本的通用机制,不同版本可能有细微差异。实际解析时需要根据目标版本做适配。