4、MySQL Binlog原理:Binlog格式、写入读取机制与GTID模式

聊到CDC增量同步,Binlog是绕不开的核心。说白了,MySQL的Binlog就是一份「操作日记」,记录了所有数据变更。我刚开始接触CDC时,以为只要开了Binlog就能随便同步,结果被格式和模式坑了好几次。今天咱们把这块彻底捋清楚。

4.1 Binlog的三种格式

MySQL提供了三种Binlog格式:Statement、Row和Mixed。每种格式都有自己的脾气,选错了轻则数据不一致,重则同步直接崩掉。

格式 记录方式 优点 缺点
Statement 记录执行的SQL语句 日志量小,易于阅读 非确定性函数可能导致主从不一致
Row 记录每一行数据的变化 精确可靠,任何情况都一致 日志量大,尤其大事务
Mixed 混合模式,自动选择 兼顾两者优点 逻辑复杂,偶有意外

4.1.1 Statement格式

Statement格式记录的是原始SQL。比如你执行UPDATE user SET age=age+1 WHERE id=1,Binlog里就原样记下这条SQL。从库重放时再执行一遍。

听起来挺合理对吧?但问题来了——非确定性函数。比如NOW()、UUID()、RAND(),主库执行时是一个值,从库重放时又是另一个值。我曾经遇到一个案例,主库用SYSDATE()做分区键,结果从库数据全跑偏了。排查了一整天才发现是Statement格式惹的祸。

注意:使用Statement格式时,务必确认所有SQL都是确定性的。包含NOW()、UUID()、RAND()等函数的操作,建议改用Row格式。

4.1.2 Row格式

Row格式记录的是每一行数据变更前后的完整镜像。同样是UPDATE user SET age=age+1 WHERE id=1,Row格式会记录:

# 变更前:id=1, name='张三', age=25
# 变更后:id=1, name='张三', age=26

这有什么好处?精确。不管你怎么折腾,从库重放时拿到的就是确定的数据。CDC工具(比如Canal、Debezium)也最喜欢Row格式,因为可以直接解析出变更前后的数据,不需要再回查数据库。

但代价也很明显——日志量暴增。一个UPDATE影响100万行,Statement格式可能就一行SQL,Row格式要记录100万行的前后镜像。我见过一个极端案例,某电商大促时一个批量更新产生了50GB的Binlog,直接把磁盘撑爆了。

建议:生产环境做CDC增量同步,优先选择Row格式。虽然日志量大,但数据一致性有保障。磁盘不够?加磁盘。数据错了?那可不是加磁盘能解决的。

4.1.3 Mixed格式

Mixed格式是MySQL的「智能选择」模式。默认情况下使用Statement格式,遇到非确定性语句时自动切换为Row格式。听起来很完美?实际用起来有点尴尬。

我个人不太推荐Mixed格式做CDC。为什么?因为CDC工具解析时,需要同时处理两种格式的解析逻辑,增加了复杂度。而且「自动切换」的时机并不总是符合预期。我曾经踩过一个坑:某个存储过程里混合了确定性和非确定性操作,Mixed格式把整个事务都切成了Row,日志量暴涨,但CDC工具却因为解析逻辑bug丢了一部分数据。

4.2 Binlog的写入与读取机制

理解了格式,咱们再看看Binlog到底是怎么写进去、怎么读出来的。这部分对理解CDC的延迟和性能至关重要。

4.2.1 写入机制

Binlog的写入流程大致如下:

  1. 事务执行:MySQL Server层执行SQL,生成事件(Event)。
  2. 写入Binlog Cache:每个事务的Binlog事件先写入线程私有的Binlog Cache。
  3. 刷盘:事务提交时,将Cache中的内容写入Binlog文件,并根据sync_binlog参数决定是否刷盘。
  4. 通知从库/CDC:写入完成后,通知Dump线程有新数据可读。

这里有个关键参数——sync_binlog:

值 行为 性能 安全性
0 由操作系统决定刷盘时机 最高 最低(宕机可能丢数据)
1 每次事务提交都刷盘 最低 最高(不丢数据)
N 每N个事务提交刷一次盘 中等 中等(最多丢N个事务)

做CDC时,我建议sync_binlog设为1。虽然写入性能会下降,但能保证CDC工具读到完整的数据。你想想看,如果CDC刚读到一半,主库宕机丢了最后几个事务,那同步的数据就不完整了。

4.2.2 读取机制

Binlog的读取依赖Dump线程。CDC工具(或从库)连接到主库后,主库会启动一个Dump线程,负责把Binlog事件推送给客户端。

读取流程:

  1. 注册Slave:客户端发送COM_REGISTER_SLAVE命令注册自己。
  2. 请求Binlog:客户端发送COM_BINLOG_DUMP或COM_BINLOG_DUMP_GTID,指定起始位置。
  3. 持续推送:Dump线程从指定位置开始,持续推送Binlog事件。
  4. ACK确认:客户端处理完事件后,发送ACK确认位置。

关键点:Dump线程是单线程推送的。如果CDC工具处理速度跟不上,Dump线程会积压事件,导致主库的Binlog文件无法及时清理。我曾经遇到一个案例,CDC工具挂了半天,Binlog文件堆积到200GB,恢复后Dump线程追了好久才追上。

4.3 GTID模式

GTID(Global Transaction Identifier)是MySQL 5.6引入的杀手级特性。它给每个事务分配一个全局唯一的ID,格式为UUID:序列号。比如d3b1c2a4-5e6f-7890-abcd-ef1234567890:12345。

4.3.1 为什么需要GTID?

没有GTID时,主从同步依赖文件名+偏移量。比如mysql-bin.000001:12345。这有什么问题?

  • 主库切换后位置失效:新主库的Binlog文件名和偏移量完全不同,从库需要重新找位置。
  • 难以判断事务是否已执行:从库不知道某个事务是否已经重放过,容易重复执行。
  • 搭建复制链路复杂:需要手动指定MASTER_LOG_FILE和MASTER_LOG_POS。

GTID完美解决了这些问题。每个事务有唯一ID,从库可以精确知道哪些事务已经执行过,哪些还没执行。主库切换时,从库自动从GTID集合中找出缺失的事务继续同步。

4.3.2 GTID的生命周期

一个事务从产生到被CDC工具消费,GTID经历了以下阶段:

  1. 分配:事务开始时,MySQL分配一个新的GTID。
  2. 写入:事务提交时,GTID随Binlog事件一起写入。
  3. 同步:从库或CDC工具读取Binlog,获取GTID。
  4. 记录:从库将已执行的GTID记录到gtid_executed表中。
  5. 清理:MySQL根据gtid_purged清理已过期的GTID。
经验之谈:使用GTID模式做CDC时,一定要记录好消费过的GTID集合。我曾经遇到一个同事,CDC工具重启后忘了记录GTID位置,结果从最新的Binlog开始消费,漏掉了中间好几个小时的数据。后来我们统一用ZooKeeper或数据库表来持久化GTID消费进度。

4.3.3 GTID模式的配置

开启GTID模式需要修改MySQL配置文件:

[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
log_bin = mysql-bin
binlog_format = ROW
server_id = 1

注意enforce_gtid_consistency必须设为ON,否则MySQL会禁止可能导致GTID不一致的语句(比如CREATE TABLE ... SELECT)。

4.3.4 GTID与CDC的配合

CDC工具(如Canal、Debezium)都原生支持GTID模式。连接时只需要指定GTID集合的起始位置,工具会自动从该位置开始消费。

举个例子,Canal连接GTID模式的MySQL:

canal.instance.master.address = 192.168.1.100:3306
canal.instance.dbUsername = canal
canal.instance.dbPassword = canal
canal.instance.gtidon = true
# 指定起始GTID集合,为空则从最新位置开始
canal.instance.gtid.start = d3b1c2a4-5e6f-7890-abcd-ef1234567890:1-1000

我个人习惯在生产环境强制使用GTID模式。虽然配置多了一步,但换来的是主库切换时的零人工干预。有一次线上主库硬件故障,我们直接把CDC工具指向新主库,GTID自动对齐,全程没丢一条数据。

4.4 本章核心知识图谱

下面这张图展示了Binlog格式、写入读取机制和GTID模式之间的关系:

Binlog核心知识体系 MySQL Binlog Binlog格式 Statement Row Mixed 写入与读取机制 写入流程 Dump线程 sync_binlog GTID模式 全局唯一ID 自动对齐 主从切换 核心结论:CDC增量同步首选 Row格式 + GTID模式,兼顾数据一致性与运维便捷性

4.5 避坑总结

最后,我把这些年踩过的坑和总结的经验列出来,希望能帮你少走弯路:

  • 格式选择:做CDC增量同步,无脑选Row格式。Statement格式省的那点磁盘空间,不够你排查一次数据不一致的。
  • sync_binlog:设为1,别犹豫。CDC工具读到一半数据丢了,比性能下降更可怕。
  • GTID持久化:一定要把消费过的GTID集合持久化到外部存储(ZooKeeper、数据库、文件都行)。CDC工具重启时,从持久化位置继续消费,避免重复或遗漏。
  • 监控Binlog堆积:定期检查Binlog文件数量和磁盘使用率。CDC工具挂了要及时恢复,否则Binlog堆积会撑爆磁盘。
  • 大事务处理:Row格式下,大事务会产生巨量Binlog。建议拆分大事务,或者对CDC工具做限流处理。
血的教训:我曾经在生产环境遇到一个Bug——某个定时任务每5分钟执行一次UPDATE全表,Row格式下每次产生2GB Binlog。CDC工具处理不过来,Dump线程积压,最终Binlog文件堆积到300GB,磁盘报警。从那以后,我强制要求所有批量操作必须分页执行,并且监控Binlog生成速率。