2、Binlog文件结构解析:物理存储、Magic Number与Event结构
Binlog 文件到底长什么样?这是很多 DBA 和开发同学问我的第一个问题。说实话,我刚开始接触 MySQL 主从复制时,也以为 Binlog 就是个简单的日志文件,每行记录一条 SQL。后来踩了坑才发现——完全不是那么回事。
Binlog 是二进制文件,不是文本文件。你不能用 cat 或者 vim 直接看。它有一套严格的物理存储格式,从文件头到每个事件,都有固定的结构。搞懂这个,你才能真正理解 CDC 是怎么工作的。
2.1 物理存储概览
一个 Binlog 文件,从磁盘角度看,就是一段连续的字节流。我习惯把它分成三个层次:
- 文件级:Magic Number + 多个 Event 串行排列
- 事件级:每个 Event 由 Header + Data 组成
- 字段级:Header 和 Data 内部又有固定偏移的字段
你想想看,这就像一本有固定章节结构的书。文件头是封面,每个事件是一页,页眉记录元信息,正文记录具体数据。
核心要点:Binlog 是 append-only 的写入模式。新事件永远追加到文件末尾,不会修改已写入的事件。这保证了数据捕获的线性顺序。
下面这张图是我自己画的结构总览,建议你多看几遍:
2.2 Magic Number:文件的身份证
每个 Binlog 文件的前 4 个字节是固定的,叫 Magic Number。它的值是 0xfe 0x62 0x69 0x6e。翻译成 ASCII 字符串就是 \xfe bin。
为什么要有这个东西?说白了,就是让 MySQL 或者解析工具能快速判断:这个文件是不是一个合法的 Binlog。我在项目中遇到过有人误把普通日志文件当成 Binlog 来解析,结果程序直接崩溃。后来我加了个 Magic Number 校验,问题就解决了。
小技巧:用 hexdump -C binlog.000001 | head -1 就能看到前 4 个字节。如果开头不是 fe 62 69 6e,那这个文件肯定有问题。
2.3 Event 结构:Binlog 的基本单元
Magic Number 之后,就是一个个 Event 首尾相连。每个 Event 都遵循同样的结构:
| 组成部分 | 大小 | 说明 |
|---|---|---|
| Event Header | 19 字节(固定) | 记录事件类型、时间戳、服务器 ID、事件长度等元信息 |
| Event Data | 可变长度 | 事件的具体负载,不同事件类型格式不同 |
嗯,这里要注意:Header 永远是 19 字节,这是 MySQL 官方定的标准。Data 的长度由 Header 中的 event_size 字段决定——event_size - 19 就是 Data 的实际大小。
2.4 Event Header 逐字段解析
Header 虽然只有 19 字节,但里面的信息量很大。我把它拆成 6 个字段,每个字段都有明确的偏移位置:
| 偏移 | 大小 | 字段名 | 说明 |
|---|---|---|---|
| 0 | 4 字节 | timestamp | 事件创建时的 Unix 时间戳(秒) |
| 4 | 1 字节 | type_code | 事件类型编号(如 2 表示 QUERY_EVENT) |
| 5 | 4 字节 | server_id | 产生该事件的 MySQL 服务器 ID |
| 9 | 4 字节 | event_size | 整个 Event 的总长度(Header + Data) |
| 13 | 4 字节 | log_pos | 该事件在 Binlog 文件中的起始偏移位置 |
| 17 | 2 字节 | flags | 标志位(如 LOG_EVENT_BINLOG_IN_USE_F) |
我个人觉得,log_pos 这个字段特别重要。它记录了事件在文件中的位置,CDC 工具就是靠它来做断点续传的。我曾经帮一个团队排查数据同步延迟问题,发现他们的解析器没有正确读取 log_pos,导致每次重启都要从头解析——那叫一个慢。
避坑指南:我曾经遇到过 MySQL 5.6 和 5.7 的 Header 解析差异。虽然 Header 结构本身没变,但某些事件类型的 flags 字段在 5.7 中新增了位含义。如果你在解析老版本 Binlog,一定要确认版本兼容性。
2.5 Event Data:负载的多样性
Data 部分没有固定结构,完全取决于 type_code。常见的 Event 类型有几十种,但 CDC 场景下我们最关心的是这几类:
- FORMAT_DESCRIPTION_EVENT (type_code=15):Binlog 文件的第一个事件,描述服务器版本和 Binlog 格式版本
- TABLE_MAP_EVENT (type_code=18):记录表 ID 与表结构的映射关系,是解析行事件的前提
- WRITE_ROWS_EVENT (type_code=30):INSERT 操作的行数据
- UPDATE_ROWS_EVENT (type_code=31):UPDATE 操作的行数据(包含旧值和新值)
- DELETE_ROWS_EVENT (type_code=32):DELETE 操作的行数据
- QUERY_EVENT (type_code=2):DDL 语句或基于语句的 DML
- XID_EVENT (type_code=16):事务提交标记
你想想看,一个完整的事务在 Binlog 里是什么样子?通常是:
GTID_EVENT (如果有开启 GTID)
→ BEGIN 标记 (QUERY_EVENT)
→ TABLE_MAP_EVENT (表结构映射)
→ WRITE_ROWS_EVENT / UPDATE_ROWS_EVENT / DELETE_ROWS_EVENT (行变更)
→ XID_EVENT (事务提交)
这个顺序是固定的。解析器必须按这个顺序来,否则拿到的数据就是乱的。
2.6 实战:如何手动解析一个 Event
光说不练假把式。我们拿一个真实的 Binlog 片段来走一遍解析流程。假设我用 hexdump 抓到了以下字节(十六进制):
00000000 fe 62 69 6e 00 00 00 00 00 00 00 01 00 00 00 00 |.bin............|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
前 4 个字节 fe 62 69 6e 是 Magic Number,确认是 Binlog 文件。接下来就是第一个 Event 的 Header:
- timestamp (偏移 0-3):
00 00 00 00→ 0,表示时间戳为 0(这是 FORMAT_DESCRIPTION_EVENT 的特殊情况) - type_code (偏移 4):
0f→ 15,确认是 FORMAT_DESCRIPTION_EVENT - server_id (偏移 5-8):
00 00 00 01→ 1,服务器 ID 为 1 - event_size (偏移 9-12):
00 00 00 00→ 这里需要继续读后面的字节才能确定
嗯,这里要注意:event_size 是 4 字节小端序。如果后面字节是 00 00 00 73,那总长度就是 115 字节,Data 部分就是 115 - 19 = 96 字节。
经验之谈:我建议你在写解析器时,先解析 Header 的 event_size,然后根据这个值跳过整个 Event。这样即使你不关心某些事件类型,也能快速定位到下一个事件。这是高效 CDC 解析器的基本功。
2.7 本章小结
Binlog 文件结构其实不复杂,但细节很多。记住三个关键点:
- 文件以 Magic Number 开头,用于校验合法性
- 每个 Event 由 19 字节 Header + 可变长度 Data 组成,Header 记录了时间戳、类型、长度、位置等关键信息
- Data 格式由 type_code 决定,CDC 场景下重点关注 FORMAT_DESCRIPTION、TABLE_MAP、行事件和 XID 事件
搞懂这些,你就掌握了 Binlog 解析的底层基础。下一层,我们会深入每个事件类型的 Data 格式,看看行数据到底是怎么编码的。