2. CDC 工作原理:日志解析机制、增量捕获原理、数据变更事件模型、事务一致性保证
CDC 的核心,说白了就是「偷看」数据库的变更日志。数据库自己记的账本,我们拿来读一读,再转成统一的事件流。这听起来简单,但里面的门道不少。我最早接触 CDC 时,以为就是读个日志文件而已,结果被事务乱序、数据不一致这些问题折腾得够呛。今天咱们就把这层窗户纸捅破。
2.1 日志解析机制:数据库的「黑匣子」
几乎所有主流数据库都会记录事务日志。MySQL 的 binlog、PostgreSQL 的 WAL、Oracle 的 Redo Log,本质上都是同一个东西——数据库的「黑匣子」。每次数据变更,数据库先写日志,再改数据页。CDC 工具就是从这个黑匣子里把变更记录一条条读出来。
核心要点:日志解析不是轮询查表,而是基于日志流的事件驱动。CDC 工具作为日志的「消费者」,实时读取新增的日志条目,解析成结构化事件。
解析过程大致分三步:
- 连接日志流——CDC 工具模拟成一个备库或日志消费者,向数据库请求日志流。
- 读取原始日志——拿到二进制格式的日志条目,比如 MySQL 的 binlog event。
- 反序列化——把二进制数据还原成表结构、字段值、操作类型等可读信息。
我在项目中遇到过一个问题:MySQL binlog 有 ROW、STATEMENT、MIXED 三种格式。如果用了 STATEMENT 格式,日志里只记录 SQL 语句,不记录具体改了多少行。CDC 工具拿到「UPDATE t SET x=1 WHERE id>100」这种日志,根本不知道影响了哪些行。所以我个人习惯,生产环境一律用 ROW 格式,这样每条变更都能精确到行。
经验之谈:MySQL 8.0 之后,binlog_row_image 参数默认是 FULL,会记录所有字段的旧值和新值。如果你只关心变更字段,可以设为 MINIMAL,能减少日志体积。但要注意,某些 CDC 工具依赖旧值做冲突检测,设成 MINIMAL 可能出问题。
2.2 增量捕获原理:从「全量」到「增量」的切换
CDC 的增量捕获,不是一上来就盯着日志看。实际部署时,通常先做一次全量快照,再切换到增量模式。这个切换点,就是 CDC 的「断点续传」基础。
增量捕获的核心机制:
- 日志偏移量(Offset)——记录当前读到日志的哪个位置。比如 MySQL 的 binlog 文件名 + 偏移量,或者 PostgreSQL 的 LSN(日志序列号)。
- 断点续传——CDC 工具定期把偏移量持久化到外部存储(比如 Kafka、文件、数据库)。如果进程重启,从上次记录的位置继续读。
- 无锁捕获——增量模式下,CDC 工具只读日志,不锁表,对源库几乎零影响。
注意:日志文件有保留期限。MySQL binlog 默认保留 7 天,PostgreSQL WAL 可能被回收。如果 CDC 工具宕机时间超过日志保留期,断点续传就失效了,需要重新做全量同步。我曾经因为没监控日志保留时间,导致一次故障恢复后数据断层,补数据补了三天三夜。
全量到增量的切换流程,我画了张图:
2.3 数据变更事件模型:统一的事件结构
不同数据库的日志格式千差万别,但 CDC 工具会把这些差异抹平,输出统一的事件模型。你想想看,如果每接一个数据库就要写一套解析逻辑,那 CDC 工具就没法用了。
一个标准的数据变更事件,通常包含以下字段:
| 字段 | 说明 | 示例 |
|---|---|---|
| op | 操作类型:c=创建,u=更新,d=删除,r=快照读 | u |
| ts_ms | 事件发生时间戳(毫秒) | 1712345678000 |
| source | 源信息:数据库名、表名、日志偏移量等 | {db: "orders", table: "t_order"} |
| before | 变更前的数据(删除/更新时存在) | {id: 1, status: "pending"} |
| after | 变更后的数据(创建/更新时存在) | {id: 1, status: "paid"} |
| transaction | 所属事务 ID | tx:12345 |
以 Debezium 为例,一条更新事件的 JSON 大概长这样:
{
"op": "u",
"ts_ms": 1712345678000,
"source": {
"db": "orders",
"table": "t_order",
"lsn": 123456789
},
"before": {
"id": 1,
"status": "pending",
"amount": 100.00
},
"after": {
"id": 1,
"status": "paid",
"amount": 100.00
},
"transaction": {
"id": "tx:12345"
}
}
实用技巧:事件模型中的 before/after 字段,在数据同步场景下特别有用。比如做数据校验时,可以用 before 的值去源库比对,用 after 的值去目标库比对。我曾经用这个特性做过一个「数据一致性巡检工具」,跑一遍就能发现哪些行同步出了问题。
2.4 事务一致性保证:CDC 的「灵魂」
CDC 最难的地方,不是读日志,而是保证事务一致性。你想想看,一个事务可能改了 10 张表,日志里这 10 条变更记录是分散的。如果 CDC 工具一条读一条发,下游看到的就是「先看到订单已支付,后看到库存已扣减」——这明显违反了事务的原子性和隔离性。
事务一致性的三个层次:
- 单行一致性——单条变更记录不会丢失或重复。这是最基本的,靠日志偏移量保证。
- 单事务一致性——同一个事务内的所有变更,要么全部可见,要么全部不可见。这需要 CDC 工具识别事务边界。
- 跨事务一致性——多个事务的变更顺序,与它们在源库的提交顺序一致。这需要 CDC 工具按提交顺序输出事件。
实现方式主要有两种:
| 方式 | 原理 | 优缺点 |
|---|---|---|
| 事务缓冲 | CDC 工具在内存中缓存同一个事务的所有变更,等收到事务提交标记后,再一次性发送 | 优点:保证原子性;缺点:大事务会占用大量内存,增加延迟 |
| 事务标记 | 在日志中插入特殊标记(如 MySQL 的 GTID),下游根据标记判断事务边界 | 优点:内存开销小;缺点:需要下游支持事务标记解析 |
我个人更倾向于事务缓冲的方式。虽然大事务会吃内存,但可以通过配置事务大小阈值来规避。比如设置 max_batch_size 为 1000 条,超过这个数就强制拆分成多个批次,同时记录事务 ID 供下游做补偿处理。
避坑指南:我曾经遇到过一个坑——MySQL 的 binlog 里,事务提交标记(XID event)和最后一个变更 event 之间,可能隔着其他事务的 event。这是因为 MySQL 的组提交(group commit)机制。如果 CDC 工具只依赖 XID event 做事务边界判断,可能会把不同事务的变更混在一起。解决办法是:用 GTID 模式,或者自己维护一个事务 ID 到 event 的映射表。
最后说一句,事务一致性不是 CDC 工具单方面能保证的。下游消费端也要配合,比如使用 Kafka 的事务性生产者,或者实现幂等消费。否则,CDC 工具再努力,下游消费乱序也是白搭。