2、CDC核心原理:三种主流实现方式
CDC(Change Data Capture)说白了就是「逮住数据的变化」。我在做数据同步项目时,最常被问到的就是:「到底该用哪种方式抓取变更?」
今天咱们就把三种主流方案掰开揉碎讲清楚。我个人习惯把它们分成三类:基于日志、基于查询、基于触发器。每种都有各自的脾气秉性,选错了可是要吃苦头的。
核心观点:没有银弹。日志型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 如何选择?我的建议
我个人习惯按这个优先级来选:
- 首选日志型——只要数据库支持(MySQL binlog、PostgreSQL WAL、Oracle Redo Log),无脑选它。性能好、侵入低、功能全。
- 次选查询型——如果数据库版本太老、DBA不给开日志、或者只是临时做个小数据量的同步,用查询型快速搞定。
- 最后考虑触发器型——除非你有非常特殊的定制需求,比如只捕获某些字段的变更、或者需要做复杂的转换逻辑。否则尽量别碰,维护起来真的很头疼。
一个小技巧:如果你刚开始接触CDC,我建议先从查询型入手,把整个同步链路跑通。等理解了数据流转的全过程,再切换到日志型。这样即使出问题,你也知道该从哪里排查。
好了,三种CDC方式的原理就讲到这里。记住一句话:日志型是王道,查询型是备胎,触发器型是特种兵。选对方案,你的数据同步之路就成功了一半。