4. 数据库日志解析实战
CDC(Change Data Capture)实时数据集成的起点,几乎都落在数据库日志解析上。对 MySQL 而言,这个起点就是 binlog。理解 binlog 的物理格式、事件类型以及位点(position)与 GTID 的管理方式,决定了后续增量同步能否做到不丢、不重、可恢复。本节从 binlog 文件结构讲起,逐步落到事件解析与位点管理的工程实现。
一、binlog 的物理格式:从文件头到事件流
MySQL binlog 是一个二进制文件序列,每个文件由 4 字节魔数(0xfe62696e,即 "\xfe" + "bin")开头,之后是连续的事件(Event)流。每个事件由公共头(Common Header)和事件体(Event Body)组成。公共头固定 19 字节,字段含义如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| timestamp | 4 字节 | 事件发生时间戳(秒) |
| event_type | 1 字节 | 事件类型码,如 2=Query、30=WriteRows |
| server_id | 4 字节 | 产生该事件的 MySQL 实例 ID |
| event_size | 4 字节 | 整个事件长度(含公共头) |
| log_pos | 4 字节 | 下一个事件的起始位置 |
| flags | 2 字节 | 事件标志位 |
解析时先读 19 字节公共头,再根据 event_size 读取剩余字节,即可切分出完整事件。log_pos 字段非常关键——它天然给出了下一个事件的偏移,是位点推进的直接依据。
二、核心事件类型与解析要点
生产环境中真正需要处理的事件类型并不多,但每一类都有解析细节:
- FORMAT_DESCRIPTION_EVENT(类型 15):每个 binlog 文件的第一个事件,描述该文件使用的 binlog 版本与事件头长度。解析器必须先读它,才能正确解析后续事件。
- TABLE_MAP_EVENT(类型 19):建立 table_id 与库表、列定义的映射。行事件本身只携带 table_id,必须依赖它才能还原出表名和列结构。
- WRITE_ROWS_EVENT / UPDATE_ROWS_EVENT / DELETE_ROWS_EVENT(类型 30/31/32,v2):真正的行变更数据。UPDATE 事件包含 before-image 和 after-image 两段,用于识别变更前后的值。
- QUERY_EVENT(类型 2):记录 DDL 或 BEGIN/COMMIT 等语句。DDL 会改变表结构,解析器需要据此刷新 TABLE_MAP 缓存。
- XID_EVENT(类型 16):事务提交标志,收到它才意味着一个事务完整结束,是位点安全推进的边界。
行事件的解析难点在于列编码。MySQL 使用 bitmap 标记哪些列存在、哪些列被更新,再按列类型逐个解码。例如整型按小端序读取,VARCHAR 先读长度再读内容,NULL 列由 null-bitmap 单独标识。解析器必须严格按 TABLE_MAP 中的列元数据来解码,否则会错位。
三、位点管理:position 与 GTID 的取舍
位点决定了“从哪里继续读”。MySQL 提供两种位点标识:
- file + position:如
mysql-bin.000007:15432,表示从 7 号文件的 15432 偏移继续。优点是直观、兼容所有版本;缺点是主从切换或文件轮转后需要人工换算。 - GTID:如
3e11fa47-71ca-11e1-9e33-c80aa9429562:1-5,全局唯一事务标识。主从切换后无需换算,天然支持断点续传与多源合并,是现代 CDC 的首选。
位点推进必须遵循“先处理、后提交”的原则。正确顺序是:解析事件 → 写入下游 → 记录位点。若先记位点再写下游,一旦下游失败就会丢数据;若下游写入成功但位点未记,重启后会重复消费,因此下游需具备幂等能力。
四、解析与位点推进流程
下图展示了一个 CDC 解析器从连接 MySQL 到安全推进位点的完整数据流,包含事件解析、事务边界识别与位点提交三个关键环节:
五、工程实现中的关键细节
实际编码时,有几个容易踩坑的点值得强调。第一,事件头长度并非固定 19 字节,v4 格式下 FORMAT_DESCRIPTION_EVENT 会声明事件头长度,解析器应动态读取而非硬编码。第二,行事件的列 bitmap 分为 before-image 和 after-image 两段,UPDATE 事件两段都要解析,DELETE 事件只有 before-image,INSERT 事件只有 after-image。第三,大事务会跨越多个事件,必须等到 XID_EVENT 才能确认提交,中途不能推进位点。
下面是一段伪代码,展示位点安全推进的核心逻辑:
while (true) {
event = readEvent(stream); // 读 19B 头 + body
switch (event.type) {
case TABLE_MAP:
tableCache.put(event.tableId, event.meta);
break;
case WRITE_ROWS:
case UPDATE_ROWS:
case DELETE_ROWS:
row = decodeRow(event, tableCache.get(event.tableId));
downstream.write(row); // 先写下游
break;
case XID:
positionStore.commit(event.nextPos); // 事务结束才提交位点
break;
}
}
位点存储建议使用支持事务的外部存储(如 MySQL 表、ZooKeeper、Kafka 内部 topic),并记录 GTID set 而非单一 position,以便在主从切换后自动定位。若使用 file+position,则需同时记录 binlog 文件名与偏移,并在文件轮转时更新文件名。
总结来看,MySQL binlog 解析的核心是“按公共头切分事件、按 TABLE_MAP 解码行数据、按 XID 确认事务边界、按 GTID 或 file+pos 管理位点”。把这四件事做扎实,CDC 链路的“不丢不重”就有了基础保障。