
做数据同步和变更追踪这几年我越来越觉得 binlog 是 MySQL 世界里最容易被低估的一类数据。它记录了每一次写操作无论是 insert、update 还是 delete甚至连表结构变更、事务提交都被完整地写进了这个二进制日志。问题在于一旦数据库跑到腾讯云这种托管环境里binlog 就不再像自建 MySQL 那样“唾手可得”了——你不能 SSH 到物理机上翻文件只能用平台提供的有限手段去取。因为这个别扭劲我花了两周时间整理了一个小工具 MysqlBinglogDigger专门解决“从腾讯云数据库拉取 binlog 并解析成可读内容”这件事。这篇文章把整体设计、关键代码思路和我在实际环境里踩过的坑都记录下来给正在做类似事情的同行一个参考。1. 为什么要跟 binlog 较劲需求背景与方案选型1.1 什么场景下必须读 binlog先说说我遇到的具体场景。公司有一套业务系统跑在腾讯云的 MySQL 上平时数据备份是有的但有一次运营同学在后台误操作把一张核心配置表的部分数据覆盖了。由于备份策略是每天凌晨全量备份当天白天的数据变更全部丢失。这种时候唯一的恢复依据就是 binlog——只要 binlog 里还有变更记录就可以把被覆盖前的数据重建出来。除此之外还有两个场景逼着我去碰 binlog。第一个是审计追溯业务方提了一个需求能不能查一下某个账号在某个时间段内到底改过哪些数据。光靠业务日志不够因为业务日志记录的是“操作行为”但数据库里的最终变更是什么还得看 binlog。第二个是异构数据同步需要把腾讯云 MySQL 里的数据变更实时同步到 Elasticsearch用于搜索。这种场景下binlog 就是天然的变更数据源。所以读 binlog 绝不是“闲着没事折腾”它直接对应数据恢复、审计、同步这三类硬需求。1.2 腾讯云数据库 binlog 和自建 MySQL 的差异如果你在自建 MySQL 上做过 binlog 相关的事情会习惯性地认为一切都是“文件操作”登录服务器找到 binlog 目录用 mysqlbinlog 直接解析完事。但腾讯云数据库无论是云数据库 MySQL 还是 TDSQL MySQL 版是托管形态用户没有底层操作系统的访问权限binlog 文件不直接暴露在某个路径下。差异主要体现在几个方面。第一binlog 文件不能直接通过文件系统读取需要通过控制台、API 或者远程连接协议来获取。第二云数据库默认开启了 binlog但 binlog_format 到底是 STATEMENT、ROW 还是 MIXED不同实例、不同版本可能不一样而 ROW 格式对数据解析最友好。第三binlog 文件在云上有保留周期超过保留天数的文件会被自动清理这一点必须有清醒认识。还有一个容易忽略的点腾讯云数据库的 binlog 下载机制在控制台上看是以“备份文件”的形式呈现的和自建 MySQL 里那种“某个目录下按序号排着的文件”是两回事。你要用一个工具去持续拉取增量 binlog不能指望登录服务器去 tail 文件。1.3 为什么没有直接用现成方案老实说市面上面向 MySQL binlog 的现成方案不少最主流的是 Canal 和 Maxwell。但我评估下来在腾讯云数据库这个特定环境下它们都有一点“水土不服”。Canal 本身需要伪装成 MySQL 的 slave 去和主库交互这在自建环境没问题但在腾讯云数据库上需要主库开启 binlog 并且授权一个具备 REPLICATION SLAVE、REPLICATION CLIENT 权限的账号。腾讯云控制台可以创建这类账号但有些实例出于安全考虑默认禁止了远程复制协议。即使能连上Canal 的部署也偏重要引入 ZooKeeper、Manager 等一系列组件对一个小需求来说太沉了。Maxwell 轻量一些但它默认把解析结果输出到 Kafka 等消息队列如果你的目标不是消息系统而是直接拿到结构化变更记录还得再包一层。至于 mysqlbinlog 这个官方工具配合 --read-from-remote-server 参数确实能远程拉取 binlog但它面向的是“人工排查”场景不适合做持续不断的增量消费。我需要的是一个能拉取、能解析、能按需输出、还能随意嵌入到脚本里的东西。于是 MysqlBinglogDigger 这个工具就诞生了它的定位非常直接——连接腾讯云 MySQL拉取 binlog 文件或事件流解析出每个变更操作以 JSON 或 SQL 形式输出。不追求大而全解决问题就行。2. 腾讯云数据库 Binlog 获取的几种方式实测2.1 控制台手动下载的适用场景最直观的方式当然是控制台手动下载。登录腾讯云控制台进入数据库实例的“备份恢复”页面可以看到 binlog 备份列表。每个文件有起始时间和结束时间点击下载就能拿到一个 binlog 文件。这种方式适合一次性操作比如误删数据后要恢复某几个文件。下载之后用 mysqlbinlog 本地方便地解析即可。但要注意一个坑控制台下载的 binlog 文件可能是经过压缩的下载下来需要解压另外文件名可能和实例内真实的 binlog 文件名不一致但文件内容是一致的可以放心解析。手动下载最大的问题是不可自动化。如果你的需求是每天定时拉取前一天的 binlog 做审计归档靠人去点控制台显然不现实。而且 binlog 文件一旦超过保留期控制台上也找不到想下载都没机会。2.2 云 API 自动获取 binlog 文件要做自动化就得走腾讯云 OpenAPI。腾讯云提供了一组与 binlog 相关的接口查询 binlog 文件列表、获取下载链接、设置下载任务等。简单来说流程分三步。第一步调用查询接口拿到指定时间段内的 binlog 文件列表每个文件会返回文件名称、起始时间、结束时间和文件大小第二步对需要下载的文件调用获取下载链接接口拿到一个临时的内网或公网 URL第三步用这个 URL 去下载文件下载完之后本地解析。签名鉴权方面推荐直接用腾讯云官方 SDK。以 Java 为例引入 tencentcloud-sdk-java 依赖用 SecretId 和 SecretKey 初始化 client然后调用对应接口即可。这里有一个经验如果工具运行在腾讯云 CVM 上并且 CVM 和数据库在同一个 VPC 内建议在接口参数中指定内网下载方式这样速度更快、也更安全。2.3 更优雅的方式使用 MySQL 复制协议直接拉取上面两种方式都是“下载文件再解析”有一种更优雅的方式是走 MySQL 复制协议。这种方式不下载文件而是让工具举个例子变成 MySQL 的 slave从主库实时接收 binlog 事件流。MysqlBinglogDigger 主要采用的就是这种方式。具体原理是客户端发送 COM_REGISTER_SLAVE 命令注册成为一个 slave然后通过 COM_BINLOG_DUMP 或 COM_BINLOG_DUMP_GTID 命令请求从某个位点开始发送 binlog 事件。主库会持续推送事件过来工具在本地解析。这种方式延迟低、实时性强适合持续消费。前提条件是要有一个具备 REPLICATION SLAVE 权限的账号并且实例允许复制连接。腾讯云数据库默认允许用户创建一个具备复制权限的账号但有些安全策略严格的实例可能需要在控制台开启相关开关。另外如果使用的是 TDSQL MySQL 版复制协议的行为可能会有差异需要先做连通性验证。这里额外说一句走复制协议拉取 binlog 时主库会在内存里为每个 slave 连接维护一个 dump 线程。所以工具必须做好断线重连否则每重连一次就新建一个 dump 线程连接多了主库会有压力。3. MysqlBinglogDigger 核心设计从拉取到解析3.1 工具整体架构与职责划分把一个 binlog 读取工具做清晰其实只需要划分好四个模块连接拉取模块、事件解析模块、位点管理模块、输出模块。连接拉取模块负责和腾讯云 MySQL 建立复制连接发送 dump 命令读取原始字节流。事件解析模块把字节流解析成一个一个的 binlog 事件对象提取数据库名、表名、操作类型、变更前后的列值等关键信息。位点管理模块记录当前已经消费到哪个 binlog 文件、哪个偏移量这样程序重启后可以继续消费不会重复也不会丢失。输出模块负责把解析结果写到文件、消息队列或直接打印到控制台。四个模块之间用简单的接口隔开互不依赖。拉取模块只负责产出原始字节解析模块只负责字节到事件的转换位点管理模块独立存储位点信息输出模块只消费解析结果。3.2 binlog 二进制格式到底怎么读说到 binlog 解析很多人第一反应是“二进制格式太复杂不如直接用 mysqlbinlog”。但如果你要做工具就必须理解它的核心套路。binlog 文件由一串事件event组成每个事件有固定的头部头部里包含事件类型、事件大小、日志文件位置、时间戳等信息头部的长度是 19 字节。事件类型决定了事件主体结构的含义。在最常见的 ROW 格式下一次 insert 操作会对应一组事件TABLE_MAP_EVENT 记录了表名和列数量信息紧接着是 WRITE_ROWS_EVENT里面按行存了实际插入的列值。update 操作则是先有 TABLE_MAP_EVENT然后是 UPDATE_ROWS_EVENT事件里同时有“更新前镜像”和“更新后镜像”两行数据。delete 操作对应 DELETE_ROWS_EVENT事件里只有“删除前镜像”。这里要特别提醒一个关键点TABLE_MAP_EVENT 本身并不包含列的数据类型信息。它只告诉解析器“这张表在对应时间点有哪几列”但列的类型需要解析器结合当时的表结构去推断。如果你在 binlog 生成之后修改了表结构比如加了列、改了字段类型解析时如果拿最新的表结构去套就会出错。正确做法是在消费时缓存表结构元数据并且这个缓存要能感知到 DDL 事件的变化。为了不重复造轮子MysqlBinglogDigger 的解析模块底层用了开源的 mysql-binlog-connector-java它已经把事件解码这层做得很稳定了。我在它之上封装业务提取逻辑把事件对象转成自己的数据结构再交给输出模块。3.3 关键事件类型速查这里整理一下我在开发和使用过程中高频遇到的事件类型供大家对照参考事件类型含义关键信息FORMAT_DESCRIPTION_EVENT文件格式描述事件标志 binlog 文件开始包含服务器版本和事件头长度QUERY_EVENT查询事件事务开始时产生记录 BEGIN 或具体 DDL 语句TABLE_MAP_EVENT表映射事件记录库名、表名、列数量WRITE_ROWS_EVENT插入行事件新插入行的列值UPDATE_ROWS_EVENT更新行事件更新前和更新后的列值DELETE_ROWS_EVENT删除行事件被删除行的列值XID_EVENT事务提交事件表示事务已提交ROTATE_EVENT日志轮转事件标记当前 binlog 文件结束下个文件名GTID_LOG_EVENTGTID 事件全局事务标识用于精确确定事务位置理解这些事件之间的顺序关系很重要。一个典型的事务在 binlog 里的大致顺序是GTID_LOG_EVENT - QUERY_EVENT(BEGIN) - TABLE_MAP_EVENT - WRITE_ROWS_EVENT / UPDATE_ROWS_EVENT / DELETE_ROWS_EVENT - XID_EVENT。如果你要按事务维度去聚合变更就得根据这个顺序把事件分组。3.4 GTID 与位点管理断点续传的关键做过 binlog 消费的人都知道位点管理是决定工具可用性的关键一环。最简单的方式是用“binlog 文件名 偏移量”作为位点比如当前消费到 mysql-bin.000012 的第 34567 字节。这种方式够用但有个问题如果 binlog 被清理或者发生了 failover文件名和偏移量可能对不上。GTID 方式更优雅。GTIDGlobal Transaction Identifier是一个全局唯一的事务标识由“源实例 UUID 事务序号”组成。它可以精确指到某个事务即使 binlog 文件已经轮转只要 GTID 还在就能基于它恢复消费。MysqlBinglogDigger 在启动时会先读取本地位点文件。如果位点文件里记录的是 GTID 集合就通过 COM_BINLOG_DUMP_GTID 命令请求从该 GTID 之后继续消费如果记录的是文件偏移量就通过 COM_BINLOG_DUMP 命令按文件偏移量消费。两种方式都支持推荐使用 GTID因为它在实例发生主备切换后更可靠。位点文件的更新策略也要讲究。我采用的是“事务提交后记录位点”的策略——当收到 XID_EVENT 或事务结束事件时才更新本地位点文件。这样即使程序中途崩溃重新启动后最多重复消费最后一个事务而不会丢失事务。相比“每收到一个事件就记录位点”这种方式牺牲了一点重复消费的概率但避免了半截事务问题。4. 实操全过程从零跑通 MysqlBinglogDigger4.1 环境准备与编译先说环境。我用的 JDK 1.8Maven 3.6 以上即可。MysqlBinglogDigger 本质上是一个 Java 工程通过 Maven 引入两个关键依赖mysql-binlog-connector-java 用于解析 binlog 事件tencentcloud-sdk-java 用于在需要时调用腾讯云 OpenAPI。一个可以用的 pom.xml 依赖片段dependency groupIdcom.zendesk/groupId artifactIdmysql-binlog-connector-java/artifactId version0.27.2/version /dependency dependency groupIdcom.tencentcloudapi/groupId artifactIdtencentcloud-sdk-java/artifactId version3.1.500/version /dependency注意 mysql-binlog-connector-java 这个库的 groupId 历史上改过几次老一点资料里是 com.github.shyiko新版本是 com.zendesk。如果你拉不到依赖检查一下是不是 groupId 写成了旧坐标。配置方面我习惯用一个简单的 properties 文件保存连接信息# 数据库连接信息 db.hostyour-cdb-instance.cdb.tencentcdb.com db.port3306 db.usernamebinlog_reader db.passwordyour_password # 位点策略gtid 或 file binlog.position.typegtid binlog.gtidyour_last_gtid # 输出方式console / file / json output.modeconsole4.2 注册复制账号与连通性验证在腾讯云数据库控制台上创建一个专门用于 binlog 读取的账号权限只需要 REPLICATION SLAVE、REPLICATION CLIENT、SELECT。注意不要直接用业务账号涨权限安全风险太大。账号创建后先用命令行工具验证一下能否正常建立复制连接mysql -h your-cdb-instance.cdb.tencentcdb.com -u binlog_reader -p \ -e SHOW MASTER STATUS; SHOW BINARY LOGS;如果这里能正常输出主库位点和 binlog 文件列表说明账号权限没问题。如果报错 Access denied检查一下账号是否真的勾选了复制权限。4.3 首次运行定位起始位点假设我们要从当前时刻开始持续消费 binlog第一步要拿到当前主库的位点。执行 SHOW MASTER STATUS 会得到类似于下面的结果File: mysql-bin.000162 Position: 901847 Binlog_Do_DB: Binlog_Ignore_DB: Executed_Gtid_Set: 7c3e6f9a-12ab-4a54-9d9e-2f2a4b6c7d8e:1-4567这个结果告诉我们两件事当前正在写入的 binlog 文件是 mysql-bin.000162当前偏移量是 901847已经执行过的 GTID 集合到 4567 号事务。如果你想“从当前这个位置之后的所有变更开始消费”就在配置里把位点类型设置为 filefile 文件名填 mysql-bin.000162偏移量填 901847。如果你想“从某个事务之后开始消费”就设置 gtid 类型填 7c3e6f9a-12ab-4a54-9d9e-2f2a4b6c7d8e:4567工具会从 4568 号事务开始推送。这里有一个值得警惕的细节SHOW MASTER STATUS 只有在主实例上执行才有意义。如果你连接的是只读实例拿到的是只读实例自己的位点而只读实例不一定会生成 binlog所以一定要确认连接的是主实例。4.4 运行工具与输出样例环境准备好后直接启动java -jar mysql-binglog-digger.jar --configbinlog.properties控制台就会开始输出解析结果。比如执行了这样一条 SQLUPDATE user_info SET age 30 WHERE id 100;工具输出的 JSON 大致长这样{ type: UPDATE, database: app_db, table: user_info, timestamp: 1713096000, gtid: 7c3e6f9a-12ab-4a54-9d9e-2f2a4b6c7d8e:4568, data: { id: 100, name: 张三, age: 30 }, before: { id: 100, name: 张三, age: 25 } }before 是更新前的镜像data 是更新后的镜像。拿到这个结构化的输出后后续无论是做数据对比、回滚语句生成还是投递到消息队列都方便得很。我同时还加了一个“输出为 SQL”的模式可以自动生成反向补偿 SQL。比如上面的 UPDATE 操作反向 SQL 就是UPDATE user_info SET age 25 WHERE id 100;这个功能在做误操作数据恢复时特别有用可以直接把变更回滚掉。4.5 断点续传的实际验证为了验证位点管理的可靠性我做过一个简单但有效的实验启动工具消费 binlog在它处理了 100 个事务后强制 kill 进程然后重新启动工具。启动时指定从同一份位点文件继续验证两条关键结果第一没有漏掉任何事务。通过比对数据库实际变更和工具输出确认 kill 后没有吞掉任何变更。第二可能存在重复输出但重复的粒度是“事务”而不是“单条事件”。出现重复是因为 kill 发生在事务提交事件之后、位点文件更新之前重启后从旧位点重新消费了最后一个事务。这个重复在大部分场景下可以接受而且幂等消费很容易处理。如果你绝对不能接受重复可以把位点更新策略改为“每收到一个事件都更新”但这样做的代价是位点文件写入频繁可能引入性能瓶颈。我的建议是优先保证“不丢”重复用业务侧幂等逻辑消化。5. 实战踩坑记录与问题排查5.1 “binlog 文件不存在”背后的真实原因遇到过一类非常迷惑的报错指定了某个 binlog 文件名开始消费主库返回错误“binlog file not found”。排查来排查去发现根源往往不是文件名写错而是这个 binlog 文件已经过了保留期被清理了。腾讯云数据库的 binlog 保留天数默认是 7 天也就是说你最多只能回溯到 7 天前的 binlog。如果业务需要更长的回溯窗口要去控制台调整 binlog 保留时间。这个坑在“按文件偏移量消费”时尤其隐蔽。因为 SHOW BINARY LOGS 里已经看不到被清理的文件了但如果你在代码里硬编码了一个旧文件名启动时就会报 not found。解决方法是启动前先动态拉取当前可用的 binlog 文件列表校验请求的文件名是否在列表内不在就主动提示并退出而不是让异常堆栈把真实原因藏住。5.2 时区问题导致解析出来的时间偏移 8 小时解析 binlog 时时间戳字段读出来是一个 Unix 时间戳。有一次我发现控制台输出的时间比数据库实际变更时间整整快了 8 小时一开始以为是解析库的问题后来才反应过来是时区设置的锅。binlog 事件头里的时间戳本身不带时区信息是一个绝对时间点。问题出在序列化输出时默认使用了 JVM 的时区。如果 JVM 时区设置成 UTC输出时间的自然就比北京时间少 8 小时如果你在 String 格式化时手动加上了东八区偏移但原始时间戳已经错了拼出来就是另一个错误结果。处理方法很简单在启动脚本里强制指定时区并且输出格式用带时区的 ISO8601 格式java -Duser.timezoneAsia/Shanghai -jar mysql-binglog-digger.jar同时程序中解析时间戳后统一使用 DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss).withZone(ZoneId.of(Asia/Shanghai)) 来格式化避免隐式使用默认时区。5.3 表结构变更导致解析字段对不上这是 ROW 格式 binlog 解析中最常见的“隐雷”。拿更新事件为例UPDATE_ROWS_EVENT 里记录的是某一列在变更前后的值但事件里并不会显式告诉你“第一列是 id第二列是 name”它只告诉你“这是第 1 列的值这是第 2 列的值”。解析器必须结合表结构元数据才能把列序号映射成列名。如果消费 binlog 期间执行了 DDL比如在表中间加了一列那么问题就来了新写入的 binlog 事件里列的数量变多了但解析器缓存的还是旧表结构。如果解析时用新表结构去解释旧事件列就会整体错位解析出完全错误的数据如果用旧表结构去解释新事件同样会错位。MysqlBinglogDigger 的解决方案是监听 DDL 事件来刷新元数据缓存。当收到 QUERY_EVENT 且 SQL 是 ALTER TABLE、CREATE TABLE、DROP TABLE 等 DDL 语句时立即重新加载对应的表结构。这个方案在实际使用中大幅减少了错位问题。但要注意加载表结构需要额外的 SELECT 权限而且在高频 DDL 场景下会增大数据库压力建议适当增加缓存刷新间隔。5.4 大字段与批量操作导致的内存压力还有一类问题出现在特殊数据上。一次批量 UPDATE 可能涉及上万行数据这些数据在一个 UPDATE_ROWS_EVENT 里会拆成多个“行事件”但如果瞬间积压在内存里JVM 堆很容易被撑爆。同样如果表里存了较大的 TEXT、BLOB 字段一个事件就可能占几十 MB 内存。我的处理经验有两点。第一在解析层做流式处理不要等一个事务的所有事件都解析完再处理。每解析到一个行事件就直接交给输出模块等事务结束时只记录位点。第二根据业务实际情况限制单行数据的最大长度对于超过阈值的行记录警告日志并跳过字段内容只保留下关键元数据库名、表名、操作类型、位点避免 OOM 影响整个消费链路。5.5 消费延迟越来越高从主库视角找原因跑了一段时间生产环境后发现工具的消费延迟从最初的几百毫秒慢慢变成了分钟级。一开始以为是解析速度不够后来通过 SHOW PROCESSLIST 查看主库的 dump 线程状态才发现问题是连接经常断开重连。每次重连后如果指定的是偏移量位点工具要从断点处重新拉取大量积压的 binlog这段时间内新增的变更只能排队等待延迟自然就上去了。解决方法是调整复制连接的几个参数。把 socket 超时调大避免主库长时间没有新事件推送时误判连接断开同时增加心跳机制在无事件时定期发送 PING 保持连接活跃。经过调整后消费延迟稳定在秒级以内。6. 工具之外的思考binlog 消费的上限与边界工具跑通了之后我反而开始思考一个更宏观的问题binlog 消费这条路哪些事能做哪些事要慎重。先说能做的。最容易落地的是审计与合规。有了 binlog 解析工具你可以把线上所有写操作完整归档这种能力对于做数据合规、安全风控非常有价值。第二个是数据恢复配合备份做时间点恢复PITR可以灵活地把数据恢复到任意时间点。第三个是异构数据同步把变更事件投递到消息系统由下游系统自行消费。这三个方向基本上覆盖了日常 90% 的 binlog 应用场景。要慎重的是跨地域实时同步。如果你打算用 binlog 做跨地域的数据库实时同步比如把腾讯云广州地域的数据库变更同步到上海地域延迟和网络稳定性都是挑战而且 binlog 无法传递会话级临时表等场景的操作。更稳妥的做法是使用云厂商提供的数据同步服务专业的事交给专业的系统去做。另外binlog 虽然有完整的变更记录但它不等于全量数据。如果你的目标是做在线查询比如“按某个业务条件查历史变更”直接从 binlog 里查是不现实的需要先把 binlog 数据导入到列式存储或者搜索引擎中。Binlog 只是数据的搬运工不是数据的最终归处。回到 MysqlBinglogDigger 这个工具本身它在设计上刻意保持简单少依赖、易部署、输出中性化。这带来的一个实际好处是无论你后面是接一个自建的日志分析平台还是接到现成的数据管道都没有额外成本。如果你也在做类似的 binlog 消费工具我有个诚心的建议先把“位点管理”和“表结构缓存”这两个基础组件做扎实再去追求解析速度和功能丰富度这能帮你避开绝大多数的线上坑。