第2章:CDC技术选型——四种主流方案深度对比

做CDC选型,说白了就是回答一个问题:我怎么才能又快又准地抓到数据库的变化?

我在过去几年里,几乎把这四种方案都踩过一遍坑。今天咱们就掰开揉碎,把基于日志的、基于触发器的、基于时间戳的、基于API的CDC,一个个说清楚。

核心结论先放这:没有银弹。选型取决于你的业务容忍度、数据库压力、以及团队运维能力。

CDC 四种技术选型对比 CDC 技术选型 基于日志的CDC 基于触发器的CDC 基于时间戳的CDC 基于API的CDC Debezium / Canal MySQL Trigger / PG Trigger modified_at / version REST / GraphQL Polling ✅ 实时、低侵入 ❌ 依赖Binlog格式 ✅ 精准、可控 ❌ 性能开销大 ✅ 实现简单 ❌ 无法捕获删除 ✅ 通用性强 ❌ 轮询延迟高 推荐:实时同步、数仓 推荐:审计、小表 推荐:离线分析 推荐:外部系统集成

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 以内。

工作原理大致是这样:

  1. Debezium 连接 MySQL,模拟一个 Slave 节点
  2. MySQL 把 Binlog 推给 Debezium
  3. Debezium 解析 Binlog 事件,转为统一的 Change Event 格式
  4. 发送到 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 我的选型建议

如果你问我怎么选,我一般按这个优先级来:

  1. 能上日志CDC,就上日志CDC。Debezium + Kafka 是目前生产环境最稳的组合。
  2. 日志CDC上不了,考虑触发器。但只在小表、低频变更的场景用。
  3. 时间戳方案只做兜底。比如日志CDC挂了,临时切到时间戳保证数据不丢。
  4. API方案是最后的选择。除非你确实拿不到数据库权限。

一句话总结:日志CDC是首选,触发器是备选,时间戳是兜底,API是无奈之举。