基于触发器实现 SAP HANA 实时数据同步(二)
背景
在 SAP HANA 中实现源端增量同步时,最初我采用的方案是“单表 CDC”模式,即让所有订阅表的增量(插入、更新、删除)都通过触发器统一写入同一张 CDC 表。这样做的初衷是简化架构,但随着数据量和字段数的增长,也带来了性能瓶颈。
本文聚焦于将“单 CDC 表”改为“表级 CDC 表”之后,整体同步效率与架构的改进思路,希望对你有所启发。
单 CDC 表模式的问题
最开始,我把所有订阅表的变更信息写到一张“统一 CDC 表”里,触发器会把每行变更拼成 JSON 再插入。示意图如下:
弊端1:触发器执行效率低
当订阅表字段超过 300 列时,拼 JSON 的过程让触发器变得非常重。实测 10 万条 UPDATE 语句的耗时如下:# 不使用触发器
real 0m56.456s user 0m8.836s sys 0m4.708s
# 表级 CDC(1v1)
real 5m45.247s user 0m9.108s sys 0m4.648s
# 单 CDC(all in 1)
real 10m35.716s user 0m9.080s sys 0m5.088s随着字段数增长,单 CDC 表触发器开销迅速放大,严重拖慢同步速度。
弊端2:增量数据混杂导致积压
把 A、B、C 三张表的变更都写到同一张 CDC 表里时,若 A 表数据量猛增,但 B 表只有少量更新,就会因为 A 表占用过多插入资源,导致 B 表的数据一直堆积,无法及时消费。这样不仅影响了 B 表的同步实时性,也让整体任务变得不稳定。
表级 CDC 表设计
针对上述问题,我把每个源表都拆成独立的 CDC 表,结构基于原表再加几个位点字段,示例如下。
原始表(TABLE_TWO_PK
):
CREATE COLUMN TABLE "SYSTEM"."TABLE_TWO_PK" (
"ORDERID" INTEGER NOT NULL,
"PRODUCTID" INTEGER NOT NULL,
"QUANTITY" INTEGER,
CONSTRAINT "TABLE_TWO_PK_pkey" PRIMARY KEY ("ORDERID", "PRODUCTID")
);
表级 CDC 表(TABLE_TWO_PK_CDC
):
CREATE COLUMN TABLE "SYSTEM"."TABLE_TWO_PK_CDC" (
"ORDERID" INTEGER,
"PRODUCTID" INTEGER,
"QUANTITY" INTEGER,
"__$DATA_ID" BIGINT NOT NULL,
"__$TRIGGER_ID" INTEGER NOT NULL,
"__$TRANSACTION_ID" BIGINT NOT NULL,
"__$CREATE_TIME" TIMESTAMP,
"__$OPERATION" INTEGER NOT NULL
);
/* 建索引等其他配置略 */
每当原表发生 INSERT/UPDATE/DELETE,都只需在对应的 CDC 表里 INSERT 一行,示例(INSERT 触发器):
CREATE TRIGGER "CLOUD_CANAL_ON_I_TABLE_TWO_PK_TRIGGER"
AFTER INSERT ON "SYSTEM"."TABLE_TWO_PK"
REFERENCING NEW ROW NEW FOR EACH ROW
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN END;
INSERT INTO "SYSTEM"."TABLE_TWO_PK_CDC" (
__$DATA_ID, __$TRIGGER_ID, __$TRANSACTION_ID, __$CREATE_TIME,
__$OPERATION, "ORDERID", "PRODUCTID", "QUANTITY"
) VALUES (
"SYSTEM"."CC_TRIGGER_SEQ".NEXTVAL,
104,
CURRENT_UPDATE_TRANSACTION(),
CURRENT_UTCTIMESTAMP,
2, -- 2 表示 INSERT 操作
:NEW."ORDERID",
:NEW."PRODUCTID",
:NEW."QUANTITY"
);
END;
这样做的好处:
触发器逻辑简单
只需执行一次INSERT
,不拼 JSON,也不扫描其他表。即使字段很多,触发器仍能高效完成插入。数据隔离更清晰
每张表对应一张 CDC 表,不同表的变更互不干扰。避免了单表写入过多导致的其它表积压问题。消费更灵活
在下游消费时,无需再把 JSON 拆出来,直接按表一次性读增量即可,业务逻辑更简单。
核心同步逻辑
在表级 CDC 方案下,源端可能有几十张 CDC 表需要同时扫描,并且要保证单表内变更的顺序。整体思路如下:
为每个 CDC 表创建独立的工作线程
按照订阅表数量,初始化对应数量的TableWorker
线程,每个线程负责扫描并消费它那张 CDC 表的数据。通过事务 ID 定位扫描范围
每个TableWorker
维护一个“表级位点”(上次消费到的事务 ID)。每次扫描:- 先查找该表 CDC 中,比位点大且属于同一事务范围的记录
- 如果发现有“尚未提交”的事务,就让线程等待,直到下轮扫描再处理
- 否则就消费该事务范围内的数据,处理完毕后更新表级位点,进入下一轮
这样做能保证:
- 单表顺序:同一个事务里的操作按顺序读到,避免乱序。
- 并行度高:不同表各自一条线程,互不干扰,消费更及时。
总结
这次优化把“单一 CDC 表”拆成“表级 CDC 表”后,核心优点体现在:
- 触发器速度提升:每次只插入对应表的一行记录,避免复杂拼接和 JSON 转换开销。
- 数据隔离:不同表互不影响,避免因一张表数据量暴增导致其他表增量滞后。
- 消费逻辑更清晰:下游按表扫描,不需要额外解析或分拣,简单高效。