跳到主要内容

基于触发器实现 SAP HANA 实时数据同步(二)

背景

在 SAP HANA 中实现源端增量同步时,最初我采用的方案是“单表 CDC”模式,即让所有订阅表的增量(插入、更新、删除)都通过触发器统一写入同一张 CDC 表。这样做的初衷是简化架构,但随着数据量和字段数的增长,也带来了性能瓶颈。

本文聚焦于将“单 CDC 表”改为“表级 CDC 表”之后,整体同步效率与架构的改进思路,希望对你有所启发。

单 CDC 表模式的问题

最开始,我把所有订阅表的变更信息写到一张“统一 CDC 表”里,触发器会把每行变更拼成 JSON 再插入。示意图如下:

hana_sync_2_01

弊端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;

这样做的好处:

  1. 触发器逻辑简单
    只需执行一次 INSERT,不拼 JSON,也不扫描其他表。即使字段很多,触发器仍能高效完成插入。

  2. 数据隔离更清晰
    每张表对应一张 CDC 表,不同表的变更互不干扰。避免了单表写入过多导致的其它表积压问题。

  3. 消费更灵活
    在下游消费时,无需再把 JSON 拆出来,直接按表一次性读增量即可,业务逻辑更简单。

核心同步逻辑

在表级 CDC 方案下,源端可能有几十张 CDC 表需要同时扫描,并且要保证单表内变更的顺序。整体思路如下:

  1. 为每个 CDC 表创建独立的工作线程
    按照订阅表数量,初始化对应数量的 TableWorker 线程,每个线程负责扫描并消费它那张 CDC 表的数据。

  2. 通过事务 ID 定位扫描范围
    每个 TableWorker 维护一个“表级位点”(上次消费到的事务 ID)。每次扫描:

    • 先查找该表 CDC 中,比位点大且属于同一事务范围的记录
    • 如果发现有“尚未提交”的事务,就让线程等待,直到下轮扫描再处理
    • 否则就消费该事务范围内的数据,处理完毕后更新表级位点,进入下一轮

这样做能保证:

  • 单表顺序:同一个事务里的操作按顺序读到,避免乱序。
  • 并行度高:不同表各自一条线程,互不干扰,消费更及时。

总结

这次优化把“单一 CDC 表”拆成“表级 CDC 表”后,核心优点体现在:

  • 触发器速度提升:每次只插入对应表的一行记录,避免复杂拼接和 JSON 转换开销。
  • 数据隔离:不同表互不影响,避免因一张表数据量暴增导致其他表增量滞后。
  • 消费逻辑更清晰:下游按表扫描,不需要额外解析或分拣,简单高效。