
凌晨两点值班群突然被一条告警刷屏线上订单表被人不带 WHERE 条件地整表 UPDATE几百行status全部变成了同一个值。领导在群里连发三条改了哪些行改之前是什么值能不能恢复这时候要是平时没开 binlog你连一点后悔药都找不到要是开了MySQL 的二进制日志就是你唯一的“黑匣子”。binlog 的查看实操是我自己从只会SHOW MASTER STATUS到能定位具体事务、还原误操作现场一步步摸出来的。这篇文章我就把整套方法从头到尾捋一遍怎么确认开关、怎么用mysqlbinlog命令行、怎么用 SQL 查、以及一次真实的误 UPDATE 定位过程。开发、运维、DBA 都适用关键是看完能直接上手。1. binlog是MySQL的“黑匣子”先搞懂它到底记了什么账1.1 一条 UPDATE 在 binlog 里是怎么留痕的binlog 是 MySQL Server 层的日志不管你用 InnoDB 还是 MyISAM只要开了它每个会在数据层面产生变更的事务都会按顺序写进文件。它默认追加写入文件写满约 1GB 就滚动生成下一个名字一般长这样mysql-bin.000001、mysql-bin.000002……文件清单会登记在mysql-bin.index里。举个具体例子你执行UPDATE t SET status3 WHERE id1并 commit 后binlog 里会新追加一条事件。事件开头通常是一堆# at xxx、end_log_pos、server_id、时间戳这些元信息紧接着才是真正的变更内容。如果当前是 ROW 格式日志里还会记录这行数据从status1变成status3的“前后映像”。正是这些映像让后端的排查变成可能——你能亲眼看到改之前长什么样、改之后长什么样而不是靠猜。1.2 先分清楚binlog 不是 redo log也不是查询日志很多人一听到“日志”就往 redo log、慢查询日志上靠实际完全不是一回事。这个区别搞不清楚后面看什么都容易混。比较维度redo logbinlog所在层次InnoDB 存储引擎层MySQL Server 层几乎所有引擎共享主要作用崩溃恢复掉电后重放未落盘的修改主从复制、数据恢复、审计排查记录内容物理页的修改偏底层逻辑变更ROW 格式记录行前后映像写入方式循环写旧内容会被覆盖追加写按策略保留使用场景实例异常重启后自动恢复从库同步、mysqlbinlog 读取、PITR慢查询日志、错误日志跟 binlog 的用途也完全不同。binlog 不是拿来分析“哪条 SQL 慢”的它是拿来做数据链路追踪的。换句话说想优化性能去找慢查询日志想知道“谁在什么时候把哪一行改成了什么”才来找 binlog。1.3 三种 binlog_format选不对后面全白看binlog 的格式由binlog_format控制常见三种STATEMENT、ROW、MIXED。我建议你在排查场景里优先让它是ROW理由看完表就明白了。格式记录内容优点缺点排查价值STATEMENT记录执行的 SQL 语句本身日志小、人眼直接能读受上下文影响回放结果可能不一致能看出执行了哪些 SQL但看不到具体行变化ROW记录每一行变更的前后映像精确、回放一致、能看到旧值新值日志体积大默认输出需要解码最强误操作还原基本靠它MIXED默认记语句遇不确定操作自动转 ROW体积和可读性平衡有时是语句、有时是行读的时候要切换思路需要同时掌握两种读法从 MySQL 5.7.7 开始默认就是ROW8.0 默认也是。所以实际生产环境里你拿到的 binlog 大概率是 ROW 格式。另外一点存储过程、触发器内部的 DML在 ROW 格式下同样会被拆成行事件记进 binlog。也就是说哪怕业务是通过存储过程把数据改坏的只要开了 ROW现场依然能挖出来。1.4 binlog 是主从复制的“快递面单”主从复制的链路本质就是围绕 binlog 转的主库提交事务时写 binlog从库的 IO 线程拉取 binlog 存成 relay log再由 SQL 线程回放。所以 binlog 里的内容和顺序直接决定了从库最终的数据状态。这就带出一个实用概念GTID。在开启 GTID 的模式下每个事务在 binlog 里都会带一个全局唯一编号类似0a2f1a5e-xxxx-xxxx-xxxx-xxxxxxxxxxxx:1-100。排查到某个具体事务时GTID 可以把定位精确到“某个事务”而不是“某段时间”这对多实例环境尤其重要。毕竟你要回答的是“这条改动到底有没有同步到从库”而不是笼统地说“可能同步了吧”。2. 动手之前先确认 MySQL 到底有没有在记账2.1 三条 SQL 摸清 binlog 家底查 binlog 前第一件事不是敲mysqlbinlog而是确认服务开没开、存在哪、用的什么格式。以下四条 SQL 基本能解决所有前置问题SHOW VARIABLES LIKE log_bin; SHOW VARIABLES LIKE binlog_format; SHOW VARIABLES LIKE log_bin_basename; SHOW BINARY LOGS; SHOW MASTER STATUS;执行结果里关键信息是这么看的log_bin显示ON说明已开启显示OFF就什么都别查了先回头开日志。binlog_format告诉你当前记录格式我记得大部分 5.7 和 8.0 默认都是ROW。log_bin_basename是 binlog 文件的完整路径前缀比如/data/mysql/binlog/mysql-bin后续mysqlbinlog命令要用到这个路径。SHOW BINARY LOGS;列出当前实例所有 binlog 文件排查时先看它就知道有哪些“卷宗”。SHOW MASTER STATUS;显示当前正在写的文件和 binlog position定位“我正站在哪儿”。权限上要注意SHOW BINARY LOGS、SHOW BINLOG EVENTS这类命令需要REPLICATION CLIENT权限很多业务账号默认没有。如果你一执行就报ERROR 1227 (42000): Access denied先用管理员账号授权GRANT REPLICATION CLIENT ON *.* TO read_user%;2.2 没开 binlog 怎么办配置文件照着抄如果你的库确实没开又因为各种原因需要开我真心建议所有环境的库都开代价很小收益极大改my.cnf后在[mysqld]段加上这几行[mysqld] server-id 1 log-bin /data/mysql/binlog/mysql-bin binlog_format ROW binlog_expire_logs_seconds 604800 max_binlog_size 1G sync_binlog 1逐行说下为什么这么写server-id必须配而且是唯一值。它是 binlog 事件里的“机器标识”不配的话做复制相关的操作会遇到一堆隐性问题多个实例之间不能重复。log-bin指定 binlog 的路径和前缀。生产环境建议单独放一个目录比如/data/mysql/binlog/避免和数据文件挤在同一块磁盘。写路径时确保 MySQL 运行账号对该目录有写权限否则启动阶段就直接报错。binlog_format ROW前面说过了恢复和排查最强。如果你担心日志体积爆炸可以先接受它偏大的事实用max_binlog_size和备份策略来控制。binlog_expire_logs_seconds按秒设置自动清理写 604800 就是保留 7 天。注意 MySQL 8.0 里老的expire_logs_days已废弃别再用5.7 两个都支持但建议直接上新参数。max_binlog_size 1G单个文件上限。这个值偏大对排查没坏处文件多了只是在SHOW BINARY LOGS里看得费劲一些。sync_binlog 1每次事务提交都强制把 binlog 刷到磁盘。这行和innodb_flush_log_at_trx_commit1搭配能做到数据库崩溃也不丢已提交事务。性能敏感场景下可以牺牲一点吞吐但重要业务库不建议关。改完后重启 MySQL再用上面那几条 SQL 验证。如果log_bin还是OFF大概率是路径没建好、没权限或者server-id忘写了。2.3 binlog 可以删除吗热词问题一次说清网上经常有人问“binlog 日志可以删除吗”。可以删但请用命令删千万别手抖去rm。尤其是你在线上直接删 binlog 文件而不同步清理.index索引文件后面 MySQL 可能直接起不来。标准操作是这两种-- 删除某个文件之前的全部日志 PURGE BINARY LOGS TO mysql-bin.000010; -- 删除某个时间点之前的全部日志 PURGE BINARY LOGS BEFORE 2024-11-01 00:00:00;RESET MASTER会把所有 binlog 清空并重新开始这个在主从环境下要极其谨慎别在从库还没追平的时候用。删除前必须确认两件事从库是否已经拉到这些文件、备份/归档策略是否已经覆盖。如果从库还落后你 purge 掉的 binlog 就是从库永远补不回来的那一截主从一致性当场断裂后续只能重新搭从库或全量重建。最省心的做法就是配好binlog_expire_logs_seconds让 MySQL 自动清理别在半夜手动做这种高危操作。3. mysqlbinlog 命令行日常查证的主力姿势3.1 最基础的打开方式mysqlbinlog是 MySQL 自带的客户端工具专门用来读取二进制日志。最直接的用法就是把文件路径丢给它mysqlbinlog /data/mysql/binlog/mysql-bin.000001输出内容大概是这样的我简化了校验和字段# at 155 #241115 14:30:55 server id 1 end_log_pos 258 CRC32 0x00000000 Query thread_id107 exec_time0 error_code0 SET TIMESTAMP1731652255/*!*/; BEGIN /*!*/; # at 258 #241115 14:30:55 server id 1 end_log_pos 389 CRC32 0x00000000 Update_rows: table id 34 flags: STMT_END_F ... # at 450 #241115 14:30:56 server id 1 end_log_pos 520 CRC32 0x00000000 Query thread_id107 exec_time0 error_code0 COMMIT/*!*/;几个高频字段必须会看# at 155事件起始位置也是后续--start-position要用到的定位点。end_log_pos 258当前事件结束位置其实也是下一个事件的起点。server id 1哪台服务器写入的多实例环境用来筛来源。SET TIMESTAMP...语句执行时刻对应的 Unix 时间戳。查不到 SELECT 是正常的。binlog 只记 DML、DDL、DCL 和事务控制类内容查询操作根本不会到这里面。3.2 ROW 格式默认是“加密状”的加参数解码直接裸读 ROW 格式 binlog 是行不通的——事件内部是二进制mysqlbinlog默认会用 base64 编码显示成一大片BINLOG ...字符串人眼完全读不了。这也是很多新手第一次看 binlog 就劝退的原因。正确姿势是加解码参数mysqlbinlog -v --base64-outputdecode-rows /data/mysql/binlog/mysql-bin.000001 # 想看得更细还可以用 -vv mysqlbinlog -v -v --base64-outputdecode-rows /data/mysql/binlog/mysql-bin.000001加-v之后你会看到类似这样的伪 SQL### UPDATE eshop.mis_order ### WHERE ### 11001 /* INT meta0 nullable0 is_null0 */ ### 22024-11-15 14:20:01 /* DATETIME meta0 nullable0 is_null0 */ ### 3已支付 /* VARSTRING(100) meta0 nullable0 is_null0 */ ### SET ### 3已关闭 /* VARSTRING(100) meta0 nullable1 is_null0 */这里有个非常容易看晕的细节1、2、3是表字段的序号不是字段名。想知道各自对应哪一列用DESC eshop.mis_order;按顺序对照即可。另外记住 ROW 格式下的事件规则UPDATEWHERE 部分是旧值SET 部分是新值。DELETE只有 WHERE没有 SET。INSERT全是新值通常显示为一串字段赋值。所以官方文档也强调-vv会把每个字段的类型、是否可空等元信息都列出来这对你判断“这个字段到底存了什么类型的数据”非常有帮助。3.3 按时间切、按位置切从大海捞针到精准出手binlog 文件动辄几百 MB直接整包输出刷屏毫无意义。高频过滤参数就两个时间和位置。按时间过滤的例子mysqlbinlog --start-datetime2024-11-15 14:20:00 --stop-datetime2024-11-15 14:40:00 \ /data/mysql/binlog/mysql-bin.000001按位置过滤的例子mysqlbinlog --start-position155 --stop-position520 /data/mysql/binlog/mysql-bin.000001什么时候用哪个我的经验是初筛用时间精读用位置。你只知道大概时间段先按时间把嫌疑范围捞出来一旦在输出里看到可疑 SQL 附近有# at xxx立刻改按 position 单独导出那一小段避免在处理几万行输出时把眼睛看花。位置信息在 binlog 里的关系是“前一个事件的end_log_pos就是后一个事件的起点”所以你用--start-position155 --stop-position520能精确导出从 155 开始到 520 结束的所有事件这通常已经足够覆盖一个完整事务。注意跨文件查的时候时间过滤没法自动跨文件衔接你需要把候选文件一次性全部传给mysqlbinlogmysqlbinlog --base64-outputdecode-rows -v --start-datetime... --stop-datetime... \ /data/mysql/binlog/mysql-bin.000089 /data/mysql/binlog/mysql-bin.0000903.4 只查某个库、只查某张表组合拳用法生产环境一个实例下可能几十上百个库全量输出出来纯属找罪受。-d参数可以直接按库过滤mysqlbinlog -d eshop --base64-outputdecode-rows -v /data/mysql/binlog/mysql-bin.000001 /tmp/eshop.sql说到-d历史版本里它对 ROW 格式的行事件过滤并不可靠我当时排查时被它坑过。现在主流版本已经处理得比较稳但保险起见我建议把-d当成“粗过滤”再用 grep 做二次精筛mysqlbinlog --base64-outputdecode-rows -v /data/mysql/binlog/mysql-bin.000001 | \ grep -n -A 20 UPDATE \eshop\.\mis_order\grep 的时候注意反引号要转义否则 shell 可能把你命令里的表名当成命令替换。输出太多就先重定向到文件再查别把几个 GB 的日志一股脑打到终端。3.5 远程服务器上怎么看mysqlbinlog 支持直连拉取有时候 binlog 不在本地或者你想立刻查一台不能随便登录的生产机上的日志mysqlbinlog可以远程读取mysqlbinlog --read-from-remote-server --host192.168.10.5 --port3306 \ --userbinlog_reader --password --protocoltcp mysql-bin.000003 /tmp/remote.sql远程读取有几个坑要注意账号必须至少有REPLICATION SLAVE权限我习惯顺带把REPLICATION CLIENT也一起授了这样远程跑SHOW BINARY LOGS也能用。--protocoltcp一定要写否则客户端默认尝试走本地 socket你会看到类似Cant connect through socket /tmp/mysql.sock的报错。远程拉取前先看文件大小。网络不稳时几 GB 的 binlog 拉到一半断掉很常见建议提前nohup或写脚本重试拉完后用ls -l对比下源文件大小。4. 直接在 SQL 里查SHOW BINLOG EVENTS 能干什么4.1 什么场景下用 SQL 查不是所有环境都方便执行mysqlbinlog比如你在只读从库、权限受限、或者手头没有服务器的 shell 权限。这时候SHOW BINLOG EVENTS就是你的替代方案。它适合做三件事快速列出某文件包含哪些事件类型。确认某个 position 附近大概有什么操作。在 JDBC 连接、GUI 工具里直接看不用跳服务器。4.2 基本语法与三个定位技巧SHOW BINLOG EVENTS IN mysql-bin.000001; SHOW BINLOG EVENTS IN mysql-bin.000001 FROM 155 LIMIT 10;注意FROM 155里的 155 是事件起始位置不是行号LIMIT 10表示最多返回 10 条事件。输出大概长这样------------------------------------------------------------------------------------------ | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | ------------------------------------------------------------------------------------------ | mysql-bin.000001 | 155 | Query | 1 | 258 | BEGIN | | mysql-bin.000001 | 258 | Table_map | 1 | 325 | Table_map: eshop.mis_order mapped to number 34 | | mysql-bin.000001 | 325 | Update_rows | 1 | 450 | table id 34 flags: STMT_END_F | | mysql-bin.000001 | 450 | Query | 1 | 520 | COMMIT | ------------------------------------------------------------------------------------------解读一下Pos列是事件起始位置Event_type是事件类型Info列是附加信息。看到Table_map基本就是 ROW 格式下某张表开始变更了紧接着的Update_rows表示发生了行更新。这样你能先摸清事件序列再决定要不要用mysqlbinlog深挖。4.3 四个边界别等踩了再懂ROW 格式下看不到行数据。SHOW BINLOG EVENTS的 Info 列只显示Table_map: db.tbl、Update_rows这类事件名并不会告诉你具体行的旧值和新值。想看行内容还是得mysqlbinlog -v。大事务会刷屏。一个 UPDATE 几千行的事务会被拆成大量Update_rows事件不配合FROM和LIMIT很容易被输出淹没而且服务端读取大文件的成本也不低。权限门槛高。执行这条 SQL 至少需要REPLICATION CLIENT权限业务账号经常没有报权限错误不要惊讶。只能查当前保留文件。已经被 purge、删除或滚动清理的 binlog 查不到所以该备份的要早点备份。5. 实战定位一次“不带 WHERE 的手滑 UPDATE”5.1 先别慌先保住现场场景我们开头已经说了eshop.mis_order表被误执行成整表 UPDATE所有记录的状态全变成同一个值。失误操作一般发生在十几分钟前binlog 大概率还在这时候第一原则是在没看完日志之前绝对不要 purge、不要删文件、不要 RESET MASTER。马上执行SHOW BINARY LOGS;把文件清单列出来再用ls -l看每个文件的修改时间。binlog 文件的 mtime 非常有用。出事时间是 14:30 左右那 14:20 到 14:40 之间 mtime 有变化的文件基本就是嫌疑文件范围。5.2 用时间窗先画嫌疑范围拿到文件清单后我对嫌疑文件跑一遍时间窗解码输出并 grep 出目标表mysqlbinlog --base64-outputdecode-rows -v \ --start-datetime2024-11-15 14:20:00 --stop-datetime2024-11-15 14:40:00 \ /data/mysql/binlog/mysql-bin.000097 /data/mysql/binlog/mysql-bin.000098 \ /tmp/check.sql grep -n -A 20 UPDATE \eshop\.\mis_order\ /tmp/check.sql从这里开始你就进入“挖案发现场”的阶段了。grep 到的那一段伪 SQL 会带出事件位置# at 1440 ### UPDATE eshop.mis_order ### WHERE ### 11001 ### 22024-11-15 14:30:01 ### 3已支付 ### SET ### 3已关闭5.3 用 position 精读最可疑那一段发现# at 1440还不够需要看它所在的完整事务是否闭合。我把范围稍微扩大一点重新导出确保包含 BEGIN 和 COMMITmysqlbinlog --base64-outputdecode-rows -v \ --start-position1300 --stop-position1780 \ /data/mysql/binlog/mysql-bin.000098 /tmp/precise.sql打开/tmp/precise.sql重点看三件事事务边界有没有 BEGIN / COMMIT中间夹着几条行事件。影响的业务数据WHERE 里的主键或唯一键是什么。同一个事务里还有没有其他表被改。不要嫌多看一眼麻烦。误操作往往不是单条语句可能是脚本里连打了几条 SQL。把整个事务拉出来你才不会再漏掉第二部分改动。5.4 从伪 SQL 里读出“旧值”和“新值”构造反向恢复binlog 的 ROW 格式里UPDATE 事件的 WHERE 子句是旧值SET 子句是新值所以反推恢复 SQL 非常直接。比如上面那段伪 SQL原本数据是status已支付被改成了status已关闭那恢复语句就是UPDATE eshop.mis_order SET status 已支付 WHERE order_id 1001 AND status 已关闭;如果失误类型是 DELETEbinlog 的事件里会带着完整的 WHERE即被删行全部字段你可以直接把它们拼成 INSERT 语句插回去。这是 binlog 最值钱的地方你不用去猜业务逻辑日志把 before image 摆在 WHERE 里了。恢复执行前有几个铁律先备份当前表把恢复 SQL 包在事务里执行先看影响行数是否符合预期能先在从库或测试环境跑一遍验证就别直接在主库赌命。5.5 如果 binlog 刚好被 purge 了怎么办说实话如果 binlog 已经被清理又没有备份归档那这次误操作基本只能靠业务日志或人工对账来弥补代价会非常大。所以这条值得反复强调binlog 一定要配保留策略生产环境 7 天起步再配合周期全备。这样即使出问题时文件被自动清理了也能从全备 归档的 binlog 链路里做 point-in-time 恢复。6. 那些年踩过的坑我的备忘清单6.1 解码参数忘了满屏都是 BASE64我见过太多人拿着mysqlbinlog mysql-bin.000001的输出截图来问“怎么全是乱码”。这不是乱码是你没加解码参数。记住这个肌肉记忆组合--base64-outputdecode-rows -v记不住就复制粘贴别靠记忆敲。6.2 时间过滤怎么都对不上先查时区事件时间戳记录的是 MySQL 服务器当时的本地时间不是你的本地时间。如果服务器是 UTC 时区你按北京时间 14:20 过滤日志里对应的其实是 06:20差距 8 小时。排查时先执行SELECT NOW();看服务器时间再和你命令里的时间窗对照一次别在时区问题上浪费半小时。6.3 SHOW BINLOG EVENTS 里找不到行数据别硬磕记住它看不到 ROW 格式的行细节。有人拿着SHOW BINLOG EVENTS的输出反复研究想知道某行数据改之前是什么值这是方法论就错了。该换mysqlbinlog就果断换。6.4 GTID 模式下导回 binlog 总是报错开启 GTID 后直接把mysqlbinlog的输出灌回 MySQL经常会碰到 GTID 相关的报错因为 binlog 里带了SET SESSION.GTID_NEXT...这类语句。一个比较稳的解法是mysqlbinlog --skip-gtids /data/mysql/binlog/mysql-bin.000098 | mysql -uroot -p--skip-gtids会忽略输出中的 GTID 设置让目标实例按自己当前 GTID 体系重新分配。如果是在主库上直接回放恢复建议回放前先SET SESSION.SQL_LOG_BIN0;避免恢复过程本身把 binlog 再污染一遍回放完再改回1。6.5 磁盘满了、日志又“删不掉”binlog 占满磁盘是最常见的故障之一。处理顺序是先确认从库同步状态SHOW SLAVE STATUS\G看Seconds_Behind_Master和Retrieved_Gtid_Set确认从库已经在SHOW BINARY LOGS列出的最新文件附近再 PURGE。如果这台机器既没有从库、也不做 PITR那 PURGE 的顾虑会小很多。更推荐的做法是提前配binlog_expire_logs_seconds把自动清理交给 MySQL而不是等磁盘警报响了才手动救火。6.6 客户端版本和 binlog 版本不匹配mysqlbinlog的工具版本最好不低于 MySQL 服务版本。我用低版本 mysqlbinlog 读 8.0 高版本 binlog 时经常遇到解析异常或直接报错读不了反过来用 8.0 的客户端读 5.7 的日志基本没问题。另外 Linux 上还可能出现mysqlbinlog: error while loading shared libraries一般是系统缺少对应 openssl、libssl 库安装完整版本的 MySQL client 依赖即可别在缺库环境里硬折腾。6.7 权限报错速查报错信息原因解决ERROR 1227 (42000): Access deniedSHOW BINLOG EVENTS / SHOW BINARY LOGS 权限不足GRANT REPLICATION CLIENT ON *.* TO userhostCant connect through socket远程命令漏了--protocoltcp加--host--protocoltcpFailed to read log file: mysql-bin.xxxx文件已被 purge或账号没有读取权限看归档/备份或检查权限这套流程我用了好几年最后形成的习惯其实特别简单任何一次 binlog 排查都先SHOW BINARY LOGS确认文件清单再mysqlbinlog时间窗 解码 grep 初筛锁定 position 后单独精读。别一上来就整库导日志也别指望SHOW BINLOG EVENTS帮你看到行数据。工具只有用对姿势才会给你想要的答案。最后再分享一个小建议开发库、测试库也顺手把 binlog 开上保留 3 到 7 天。平时这点成本几乎可以忽略但哪天你面临“谁把数据改了”这种灵魂拷问时它就是唯一的后悔药。