2、CDC核心原理:三种主流实现方式

CDC(Change Data Capture)说白了就是「逮住数据的变化」。我在做数据同步项目时,最常被问到的就是:「到底该用哪种方式抓取变更?」

今天咱们就把三种主流方案掰开揉碎讲清楚。我个人习惯把它们分成三类:基于日志、基于查询、基于触发器。每种都有各自的脾气秉性,选错了可是要吃苦头的。

核心观点:没有银弹。日志型CDC是当前生产环境的首选,但查询型和触发器型在特定场景下依然有不可替代的价值。

CDC 三种实现方式对比 基于日志的CDC • 解析数据库事务日志 • 无侵入,不修改业务表 • 支持全量+增量 • 延迟毫秒级 • 代表:Debezium、Canal • 依赖数据库版本/配置 基于查询的CDC • 轮询查询源表 • 依赖时间戳/版本号 • 实现简单,兼容性好 • 延迟秒级~分钟级 • 代表:自写轮询脚本 • 对源库有查询压力 基于触发器的CDC • 数据库触发器捕获变更 • 实时性极高 • 可自定义捕获逻辑 • 影响源库写入性能 • 代表:自定义触发器 • 维护成本高 选型建议 • 生产环境首选:日志型CDC(低延迟、无侵入) • 简单场景/小数据量:查询型CDC(快速实现) • 特殊定制需求:触发器型CDC(完全可控)

2.1 基于日志的CDC(Log-based CDC)

这是目前最主流的方式。它的原理很简单:数据库每次提交事务,都会把变更记录写到事务日志里。我们只要盯着这个日志文件读就行了。

拿MySQL举例,binlog里记录了每一行数据的插入、更新、删除操作。Canal或Debezium这类工具,就是模拟成MySQL的从库,把binlog拉过来解析。

我的经验:我在某电商平台做订单同步时,用的就是Debezium + Kafka。binlog一有变化,几毫秒内就能同步到下游。但要注意——binlog格式必须设为ROW模式,否则你拿到的可能是模糊的SQL语句,而不是精确的行变更。

核心优势:

  • 无侵入——不需要改业务表结构,不需要加触发器
  • 低延迟——日志一落盘就能读到,毫秒级延迟
  • 完整——能捕获所有变更,包括DELETE操作
  • 支持全量+增量——先做一次快照,再持续消费增量日志

但坑也不少:

  • 日志格式依赖数据库版本,MySQL 5.6和8.0的binlog格式有差异
  • 日志会过期,如果消费端挂了太久,日志被清理了就麻烦了
  • 需要数据库开启相关配置(比如MySQL的binlog),有些DBA不愿意开

避坑指南:我曾经遇到过一个案例——binlog保留时间只设了24小时,结果下游消费程序因为网络故障停了30小时。恢复后日志已经没了,只能重新做全量同步。所以建议binlog保留时间至少设72小时,关键业务设7天。

2.2 基于查询的CDC(Query-based CDC)

这种方式说白了就是「定时去查一下」。你在源表上加一个last_modified时间戳字段,然后每隔几秒或几分钟跑一条SQL:

SELECT * FROM orders 
WHERE last_modified > :last_check_time 
ORDER BY last_modified ASC;

或者用自增ID做增量标记:

SELECT * FROM orders 
WHERE id > :last_max_id 
ORDER BY id ASC;

嗯,这里要注意——这种方式实现起来确实简单,但问题也很明显。

优点:

  • 几乎兼容所有数据库,不需要特殊配置
  • 实现成本低,写个定时任务就行
  • 容易控制同步频率,对下游压力可控

缺点:

  • 无法捕获DELETE——除非你用软删除(逻辑删除)
  • 对源库有压力——每次查询都是一次全表扫描或索引扫描
  • 延迟较高——取决于轮询间隔,做不到实时
  • 时间戳精度问题——如果同一毫秒内有多条记录变更,可能漏掉

适用场景:数据量不大(百万级以内)、对实时性要求不高(分钟级可接受)、源库不方便开binlog的场景。比如一些老旧的Oracle系统,DBA死活不给开归档日志,那就只能用查询型。

2.3 基于触发器的CDC(Trigger-based CDC)

这种方式是在源表上创建触发器。每当有INSERT、UPDATE、DELETE操作时,触发器把变更记录写到一张专门的日志表里。然后另一个进程从日志表里读取变更。

举个例子,MySQL的触发器大概长这样:

CREATE TRIGGER trg_orders_after_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
    INSERT INTO cdc_orders_log(
        operation, order_id, order_data, change_time
    ) VALUES (
        'INSERT', NEW.id, JSON_OBJECT(
            'order_no', NEW.order_no,
            'amount', NEW.amount,
            'status', NEW.status
        ), NOW()
    );
END;

你想想看,每次业务INSERT一条订单,触发器就自动往日志表里插一条记录。下游程序只需要轮询这个日志表就行。

优点:

  • 实时性极高,触发器是同步触发的
  • 可以自定义捕获逻辑,比如只捕获某些字段的变化
  • 不依赖数据库的特殊配置

缺点:

  • 影响源库性能——每次DML操作都要额外执行触发器逻辑,写入延迟增加
  • 维护成本高——表结构变更时,触发器也要跟着改
  • 容易造成死锁——如果触发器里又操作了业务表,可能产生嵌套调用
  • 日志表可能成为瓶颈——高并发下日志表写入压力很大

我的教训:曾经在一个高并发支付系统里,我用了触发器CDC。结果业务高峰期,触发器里的INSERT操作和业务事务争抢同一个缓冲池,导致整体TPS下降了30%。后来我果断换成了日志型CDC,性能问题才解决。所以触发器方案只适合低并发场景。

2.4 三种方式对比总结

对比维度 日志型CDC 查询型CDC 触发器型CDC
实时性 毫秒级 秒级~分钟级 毫秒级
对源库影响 低(只读日志) 中(查询压力) 高(额外写入)
捕获DELETE 支持 不支持(需软删除) 支持
实现复杂度 高 低 中
维护成本 低 低 高
数据库依赖 需开启日志 无特殊依赖 需触发器权限
典型工具 Debezium, Canal, Maxwell 自写脚本, Apache SeaTunnel 自定义触发器

2.5 如何选择?我的建议

我个人习惯按这个优先级来选:

  1. 首选日志型——只要数据库支持(MySQL binlog、PostgreSQL WAL、Oracle Redo Log),无脑选它。性能好、侵入低、功能全。
  2. 次选查询型——如果数据库版本太老、DBA不给开日志、或者只是临时做个小数据量的同步,用查询型快速搞定。
  3. 最后考虑触发器型——除非你有非常特殊的定制需求,比如只捕获某些字段的变更、或者需要做复杂的转换逻辑。否则尽量别碰,维护起来真的很头疼。

一个小技巧:如果你刚开始接触CDC,我建议先从查询型入手,把整个同步链路跑通。等理解了数据流转的全过程,再切换到日志型。这样即使出问题,你也知道该从哪里排查。

好了,三种CDC方式的原理就讲到这里。记住一句话:日志型是王道,查询型是备胎,触发器型是特种兵。选对方案,你的数据同步之路就成功了一半。