☰
达梦CDC日志同步:DBMS_LOGMNR到Flink upsert-kafka
2026/11/2 15:31:51 网站建设 项目流程

简介:面向需要在 Apache Flink 中接入达梦数据库的开发者与数据平台团队,这份压缩包围绕 FlinkCDC 基于日志的实时同步场景,提供可直接参考的工程样例与配置素材。包内共 315 个文件,以 263 个 jar 依赖、若干 xml 配置、class 与 java 源码为主,另含 sql 脚本、properties 参数文件及 docx 说明,压缩包约 341.71MB,便于还原连接器运行环境与项目结构。已有 776 人学习下载。读者可从中了解达梦日志捕获、插入更新删除变更同步到 Flink 流处理链路的基本组织方式,结合 Java 作业与 SQL 两种接入思路,查看反序列化、统计工具、CDC 主流程等模块的代码骨架,并据此排查连接参数、依赖版本与作业启动问题。适合具备一定 Flink 与数据库基础、需要搭建实时同步原型或课程实验环境的中高级读者参考。

1. 官方 connector 清单里翻不到达梦,FlinkCDC 的日志同步照样能落地

先说结论:想在 FlinkCDC 官方那套 connector 清单里找达梦数据库的条目,基本会扑空,照着抄一句connector = 'dm-cdc'的建表语句大概率直接报类找不到。但「基于日志实时同步」这件事在达梦上完全成立——达梦有归档日志、有行级逻辑日志、有DBMS_LOGMNR日志挖掘包,这三样串起来就是一条能用的变更流。真正要干的不是找现成 connector,而是把达梦日志翻译成 Flink 认识的 changelog,剩下的去重、分流、落仓交给 Flink。这篇内容适合正在做信创替换、存量 DM 库要实时入仓、又不想退回定时抽数的人,也适合已经被「按时间戳轮询漏更新」坑过一轮的团队。

2. 打开达梦的行级逻辑日志:归档、级别与可挖掘性验证

2.1 归档和逻辑日志是两个开关,缺一个都白搭

很多人第一次配达梦 CDC 会翻车在同一处:只改了RLOG_APPEND_LOGIC,以为日志里就有行了,结果归档目录里空空如也,或者归档里挖出来全是物理页号。

这两件事的职责要分清。归档负责「日志留不留、留多久」,由ARCH_INI加dmarch.ini控制;逻辑日志负责「redo 里带不带行级的前后值」,由RLOG_APPEND_LOGIC控制。前者是载体,后者是内容,少任何一个都做不成变更捕获。更麻烦的是这两个参数在多数小版本里都是静态参数,改完必须重启实例才生效,动态SP_SET_PARA_VALUE改完看着成功了、实际没落地,这是最常见的假象。

我一般在dm.ini里固定这么几项:

参数默认建议值作用是否需重启
ARCH_INI01启用归档配置,读取dmarch.ini是
RLOG_APPEND_LOGIC02行级逻辑日志级别,2 表示记录全部列是
RLOG_APPEND_OPTIMIZE01合并部分冗余逻辑记录,减小日志体积是
LOG_BUFFER_SIZE864redo 缓冲区大小,单位 MB是
SVR_LOG0视情况服务端 SQL 日志,仅排错期开否

dmarch.ini里至少要有本地归档一段,否则开了ARCH_INI也不会落盘:

[ARCHIVE_LOCAL1] ARCH_TYPE = LOCAL ARCH_DEST = /dmdata/arch ARCH_FILE_SIZE = 2048 ARCH_SPACE_LIMIT = 0 ARCH_RESERVE_TIME = 168

ARCH_SPACE_LIMIT = 0表示不做空间上限硬限制,生产上我反而建议给个上限并配合监控,避免归档把盘吃满导致实例挂起;ARCH_RESERVE_TIME单位是小时,这里给 168 小时即七天,这个值必须大于「同步链路最长可能停机时间」,否则停机超过保留期就再也补不回来了,位点直接断档。改完这两处,重启实例,再回到数据库里确认参数真的落到内存里:

-- 确认参数已生效,注意 PARA_VALUE 是运行值 SELECT PARA_NAME, PARA_VALUE FROM V$DM_INI WHERE PARA_NAME IN ('ARCH_INI','RLOG_APPEND_LOGIC','RLOG_APPEND_OPTIMIZE','LOG_BUFFER_SIZE'); -- 确认归档配置被识别 SELECT ARCH_NAME, ARCH_TYPE, ARCH_DEST, ARCH_SPACE_LIMIT FROM V$DM_ARCH_INI;

V$DM_INI里看到的是运行值,改完没重启的话PARA_VALUE还是旧值——这一行查询就是最省事的「后悔药」,别跳过。V$DM_ARCH_INI返回空行说明dmarch.ini没被加载,常见原因是文件放在实例目录但路径大小写不对,或者ARCH_INI没生效。

2.2 用 DBMS_LOGMNR 先把「能不能挖」验证掉

参数配好之后不要急着搭 Flink 作业,先证明日志挖得出来。达梦的DBMS_LOGMNR包用法和 Oracle 那套很像,但系统包默认不一定存在,需要先创建一次:

-- 系统包只建一次,重复执行会提示已存在,忽略即可 SP_CREATE_SYSTEM_PACKAGES(1);

然后按「加载日志文件 → 启动挖掘 → 查内容 → 结束挖掘」四步走:

DECLARE BEGIN -- NEW 表示清空文件列表后重新加,一次挖掘只调一次 DBMS_LOGMNR.ADD_LOGFILE('/dmdata/arch/ARCHIVE_LOCAL1_20250101.log', DBMS_LOGMNR.NEW); -- DICT_FROM_ONLINE_CATALOG 用当前在线字典解析,不需要单独导入字典 DBMS_LOGMNR.START_LOGMNR(OPTIONS => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG); END; / -- 只捞一张业务表看 20 行,重点看 UPDATE 的前后值是否完整 SELECT SCN, COMMIT_SCN, TIMESTAMP, OPERATION, SEG_OWNER, TABLE_NAME, SQL_REDO FROM V$LOGMNR_CONTENTS WHERE SEG_OWNER = 'BIZ' AND TABLE_NAME = 'T_ORDER' AND ROWNUM <= 20 ORDER BY COMMIT_SCN, SCN; EXEC DBMS_LOGMNR.END_LOGMNR();

这段验证覆盖三个关键点:OPERATION里是否出现INSERT、UPDATE、DELETE三种;UPDATE的记录里能不能同时看到改前和改后的列值;COMMIT_SCN是不是非空且单调递增。三个都成立,说明日志侧准备工作完成,可以进下一步;只出现INSERT而没有UPDATE的完整列,八成是RLOG_APPEND_LOGIC还停在 1。

START_LOGMNR的OPTIONS参数我一般只用DICT_FROM_ONLINE_CATALOG。用在线字典解析性能好、不需要额外导入字典文件,代价是历史日志遇到 DDL 之后会解析失败——跨 DDL 的日志段必须换成导入字典的方式重挖,这个坑在避坑章节展开。另外包内过程的参数名在不同小版本之间有出入,写代码前先用SELECT TEXT FROM USER_SOURCE WHERE NAME='DBMS_LOGMNR'把本机定义确认一遍,比照文档抄稳妥得多。

2.3 日志级别 1/2/3 怎么选,以及主键这件事

RLOG_APPEND_LOGIC的取值不是越大越好,它是拿存储和写入开销换信息量:

级别记录内容日志增量典型用途
0不记录行级逻辑信息无不做 CDC
1仅主键/行标识小只判断「这行变过」,拿不到列值
2全部列的前后值中等主流选择,能做 upsert
3全部列 + 附加行定位信息较大无主键表、需要精确定位的场景

多数同步场景我会选 2。选 3 的典型理由是无主键表要靠额外信息定位行,但更正确的做法是给业务表补主键或唯一约束——逻辑日志里没有稳定的行标识,下游的 upsert 就无从谈起,这属于用参数补数据模型欠账,迟早要还。

补主键这一步别想着「以后再说」。表没有主键时,V$LOGMNR_CONTENTS里 UPDATE 的记录缺少可靠定位字段,解析器只能退化成「整表按时间戳覆盖」,那就不是日志同步了。上线前把待同步表清单拉出来,逐张确认主键或唯一索引,比后面排查数据重复省事十倍。

3. 把日志挖成变更流:位点、事务边界与 Kafka 投递

3.1 位点用 COMMIT_SCN,不要用行级 SCN

这一步是整条链路最容易做错的地方。V$LOGMNR_CONTENTS里每条记录都带SCN,看起来很自然地想拿它当位点:读到哪条记哪条。问题在于行级 SCN 反映的是「这个块被改的时间」,不是「这个事务提交的时间」。一个大事务改了几万行、最后回滚,行级记录照样在日志里;另一个长事务先改后提交,行 SCN 早、提交晚。用行 SCN 做位点,下游就会出现「先看到后提交的数据」和「看到最终被回滚的数据」两种不一致。

正确做法是以COMMIT_SCN作为对外可见的位点,并且输出顺序按COMMIT_SCN, SCN排:

SELECT COMMIT_SCN, SCN, TIMESTAMP, OPERATION, SEG_OWNER, TABLE_NAME, SQL_REDO FROM V$LOGMNR_CONTENTS WHERE OPERATION IN ('INSERT','UPDATE','DELETE') AND COMMIT_SCN > ? -- 上次消费到的位点 AND COMMIT_SCN <= ? -- 本次挖掘上界 ORDER BY COMMIT_SCN, SCN;

COMMIT_SCN为空的记录属于未提交事务,直接过滤掉,不要试图缓存起来等——解析器是短生命周期进程,缓存没有意义,下一次挖掘它们会带着真正的COMMIT_SCN重新出现。位点表我一般单独建一张SYNC_OFFSET,字段就TASK_NAME和LAST_SCN两个,用MERGE INTO写,保证多次执行只留一行:

CREATE TABLE SYNC_OFFSET ( TASK_NAME VARCHAR(64) PRIMARY KEY, LAST_SCN BIGINT, UPD_TIME TIMESTAMP DEFAULT SYSDATE );

位点必须和消息投递在同一个事务语义下推进。常见做法是「先发 Kafka 并等 ack,再更新位点」,中间进程挂掉会重发一批消息,靠下游 upsert 的幂等性兜住;反过来「先更新位点再发」则会丢数据,丢数据比重复难查得多,我宁可重复。

3.2 一条变更消息长什么样

消息结构决定了 Flink 侧能不能直接用 upsert 语义,所以字段设计要一次到位。我一般拆成两个主题:dm_cdc_audit存完整的 before/after,用于审计和对账;dm_cdc_upsert只存变更后的整行,删除时发一条 value 为 null 的墓碑消息。后者是给 Flink 的upsert-kafka直接消费的,前者留着出问题时当黑匣子翻。

dm_cdc_audit的主题体:

{ "schema": "BIZ", "table": "T_ORDER", "op": "U", "scn": 88213344, "commit_scn": 88213390, "ts": "2025-01-01 10:00:00.123", "key": "BIZ.T_ORDER:10086", "before": {"ID": 10086, "AMOUNT": 10.00, "STATUS": "NEW"}, "after": {"ID": 10086, "AMOUNT": 12.00, "STATUS": "PAID"} }
字段含义生成规则
op操作类型I/U/D三选一,由OPERATION映射
key全局行标识schema.table:主键值,多列主键用 `
commit_scn事务提交位点直接取COMMIT_SCN,用于排序和断点
before变更前整行仅U/D有,I时为 null
after变更后整行仅I/U有,D时为 null

key的生成规则必须全局统一,大小写、前后空格、数值型主键的字符串化方式都要在解析器里固定死。下游 Flink 的去重和 upsert 全靠这个 key,一旦同一个主键生成了两种写法,就会变成两条独立的行,这种重复用肉眼几乎发现不了,只能靠对账。

3.3 解析器骨架:一个能跑起来的最小闭环

解析器不必上重型框架,一个定时循环加上位点表就够跑通。下面这段是骨架,用达梦官方 Python 驱动连库、kafka-python 投递:

import json, time import dmPython from kafka import KafkaProducer BATCH_SCN_SPAN = 200000 # 单次挖掘的 SCN 跨度,按日志量调 TASK = "biz_order_to_dw" producer = KafkaProducer( bootstrap_servers=["10.0.0.21:9092", "10.0.0.22:9092"], acks="all", # 至少等所有 ISR 落盘 enable_idempotence=True, # 开启幂等,避免重试产生乱序 key_serializer=lambda k: k.encode("utf-8"), value_serializer=lambda v: json.dumps(v, ensure_ascii=False).encode("utf-8") if v is not None else None, ) def read_offset(cur): cur.execute("SELECT LAST_SCN FROM SYNC_OFFSET WHERE TASK_NAME = ?", (TASK,)) row = cur.fetchone() return int(row[0]) if row else 0 def write_offset(conn, cur, scn): cur.execute( "MERGE INTO SYNC_OFFSET t " "USING (SELECT ? AS TASK_NAME, ? AS LAST_SCN FROM DUAL) s " "ON (t.TASK_NAME = s.TASK_NAME) " "WHEN MATCHED THEN UPDATE SET t.LAST_SCN = s.LAST_SCN, t.UPD_TIME = SYSDATE " "WHEN NOT MATCHED THEN INSERT (TASK_NAME, LAST_SCN) VALUES (s.TASK_NAME, s.LAST_SCN)", (TASK, scn), ) conn.commit() def mine_once(conn, cur, start_scn): end_scn = start_scn + BATCH_SCN_SPAN # 调用包内过程,参数顺序为 起始SCN、结束SCN、选项 cur.execute("{call DBMS_LOGMNR.START_LOGMNR(?, ?, ?)}", (start_scn, end_scn, 0)) cur.execute( "SELECT COMMIT_SCN, SCN, OPERATION, SEG_OWNER, TABLE_NAME, SQL_REDO " " FROM V$LOGMNR_CONTENTS " " WHERE OPERATION IN ('INSERT','UPDATE','DELETE') " " AND SEG_OWNER = 'BIZ' AND COMMIT_SCN IS NOT NULL " " ORDER BY COMMIT_SCN, SCN" ) max_scn, sent = start_scn, 0 while True: rows = cur.fetchmany(500) # 分批取,避免一次性拉爆内存 if not rows: break for commit_scn, scn, op, owner, table, redo in rows: key = f"{owner}.{table}:{scn}" # 示例:真实场景用主键值,此处仅占位 payload = { "schema": owner, "table": table, "op": op[0], "scn": int(scn), "commit_scn": int(commit_scn), "key": key, "sql_redo": redo, } producer.send("dm_cdc_audit", key=key, value=payload) sent += 1 max_scn = max(max_scn, int(commit_scn)) producer.flush() cur.execute("{call DBMS_LOGMNR.END_LOGMNR()}") return max_scn, sent if __name__ == "__main__": conn = dmPython.connect(user="SYNC_USER", password="******", server="10.0.0.10", port=5236) cur = conn.cursor() while True: offset = read_offset(cur) new_offset, sent = mine_once(conn, cur, offset) if sent: write_offset(conn, cur, new_offset) # 先发后记,重复由下游幂等兜住 time.sleep(1) # 无变更时歇一秒,别空转打满 CPU

逻辑上有三处值得展开。START_LOGMNR的结束 SCN 我用start_scn + BATCH_SCN_SPAN推进,而不是挖到当前时刻,好处是每次工作量可控、失败重试只影响一小段,坏处是延迟取决于循环间隔,一秒一轮、跨度二十万 SCN,多数业务量下延迟在秒级,够用。acks="all"加enable_idempotence=True是 Kafka 生产端的基本配置,前者保证消息落到所有同步副本,后者保证重试不会产生乱序,两个一起开才能和「先发后记位点」的重复投递策略配套。producer.flush()放在更新位点之前,保证这一批要么全发出去,要么位点不动,不会出现「位点前进了但消息还压在客户端缓冲区」的情况。

参数上最需要按现场调的是BATCH_SCN_SPAN和fetchmany的批大小。SCN 跨度太小会导致挖掘调用过于频繁、每次都要重新加载日志文件,吞吐上不去;跨度太大会让单次事务变长、失败重试代价高。经验做法是先按 20 万跑,观察单次耗时,稳定在 1 秒以内就保持,超过 2 秒就减半。

4. Flink 侧接流:upsert-kafka 建表、按表分流与落仓

4.1 为什么用 upsert-kafka 而不是普通 kafka source

普通kafkaconnector 在 Flink SQL 里被当作 append 流处理,它不知道消息之间存在「同一个主键的后一次覆盖前一次」这层关系,你要自己去重就得开状态、写ROW_NUMBER(),而这在无界流上是没有正确语义的——ROW_NUMBER只是逐条处理,不会因为后到的同主键数据而撤回之前的结果。

upsert-kafka把这件事做进了 connector 内部:它要求声明主键,按主键把消息解释成+I、-U、+U、-D,value 为 null 时生成-D。配合解析器「只发变更后整行、删除发墓碑」的设计,链路上天然就是幂等的,同一条消息重复投递多少次,最终结果都一样。

这里有个前提必须对齐:upsert-kafka的主键字段和消息里 key 的生成规则要一致。我在三章里强调 key 统一,原因就在这里——key 格式一乱,upsert-kafka就认不出是同一行,去重直接失效。

4.2 建表与按表分流

源表声明三个字段组成主键,对应解析器输出的schema、table、pk:

CREATE TABLE dm_cdc_upsert ( `schema_name` STRING, `table_name` STRING, `pk_value` STRING, `row_json` STRING, -- 变更后整行的 JSON,删除时为 NULL `op` STRING, `commit_scn` BIGINT, `ts` TIMESTAMP(3) METADATA FROM 'timestamp', PRIMARY KEY (`schema_name`, `table_name`, `pk_value`) NOT ENFORCED ) WITH ( 'connector' = 'upsert-kafka', 'topic' = 'dm_cdc_upsert', 'properties.bootstrap.servers' = '10.0.0.21:9092,10.0.0.22:9092', 'key.format' = 'json', 'key.json.ignore-parse-errors' = 'true', 'value.format' = 'json', 'value.json.fail-on-missing-field' = 'false', 'properties.auto.offset.reset' = 'earliest', 'properties.enable.auto.commit' = 'false' );

PRIMARY KEY ... NOT ENFORCED是声明式主键,Flink 不会真去校验,但 upsert 语义靠它建立。auto.commit关掉是必须的:让 Flink 自己用 checkpoint 管理 offset,否则作业重启时可能出现「offset 已提交但状态没做快照」的错位。auto.offset.reset = earliest只在新作业首次启动、没有 checkpoint 时生效,别指望它能解决位点回退。

分流我用视图做,每张业务表一个视图,把 JSON 里的字段摊平并转型:

CREATE VIEW v_order AS SELECT CAST(JSON_VALUE(`row_json`, '$.ID') AS BIGINT) AS id, CAST(JSON_VALUE(`row_json`, '$.AMOUNT') AS DECIMAL(18,2)) AS amount, JSON_VALUE(`row_json`, '$.STATUS') AS status, `commit_scn`, `ts` FROM dm_cdc_upsert WHERE `schema_name` = 'BIZ' AND `table_name` = 'T_ORDER';

用视图而不是在 sink 里写过滤条件,好处是每张表的字段映射独立演进,新增一张表就是加一个视图加一个 INSERT,不用动源表定义。JSON_VALUE取不到值时返回 null 而不是报错,这一点比JSON_QUERY稳妥,字段增删不会把整个作业打挂。代价是每加一个字段都要改视图,这个人工成本我认为值得——字段类型从字符串转成 DECIMAL 这类事,让 Flink 在 SQL 层显式做,比在解析器里隐式猜类型可控。

4.3 写目标端与几个必调参数

目标端用 JDBC sink,主键声明后自动走 upsert:

CREATE TABLE sink_order ( id BIGINT NOT NULL, amount DECIMAL(18,2), status STRING, updated_at TIMESTAMP(3), PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'jdbc', 'url' = 'jdbc:mysql://10.0.0.31:3306/dw', 'table-name' = 'dwd_order', 'username' = 'flink_rw', 'password' = '******', 'sink.buffer-flush.max-rows' = '2000', 'sink.buffer-flush.interval' = '2s', 'sink.max-retries' = '3' ); INSERT INTO sink_order SELECT id, amount, status, `ts` FROM v_order;

sink.buffer-flush.max-rows和interval是一对:满足任一条件就刷一次。批量大能提吞吐,但会放大故障时的重复窗口,2000 行 / 2 秒是我在中等数据量下的常用组合,数据量再大就往上调,别一次调到几万,那样写入失败重试的代价太高。sink.max-retries = 3只覆盖可重试异常,主键冲突这类逻辑错误不会重试,会直接把作业打挂,这是好事,别为了「不挂」把重试次数调高。

作业级别还有两个参数要显式设置:execution.checkpointing.interval我一般给 30 秒到 1 分钟,太短会让 upsert-kafka 频繁做快照、拖慢吞吐;table.exec.state.ttl给 24 小时以上,去重和聚合状态都要靠它兜底,设太短会导致重启后状态丢失、重复计算。并行度按 Kafka 分区数对齐,源端并行度超过分区数会有空闲 subtask,白白占资源。

如果后续达梦的 source connector 生态补齐了,整库同步可以直接换成 Flink CDC pipeline 的 yaml 写法,route规则和今天这套「按表分流」是一一对应的,迁移成本主要在字段映射上。现阶段用 Kafka 承接反而多了一层缓冲,源库出问题时变更消息还在主题里,补数不用回源重挖日志。

5. 避坑:五类翻车场景的现象、原因与解决

5.1 开了逻辑日志,UPDATE 却只挖到主键

现象:V$LOGMNR_CONTENTS里能看到UPDATE操作,但SQL_REDO只有主键列,改动的业务字段完全缺失,下游更新后数据看起来「没变」。

原因:RLOG_APPEND_LOGIC停在 1。级别 1 只记录行标识,天然没有列值,参数改完没重启也可能停留在旧值。

解决:先查V$DM_INI确认运行值是 2 而不是配置文件里写了 2,再重启实例。重启后拿一笔测试更新重新挖一遍验证,别直接放开全量同步——日志级别变更只在变更之后的日志里生效,历史日志挖不出列值,位点要重新对齐。

5.2 无主键表把 UPDATE 变成了 INSERT

现象:下游目标表里同一业务行出现多条记录,主键列值相同但其他字段是不同时间点的快照,越跑越多。

原因:源表没有主键或唯一索引,逻辑日志缺少稳定的行定位信息,解析器退化成用SCN或ROWID拼 key,每次更新都生成新 key,upsert-kafka认为是新行。

解决:给源表补主键,这是唯一正确的路。实在不能改表结构,就在解析器里用「全部业务列」做联合 key,代价是同值多行无法区分、删除操作基本失效。这种妥协要写进交接文档,别让下一个人以为链路是完整的。

5.3 归档被清理,位点再也追不上

现象:解析器重启后报日志文件不存在,或者挖出来的记录从某个时间点之后突然空了一大段,对账发现中间少了几小时数据。

原因:归档保留策略太激进,ARCH_RESERVE_TIME设得比最长停机时间短,或者磁盘满导致归档写入失败被跳过。

解决:ARCH_RESERVE_TIME至少覆盖「同步链路可能停摆的最长时间 × 2」,同时给归档目录加容量告警。已经断档的情况只能重新做一次全量加增量对齐,没有别的办法——这也是为什么位点表要单独持久化,至少要能准确知道断在哪。

5.4 大事务把 Kafka 打爆、Flink 持续背压

现象:业务侧跑一次批量更新几十万行,Kafka 主题瞬时写入飙升,Flink 作业背压指标拉满,checkpoint 超时失败,然后作业反复重启。

原因:解析器按 SCN 区间一次性把整个事务的所有行都投出去,没有限速;同时下游 sink 的批量参数偏小,吞吐跟不上。

解决:解析器侧按批send并在每批后做一次flush加短暂 sleep,把瞬时压力摊平;Flink 侧调大 sink 批量、增加 Kafka 分区数并同步提高作业并行度。checkpoint 超时可以把execution.checkpointing.timeout适度放宽,但这是缓解不是根治,根本还是要控制单次事务的消息体量。

5.5 跨 DDL 之后解析全部失败

现象:源库做了一次加字段的变更,之后解析器报字典相关错误,或者新字段一直取不到值。

原因:START_LOGMNR用DICT_FROM_ONLINE_CATALOG,拿当前字典去解析 DDL 之前的日志,表结构对不上。DDL 之后的日志因为视图里没有新字段,JSON_VALUE取不到值返回 null。

解决:DDL 必须纳入发布流程,改表结构前先停同步、记录位点,改完再启动并同步更新 Flink 侧视图定义。跨 DDL 的历史日志要用导入字典的方式重挖,不能继续用在线字典。这条听起来很重,但比起数据错位后再去对账,停十分钟划算得多。

6. 上线前的校验清单与一个并行挖掘的提速技巧

链路跑通不等于可以上线。我习惯在切生产前走一遍下面这张表,每一项都要有明确的验证动作和通过标准,不能靠「看起来正常」:

校验项验证动作通过标准
日志级别查V$DM_INI运行值RLOG_APPEND_LOGIC = 2且重启后未回退
主键覆盖导出待同步表清单逐张核对全部有主键或唯一索引
位点连续性手工 kill 解析器再拉起位点从上次COMMIT_SCN续上,无重复无丢失
顺序正确性同一主键连续改三次后查目标表目标表是最新值,中间态不残留
删除语义源库删一行目标表对应行消失,非逻辑删除残留
归档容量观察三日归档日增量保留期内不会被清理,磁盘余量大于 30%
背压压测期间看 Flink 背压指标无持续 high 背压,checkpoint 全部成功

最后说一个提速技巧:单实例的DBMS_LOGMNR挖掘是串行的,日志量大时它会是瓶颈。可以把归档文件按时间切成若干段,每段起一个独立解析进程并行挖,各自产出到不同的 Kafka 分区,但位点表必须按「段」独立记录,合并时以段内最大COMMIT_SCN为准。这么做的代价是跨段的事务顺序无法保证,所以只适合按主键分区、下游本来就是 upsert 的场景;如果业务要求全局严格有序,就别拆,老老实实单进程跑,把 SCN 跨度调大比并行更安全。

这套东西我从第一次配达梦日志同步时踩到「参数改了没重启」开始,前后翻车过好几轮,最惨的一次是归档被清导致断档两天,对账对了整整一周。从那以后我每次上线前都强制走一遍上面那张表,尤其是位点连续性那一项,宁可手工 kill 两次进程,也不赌它「应该没问题」。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询