第2章:CDC技术选型——四种主流方案深度对比
做CDC选型,说白了就是回答一个问题:我怎么才能又快又准地抓到数据库的变化?
我在过去几年里,几乎把这四种方案都踩过一遍坑。今天咱们就掰开揉碎,把基于日志的、基于触发器的、基于时间戳的、基于API的CDC,一个个说清楚。
核心结论先放这:没有银弹。选型取决于你的业务容忍度、数据库压力、以及团队运维能力。
2.1 基于日志的CDC——我最推荐的生产方案
基于日志的CDC,核心思路是:直接读取数据库的事务日志(Binlog、WAL、Redo Log),解析出数据变更事件。
我个人的习惯是,只要数据库版本支持、团队有运维能力,优先选这个方案。为什么?因为它对源库几乎零侵入。
代表工具:
- Debezium(基于Kafka Connect,支持MySQL、PostgreSQL、MongoDB、SQL Server等)
- Canal(阿里开源,主要针对MySQL Binlog)
- Maxwell(也是MySQL,输出JSON到Kafka)
我的经验:Debezium 配合 Kafka,是目前最成熟的日志CDC方案。我在一个日活千万的电商项目中,用 Debezium 同步订单数据到 Elasticsearch,延迟控制在 200ms 以内。
工作原理大致是这样:
- Debezium 连接 MySQL,模拟一个 Slave 节点
- MySQL 把 Binlog 推给 Debezium
- Debezium 解析 Binlog 事件,转为统一的 Change Event 格式
- 发送到 Kafka Topic,下游消费
// Debezium 输出的典型 Change Event(简化)
{
"op": "c", // c=create, u=update, d=delete, r=read
"ts_ms": 1700000000000,
"before": null,
"after": {
"id": 1001,
"username": "zhangsan",
"email": "zhangsan@example.com"
},
"source": {
"db": "user_db",
"table": "users",
"snapshot": false
}
}
注意:Binlog 格式必须设置为 ROW 模式。如果用的是 STATEMENT 或 MIXED,Debezium 无法正确解析。我曾经接手过一个项目,Binlog 是 STATEMENT 模式,结果 Debezium 只能捕获到部分更新,排查了两天才发现是配置问题。
2.2 基于触发器的CDC——精准但代价高
基于触发器的CDC,就是在源表上创建 INSERT、UPDATE、DELETE 触发器,把变更记录写入一张专门的变更日志表。
说实话,这个方案我一般只在两种场景下用:
- 数据库不支持日志解析(比如某些老版本 MySQL、SQLite)
- 需要非常精细的变更审计,比如记录“谁在什么时间改了哪个字段”
-- MySQL 触发器示例
CREATE TRIGGER trg_users_audit
AFTER UPDATE ON users
FOR EACH ROW
BEGIN
INSERT INTO users_audit_log (
user_id, old_email, new_email, changed_by, changed_at
) VALUES (
OLD.id, OLD.email, NEW.email, CURRENT_USER(), NOW()
);
END;
但你要想清楚代价:
- 性能开销:每个 DML 操作都多一次写入,高并发场景下触发器可能成为瓶颈
- 维护成本:表结构变更时,触发器也要同步更新,容易遗漏
- 事务放大:触发器里的操作和原事务在同一个事务里,如果审计表出问题,源表也写不进去
避坑指南:我曾经在一个金融项目中,用触发器做变更追踪。上线后数据库负载飙升 30%,排查发现触发器里有个慢查询。后来我们改成异步写审计日志,才把性能拉回来。所以——触发器里千万别做复杂逻辑,只做最简单的 INSERT。
2.3 基于时间戳的CDC——简单但有限
这个方案最直观:在表里加一个 modified_at 或 version 字段,定时查询“上次同步之后有哪些记录变了”。
实现起来确实简单:
-- 查询最近变更的数据
SELECT * FROM users
WHERE modified_at > '2024-01-01 00:00:00'
ORDER BY modified_at ASC
LIMIT 1000;
但它的硬伤也很明显:
- 无法捕获 DELETE 操作——记录都删了,你还怎么查?
- 依赖业务代码正确维护时间戳——如果某个更新忘了设 modified_at,数据就丢了
- 时间精度问题——同一毫秒内多条变更,可能漏掉或重复
我的建议:时间戳方案只适合做离线批处理,或者对实时性要求不高的场景。比如每天凌晨同步一次商品信息到搜索引擎,这种场景用时间戳完全够用。
2.4 基于API的CDC——通用但轮询成本高
基于API的CDC,说白了就是:通过调用业务系统的API接口,轮询获取变更数据。
这种方案常见于:
- 你无法直接访问数据库(比如 SaaS 系统、第三方平台)
- 数据库权限受限,不允许开 Binlog 或触发器
- 需要从多个异构系统汇聚数据
// 伪代码:轮询 API 获取变更
async function pollChanges(lastSyncTime) {
const response = await fetch(
`https://api.example.com/v1/changes?since=${lastSyncTime}`
);
const changes = await response.json();
for (const change of changes) {
await processChange(change);
}
return changes.length > 0 ? changes[changes.length-1].timestamp : lastSyncTime;
}
但你要面对的问题:
- 轮询间隔决定了实时性——间隔短,API 压力大;间隔长,数据延迟高
- API 限流和分页——很多 API 有调用次数限制,数据量大时分页逻辑很头疼
- 数据一致性难保证——API 返回的数据可能是最终一致性的,中间状态可能丢失
注意:如果 API 不支持增量查询(只支持全量导出),那这个方案基本不可用。我见过一个团队用全量 API 做 CDC,每 5 分钟拉一次全量数据,结果把对方服务器打挂了……
2.5 四种方案对比总结
| 维度 | 基于日志 | 基于触发器 | 基于时间戳 | 基于API |
|---|---|---|---|---|
| 实时性 | 毫秒级 | 秒级(事务内) | 分钟级(取决于轮询间隔) | 秒~分钟级 |
| 源库侵入性 | 低(只读日志) | 高(额外写入) | 低(只加字段) | 低(只调API) |
| 捕获删除 | ✅ 支持 | ✅ 支持 | ❌ 不支持 | ✅ 取决于API设计 |
| 数据一致性 | 高(Exactly-Once) | 高(事务内) | 中(可能漏数据) | 低(最终一致性) |
| 运维复杂度 | 中高(需维护Kafka等) | 中(需管理触发器) | 低 | 中(需处理限流) |
| 适用场景 | 实时同步、数仓入湖 | 审计、小表变更追踪 | 离线批处理、非核心数据 | 外部系统集成、SaaS |
2.6 我的选型建议
如果你问我怎么选,我一般按这个优先级来:
- 能上日志CDC,就上日志CDC。Debezium + Kafka 是目前生产环境最稳的组合。
- 日志CDC上不了,考虑触发器。但只在小表、低频变更的场景用。
- 时间戳方案只做兜底。比如日志CDC挂了,临时切到时间戳保证数据不丢。
- API方案是最后的选择。除非你确实拿不到数据库权限。
一句话总结:日志CDC是首选,触发器是备选,时间戳是兜底,API是无奈之举。