CDC 数据变更捕获实战:从 binlog 到实时同步

CDC 数据变更捕获(Change Data Capture)是一种实时感知数据库增删改的技术:它不靠定时轮询,而是订阅数据库的事务日志(如 MySQL 的 binlog、PostgreSQL 的逻辑复制流),把每一行数据的变化按发生顺序捕获出来。借助 CDC,你可以把生产库的变更近乎实时地同步到缓存、数仓或消息队列,从而解耦读写、构建实时数据链路。本文从原理到落地,带你把 CDC 真正用起来。

一、为什么需要 CDC

传统的数据同步大多靠“定时任务 + 全表扫描”或“在业务代码里双写”。前者延迟高、对主库压力大;后者容易产生不一致——只要有一处写入遗漏,数据就开始漂移。CDC 的出现就是为了解决这两类痛点:它站在数据库引擎之外,只读取已经提交的事务日志,因此既不侵入业务代码,也不增加线上查询负担。

举一个最常见的场景:缓存与数据库的一致性。很多团队用“先更新数据库,再删缓存”的策略,但面对高并发依然会出现短暂脏读。如果改用 CDC 订阅数据库的变更事件,由独立的消费者去失效或刷新缓存,就能把“数据变了”这个事实以事件的形式可靠地广播出去,链路更健壮。关于缓存一致性的工程细节,可参考我们之前的《缓存与数据库一致性实战:延迟双删与 binlog 订阅》。

二、CDC 的两条技术路线

从实现原理看,CDC 大体分为两类:

  • 查询式 CDC:基于时间戳、自增 ID 或版本号,周期性地 SELECT 出“最近变化”的行。实现简单,但无法捕获删除操作(行没了就 SELECT 不到),且对大表有扫描压力。
  • 日志式 CDC:直接解析数据库的事务日志(binlog / WAL / logical replication slot),捕获 INSERT、UPDATE、DELETE 的完整前后镜像。这是生产级 CDC 的主流方案,延迟可做到秒级甚至亚秒级。

本文聚焦日志式 CDC,因为它能覆盖“删”这一最难同步的操作,也是 Canal、Debezium、Flink CDC 等工具的核心能力。

三、MySQL binlog 是怎么被捕获的

MySQL 的 binlog 本质是一份按事务顺序追加的“操作流水”。当它工作在 ROW 格式时,每一条变更都会记录受影响行的完整列值(UPDATE 还会同时保留前镜像与后镜像)。CDC 客户端以“从库”身份连上 MySQL,发送 COM_BINLOG_DUMP 命令,从一个指定的 binlog 位点(file + position)开始,持续拉取事件流。

关键点在于位点(position):只要消费者把“已处理到的位点”持久化下来,即使进程重启,也能从断点续传,不丢不重。这也是 CDC 可靠性的根基。

四、主流 CDC 工具横向对比

工具数据源投递目标特点
CanalMySQL binlogKafka / RocketMQ / 自研客户端阿里开源,轻量,Java 生态友好,适合只做 MySQL→消息
DebeziumMySQL / PG / MongoDB 等Kafka Connect支持多数据库,事件格式统一,生态成熟
Flink CDCMySQL / PG / Oracle 等Flink 作业(任意 Sink)带计算能力,可做清洗、打宽、聚合后再写

如果目标只是“把变更丢进 Kafka 给别的系统消费”,Canal 或 Debezium 足够;如果要在同步过程中做转换(比如把多张表 join 成宽表再落库),Flink CDC 更顺手。

五、实战:Canal 捕获 MySQL 变更

Canal 的服务端负责模拟 MySQL 从库、解析 binlog;业务侧用 Canal 客户端接收变更消息。下面是一段订阅并处理订单表变更的 Java 伪代码:

// Canal 客户端消费示例(简化)
CanalConnector connector = CanalConnectors.newSingleConnector(
    new InetSocketAddress("127.0.0.1", 11111), "example", "", "");
connector.connect();
connector.subscribe("shop\\.orders");   // 只订阅 shop 库的 orders 表

while (true) {
    Message message = connector.getWithoutAck(1000); // 一次最多拉 1000 条
    long batchId = message.getId();
    for (Entry entry : message.getEntries()) {
        if (entry.getEntryType() != EntryType.ROWDATA) continue;
        RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
        EventType eventType = rowChange.getEventType(); // INSERT/UPDATE/DELETE
        for (RowData rowData : rowChange.getRowDatasList()) {
            // 拿到变更前后的列值,发往下游(Kafka / 缓存失效)
            handleChange(eventType, rowData.getAfterColumnsList());
        }
    }
    connector.ack(batchId); // 处理成功才提交位点
}

注意 ack 的时机:必须等变更真正被下游消费成功后再确认,否则进程崩溃会丢失这批数据。这也是“至少一次(at-least-once)”语义的来源。

六、实战:Debezium 连接器配置

Debezium 以 Kafka Connect 插件形式运行,一份 JSON 配置即可拉起一个 MySQL 源连接器:

{
  "name": "mysql-cdc-connector",
  "config": {
    "connector.class": "io.debezium.connector.mysql.MySqlConnector",
    "database.hostname": "mysql-prod",
    "database.port": "3306",
    "database.user": "cdc_user",
    "database.password": "******",
    "database.server.id": "184054",
    "database.server.name": "mysql_prod",
    "database.include.list": "shop",
    "table.include.list": "shop.orders,shop.users",
    "topic.prefix": "cdc",
    "snapshot.mode": "initial"
  }
}

启动后,Debezium 会先在 snapshot.mode=initial 下对存量数据做一次全量快照,再无缝切换到增量 binlog 流,保证历史数据和实时变更不漏。PostgreSQL 侧的原理类似,只是底层走逻辑复制槽,细节可看我们的《PostgreSQL 逻辑复制实战:实时数据同步与分发》。

七、实战:Flink CDC 做实时打宽

当同步过程需要计算时,Flink CDC 优势明显。下面这条 SQL 直接把 MySQL 的订单流与维表关联,输出“带用户信息的订单”宽表:

-- Flink CDC SQL(简化)
CREATE TABLE orders (
  id BIGINT, user_id BIGINT, amount DECIMAL(10,2), ts TIMESTAMP(3)
) WITH (
  'connector' = 'mysql-cdc',
  'hostname' = 'mysql-prod',
  'port' = '3306',
  'username' = 'cdc_user',
  'password' = '******',
  'database-name' = 'shop',
  'table-name' = 'orders'
);

INSERT INTO order_wide
SELECT o.id, o.amount, u.name, u.city
FROM orders o
JOIN users FOR SYSTEM_TIME AS OF o.ts AS u
  ON o.user_id = u.id;

相比“先同步再离线 join”,这种在流上直接打宽的方式能把端到端延迟从小时级压到秒级,非常适合实时大屏、风控等场景。

八、CDC 的五个生产级坑

1. 重复消费与幂等

CDC 通常只能保证“至少一次”。同一行变更可能因为重平衡、位点回退而被投递两次。下游写入必须幂等——例如用 (表名 + 主键 + 业务版本号) 做 upsert,或用主键去重。

2. 事件顺序

同一主键的 UPDATE 必须按 binlog 顺序到达,否则会“后到的旧值覆盖新值”。分区键务必用主键,避免同一行被打散到不同分区。

3. Schema 变更

线上加字段是常态。Canal/Debezium 能感知 DDL 并带上 schema 版本,但下游消费者要能容忍“旧事件少字段、新事件多字段”,否则反序列化会失败。

4. 延迟与积压

大事务(如一次性 UPDATE 十万行)会瞬间产生海量事件,撑爆内存或造成消费滞后。建议在对端做限流、批处理,并监控“位点滞后(lag)”指标。

5. 断点续传

位点必须落库而不是只存在内存。失败的批次绝不要 ack,下次从最后成功的位点重拉即可,这是 CDC“不丢”的底线。

九、典型应用场景一览

场景做法收益
缓存失效捕获 DB 变更 → 删/刷 Redis彻底解决缓存与 DB 不一致
数仓/ES 同步变更流入 Kafka → 落仓/建索引近实时分析,无需批量抽数
微服务解耦主库变更 → 领域事件其他服务异步感知,去掉强依赖
审计与留痕变更流持久化到审计表完整数据血缘,合规可查

当你把“数据库变了”变成一个可在系统内流动的事件,很多原本耦合的架构就能顺势解耦。CDC 不是银弹,但它是构建实时数据链路的基石能力。如果你的主库是 PostgreSQL,也可以从逻辑复制这条路径平滑切入,思路与 binlog 一脉相承。

总结一句:选工具看目标(只同步用 Canal/Debezium,要计算用 Flink CDC),落地时死磕幂等、顺序、位点三件事,CDC 就能稳稳地扛起你的实时数据管道。

上一篇 微服务陷阱:分布式架构下 7 个最常见的坑与避坑清单
下一篇 Zod 实战:TypeScript 运行时类型校验全攻略
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]