2. 变更数据捕获原理

变更数据捕获(Change Data Capture,CDC)要解决的核心问题只有一个:如何以尽可能低的侵入性和尽可能小的延迟,把源库中已经发生或正在发生的数据变化,准确地识别出来并交付给下游。围绕这个问题,业界形成了三条主流技术路线——基于日志、基于触发器、基于时间戳。它们并不是同一层面的三种“实现细节”,而是三种对“变化从哪里被发现”这一根本问题的不同回答。理解这一点,选型才不会停留在“哪个快用哪个”的层面。

一、基于日志的CDC:从数据库内部读“已提交的事实”

日志型CDC不查询业务表,而是读取数据库自身为保证持久化和复制而写入的日志:MySQL 的 binlog、PostgreSQL 的 WAL(逻辑复制槽)、Oracle 的 Redo Log / LogMiner、SQL Server 的事务日志。数据库在事务提交时,已经把“哪一行、哪个字段、从什么值变成什么值”按顺序写进了日志,CDC 工具只是这些日志的另一个消费者。

以 MySQL 的 ROW 格式 binlog 为例,一条 UPDATE 会记录变更前后的镜像。开启方式通常是在 my.cnf 中设置:

[mysqld]
server-id        = 223344
log_bin          = mysql-bin
binlog_format    = ROW
binlog_row_image = FULL
expire_logs_days = 7

随后由 Debezium、Canal、Flink CDC 等工具以复制协议连接,读取到的事件形如:

{
  "op": "u",
  "source": {"table": "orders", "ts_ms": 1718000000123},
  "before": {"id": 1001, "status": "CREATED", "amount": 199.00},
  "after":  {"id": 1001, "status": "PAID",    "amount": 199.00}
}

日志型CDC的关键优势在于:对业务表零侵入(不需要建触发器、不需要改表结构)、能捕获删除(DELETE 在日志中有记录)、能拿到变更前后镜像、吞吐高且延迟可做到秒级甚至亚秒级。代价是它强依赖数据库类型与版本,需要额外的日志保留空间和复制权限,并且日志的解析格式随版本变化,运维复杂度不低。此外,DDL 变更、大事务、日志被过早清理,都是生产上必须提前处理的边界。

二、基于触发器的CDC:把变化“写”进一张影子表

触发器型CDC在源表上创建 AFTER INSERT / UPDATE / DELETE 触发器,每当业务写入发生,触发器把变更行插入一张专门的变更日志表(常称 CDC 表或 shadow table)。下游再轮询这张表并消费。

CREATE TABLE orders_cdc (
  cdc_id     BIGINT AUTO_INCREMENT PRIMARY KEY,
  op         CHAR(1),
  order_id   BIGINT,
  status     VARCHAR(20),
  changed_at DATETIME(3)
);

CREATE TRIGGER trg_orders_au
AFTER UPDATE ON orders
FOR EACH ROW
INSERT INTO orders_cdc(op, order_id, status, changed_at)
VALUES ('U', NEW.id, NEW.status, NOW(3));

它的优点是与数据库种类无关,只要支持触发器就能用,且变更语义由业务自己定义,容易做字段级过滤。但缺点同样突出:触发器在同一个事务内同步执行,会直接增加源库写入延迟;高并发写入时触发器本身成为瓶颈;变更表会持续膨胀,需要清理策略;更麻烦的是,触发器逻辑与业务表结构强耦合,一旦加字段、改类型,触发器也要同步维护,容易漏改。因此触发器型CDC更适合变更量不大、实时性要求中等、且源库不便开启日志的场景。

三、基于时间戳的CDC:用“最后修改时间”做增量扫描

时间戳型CDC不依赖数据库内部机制,而是在业务表上维护 last_updated 之类的字段,下游按时间窗口轮询:

SELECT * FROM orders
WHERE last_updated >= :last_watermark
  AND last_updated <  :now
ORDER BY last_updated;

它实现最简单、对数据库最友好、跨库通用,但存在几个必须正视的硬伤:无法捕获物理删除(行没了,时间戳也就没了,只能靠软删除标记);同一时间戳内的多条变更可能被漏读或重复读,水位线边界处理稍有不慎就会丢数据;时钟回拨、批量补数、时区不一致都会破坏增量语义;而且每次都要扫描索引区间,变更稀疏时开销相对偏高。它适合变更频率低、允许软删除、对实时性要求不苛刻的同步场景,例如每日对账、报表抽取。

三种CDC机制的数据通路对比 同一张业务表 orders,变化被发现的“位置”不同,决定了能力边界不同 业务表 orders id / status / amount last_updated INSERT / UPDATE / DELETE ① 数据库日志 binlog / WAL / Redo 含 before / after 镜像 ② 触发器 + 变更表 AFTER INSERT/UPDATE/DELETE 同事务内写入 orders_cdc ③ 时间戳轮询 WHERE last_updated > watermark 按时间窗口增量扫描 Debezium Canal / Flink CDC 轮询消费程序 读 orders_cdc 定时调度任务 维护 watermark 下游 Kafka 数仓 / 搜索 缓存 能力边界速查 日志:捕获删除 ✓ 前后镜像 ✓ 侵入性 低 延迟 秒级 依赖 数据库类型/版本 触发器:捕获删除 ✓ 前后镜像 需自建 侵入性 高(同事务) 延迟 毫秒级但拖慢写入 时间戳:捕获删除 ✗(需软删除) 前后镜像 ✗ 侵入性 低 延迟 取决于轮询周期

四、选型依据:先问约束,再问性能

把三种机制放在一起,选型其实是在回答四个问题。第一,源库能不能开日志、有没有权限?能开且版本受支持,日志型几乎总是首选。第二,业务是否允许物理删除?如果删除必须被下游感知,时间戳型直接出局。第三,源库写入延迟的容忍度是多少?触发器会放大写延迟,高并发交易库上要慎用。第四,变更量级与实时性要求?日志型适合高吞吐、低延迟;时间戳型适合低频、准实时。

一个常见的生产组合是:核心交易库用日志型CDC(Debezium + Kafka)做实时同步,历史归档库或第三方只读库用时间戳型做每日增量抽取,而个别无法开启日志的遗留系统,才用触发器兜底。需要强调的是,三种机制可以共存,但不要在同一张表上叠加两种,否则变更会被重复投递,下游去重成本会迅速上升。

选型时优先确认三件事:源库类型与版本是否支持日志解析、业务是否存在物理删除、源库能否承受触发器带来的写放大。这三点基本决定了技术路线的可行集合,性能指标只在可行集合内做二次排序。

最后要提醒的是,无论选择哪种机制,CDC 的可靠性都取决于位点/水位线的持久化与幂等消费。日志型要保存 binlog 位点或 GTID,时间戳型要保存 watermark,触发器型要保存已消费的 cdc_id 上界。只有位点可靠落盘,重启后才能从断点续传,而不是从头重放或直接丢段——这一点,比机制本身的选择更影响最终的数据一致性。