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 工具横向对比
| 工具 | 数据源 | 投递目标 | 特点 |
|---|---|---|---|
| Canal | MySQL binlog | Kafka / RocketMQ / 自研客户端 | 阿里开源,轻量,Java 生态友好,适合只做 MySQL→消息 |
| Debezium | MySQL / PG / MongoDB 等 | Kafka Connect | 支持多数据库,事件格式统一,生态成熟 |
| Flink CDC | MySQL / 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 就能稳稳地扛起你的实时数据管道。




