ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL binlog实战指南:从误删恢复到主从复制一次讲透

MySQL binlog实战指南:从误删恢复到主从复制一次讲透 深夜十一点线上库一个DELETE条件写漏了几十万行订单数据瞬间蒸发。老板在群里问能不能恢复你说应该能但心里其实没底。这时候唯一能救你的就是平时不起眼、你可能连开都没开的binlog。做了十年MySQL运维和开发我自己经历过三次靠binlog翻盘的时刻。第一次是把一张配置表整表删了第二次是存储过程批量更新时条件被置空第三次是帮同事从一天前的binlog里捞回被覆盖的字段值。每次结束后我都会想如果当初没开binlog这活儿根本没法接。这篇文章不打算写成官方文档的复读机而是把我的实战经验、踩过的坑、以及那些面试里常被问到但很多人都答不利索的细节一次讲清楚。不管你是DBA、后端开发还是运维看完都能对binlog有个完整的认知并且知道真出事时怎么用它捞数据。1. 从一次误删数据讲起binlog到底是什么凭什么能救命1.1 先搞清楚它记录了什么binlog全称是Binary Log也就是二进制日志。它是MySQL Server层生成的日志记录了所有对数据库产生变更的操作INSERT、UPDATE、DELETE这些DML以及CREATE TABLE、ALTER TABLE、DROP TABLE这些DDL都会按顺序写进binlog。注意一个关键点SELECT和SHOW这类只读操作不会进binlog因为数据库状态没有被改变。这个特性和业务审计需求时要区分清楚——如果你想记录谁查了什么数据binlog帮不上忙那是general log或者审计插件的事。日志以二进制格式写入你直接用文本编辑器看会看到乱码但这不是bug它本来就不是给人直接读的。正因为是二进制格式它才能高效记录变更、支持主从复制传输、也能被mysqlbinlog工具在离线状态下解析成可读SQL。1.2 同一个数据库为什么需要两份日志很多刚接触MySQL的人会把binlog和InnoDB的redo log搞混。这俩名字看着都带log但职责完全不同对比项binlogredo log所属层级MySQL Server层InnoDB存储引擎层记录内容逻辑变更记录SQL或行变更前后镜像物理变更记录数据页的修改主要用途主从复制、时间点恢复、审计Crash Safe崩溃后恢复内存中未落盘的数据页写入时机SQL执行后、事务提交时事务执行过程中持续写入日志格式二进制可通过mysqlbinlog解析二进制InnoDB内部使用不对外提供解析工具打个比方redo log是厨师在厨房里记账记的是我切了哪些菜、炒到什么火候是为了防止灶台塌了之后重新做时不知道做到哪一步binlog则是饭店面向外界出具的流水单记的是这桌客人点了什么菜、付了多少钱用于全店统一对账主从和事后追溯恢复。1.3 两阶段提交为什么必须两阶段既然有两份日志就涉及一致性问题。假设执行一条UPDATEInnoDB里数据页已经改了redo log也写了但binlog还没写这时候MySQL崩溃主库用redo log恢复出了新值但从库没收到binlog还是旧值两边就不一致了。为了解决这个问题MySQL引入了两阶段提交Two-Phase Commit。在事务提交时先写redo log并进入prepare状态然后写binlog最后把redo log标记为commit状态。整个流程保证redo log和binlog要么都生效要么都不生效。这也是面试里问为什么binlog和redo log需要两阶段提交的标准答案。实际排错时如果发现主从数据不一致经常就是这两份日志在某个边界时刻没对齐理解了这两阶段排查方向一下就hold住了。2. STATEMENT、ROW、MIXED三种格式怎么选别等出事才后悔binlog有三种记录格式选错代价很大。我见过不止一个项目用默认的STATEMENT跑了好几年直到某个UUID()函数导致主从数据不一致才回头研究格式问题。2.1 STATEMENT格式记SQL不记数据STATEMENT格式下binlog只记录执行的原生SQL语句。从库拿到这条SQL在自己的本地环境重新执行一遍。它的优点很直接日志量极小一条UPDATE改造一万行binlog里只有一行SQL。而且因为记录的是语句本身对人工阅读日志排查问题非常友好。缺点也致命结果依赖上下文。如果SQL里用了NOW()、UUID()、RAND()这类非确定性函数从库执行时会得到不同的值如果用LIMIT配合无ORDER BY的更新主从命中的行可能都不一样。此外涉及存储过程、触发器的复现也会非常麻烦。2.2 ROW格式记每一行的前后镜像ROW格式下binlog不记录SQL语句本身而是记录每个被修改行的前像和后像。比如一条UPDATE student SET score score 10 WHERE class_id 5更新了500行binlog里就会记录这500行每一行修改前后的完整数据。这是最安全的记录方式主从完全基于物理变更同步不存在非确定性函数问题。缺点也很直观日志量大。批量或大事务场景下ROW格式产生的binlog体积可能是STATEMENT的几十倍。实际使用中配合binlog_row_image MINIMAL可以大幅瘦身让它只记录变更行的主键与变更字段而不是整行完整镜像。这个参数在外面很多生产配置里其实已经被默认打开但如果你还在用5.6、5.7早期版本建议手动确认一下。2.3 MIXED格式MySQL帮你做选择题MIXED是STATEMENT和ROW的混合体。默认情况下使用STATEMENT当MySQL判断当前SQL存在主从复制安全隐患时自动切换为ROW格式。听起来很聪明省心但我的建议是新的生产环境默认别用MIXED。原因很简单——你无法预测MySQL会在哪条SQL上切换格式一旦出问题日志分析路径会变得很复杂。主从延迟排查时一会儿看SQL一会儿看ROW镜像思维负担很大。2.4 我的选择建议互联网核心业务系统我推荐直接选ROW。数据一致性比磁盘多占几十G重要得多。binlog_row_image MINIMAL必须配上否则ROW格式在联表更新或大字段场景下容量会爆。如果是数据分析类离线库、日志量极度敏感且对复制一致性要求没那么苛刻的场景可以选STATEMENT。而MIXED除非是Oracle DBA转过来实在不习惯看ROW格式否则我不推荐。3. 正确开启和配置binlog你的MySQL可能根本没开这东西3.1 先检查当前状态很多开发同学的本地MySQL是装完就用的默认配置binlog大概率没开。检查方法非常简单SHOW VARIABLES LIKE log_bin; ---------------------- | Variable_name | Value | ---------------------- | log_bin | OFF | ----------------------如果返回OFF下面的内容就是为你准备的。3.2 核心参数一次配齐还是在/etc/my.cnf或/etc/mysql/mysql.conf.d/mysqld.cnf的[mysqld]段下配置[mysqld] # 开启binlog指定日志基础文件名 log_bin /var/lib/mysql/binlog # 日志格式强烈建议ROW binlog_format ROW # ROW格式下只记录主键和变更字段体积小很多 binlog_row_image MINIMAL # 单个binlog文件上限达到上限自动切换到下一个编号文件 max_binlog_size 1G # 保留时长8.0用秒5.7及以前用天 # 下面两个选一个别同时配5.7 binlog_expire_logs_seconds 604800 # expire_logs_days 7 # 每次事务提交都强制把binlog刷到磁盘 sync_binlog 1几个参数我展开说一下log_bin如果你只写log_bin ON日志文件会默认在数据目录下按主机名命名。我建议显式写一个带路径的文件名比如/var/lib/mysql/binlog这样实际文件会生成binlog.000001、binlog.000002这种带编号的序列文件管理和查找都方便。binlog_expire_logs_secondsMySQL 8.0已经废弃了expire_logs_days用秒为单位更灵活。比如想要7天就写604800。用SHOW VARIABLES LIKE binlog_expire_logs_seconds验证。5.7及以前版本如果同时配了新旧两个参数会以新参数为准但在8.0中配置旧的expire_logs_days会导致MySQL拒绝启动。注意binlog保留时间不建议设到0。0代表永久保留会让磁盘被binlog占满从而触发MySQL自我保护式的拒绝写入。也别设太短常见故障排查和数据恢复往往需要回看一周以上的日志。sync_binlog这个参数是性能和安全的博弈点。 1表示每提交一个事务就把binlog刷一次盘最安全但小事务高并发场景下会有性能损耗。 0表示交给系统决定什么时候刷盘性能好但崩溃时可能丢失最近几个事务的binlog。生产环境我建议保持1配合innodb_flush_log_at_trx_commit 1这是数据不丢的基本组合。3.3 开启后必须做的验证改完配置重启MySQL后别急着下班跑一遍验证SHOW VARIABLES LIKE log_bin; SHOW VARIABLES LIKE binlog_format; SHOW BINARY LOGS;SHOW BINARY LOGS会列出所有binlog文件列表、大小和加密标志看到这个列表就说明binlog真的在工作了。接着手动执行一条数据变更再SHOW MASTER STATUS;观察Position是否变化。如果变了说明日志正在实时写入。3.4 开启过程中的常见坑server_id未设置从MySQL 5.7开始开启binlog必须有server_id否则启动直接报错。在配置里加一行server_id 1这个不是用来标识机器ID的也可以用只是需要唯一值。日志目录权限不够如果指定的路径不存在或MySQL的运行用户没有权限写会导致启动失败。稳妥做法是用mkdir -p创建目录然后chown mysql:mysql授权。参数位置写错binlog_format这类参数必须写在[mysqld]段下写在[client]或[mysql]下会被无视启动时也不报错但验证时发现没生效。MySQL 8.0的默认值变化8.0默认且强制开启binlog默认格式是ROW。所以你如果装了8.0大概率已经是开着binlog的状态不用重复配置log_bin ON。但binlog_row_image MINIMAL这种优化参数依然值得手动检查。4. 用mysqlbinlog解析日志数据库必备的黑匣子阅读器binlog是二进制的直接cat只能得到一堆乱码。MySQL官方提供的mysqlbinlog工具就是专门干这个的用法非常简单。4.1 基础用法# 直接查看某个binlog文件的内容 mysqlbinlog /var/lib/mysql/binlog.000012 # 指定时间范围 mysqlbinlog --start-datetime2025-01-01 00:00:00 --stop-datetime2025-01-01 10:00:00 /var/lib/mysql/binlog.000012 # 指定Position范围 mysqlbinlog --start-position1 --stop-position500 /var/lib/mysql/binlog.000012注意mysqlbinlog也可以直接读多个binlog文件把文件名依次列出来即可。常见的恢复姿势是把所有需要的binlog按顺序拼起来然后管道交给mysql客户端执行。4.2 ROW格式怎么读ROW格式的binlog内容默认是一堆BASE64编码的event直接看根本不知道改了啥。这时候必须加上-v参数让它把事件解码成带###注释的伪SQLmysqlbinlog --base64-outputdecode-rows -v /var/lib/mysql/binlog.000012输出大概长这样### INSERT INTO test.t_user ### SET ### 110001 ### 2张三 ### 3138xxxx这里的1、2是字段序号不是真实字段名但配合SHOW CREATE TABLE看一眼就能看懂是哪一行的数据。4.3 遇到GTID怎么办如果你的环境开着GTIDgtid_mode ON用mysqlbinlog解析事务日志时会看到每个事务前面都有一段SET SESSION.GTID_NEXT xxxx。在恢复时需要特别注意这些GTID标记否则用binlog回放的SQL会在从库上被认为已经执行过从而被跳过。稳妥的做法是加--skip-gtids参数mysqlbinlog --skip-gtids /var/lib/mysql/binlog.000012这样在目标库上执行时就不会带上GTID信息事务会被强制重新执行一遍。4.4 一个小技巧快速定位误操作的Position假设你10点误删了数据但不知道具体从哪个位置开始。可以先解出全量日志用grep找到那条致命的DELETE语句往上找最近的COMMIT位置这个位置就是恢复截止点mysqlbinlog --base64-outputdecode-rows -v binlog.000012 | grep -n DELETE FROM | tail -n 20我经常用这个方法在上亿的binlog文件里定位比纯靠时间估算精准得多。5. 误删数据后的完整救援流程我用binlog成功恢复过的实战路径场景设定你的表t_order在上午10:00被一条DELETE FROM t_order WHERE status 1误执行全表符合条件的一共85000行被删。数据库有当天凌晨2点的全量备份binlog是开启的保留7天。这种情况下恢复思路非常清晰。5.1 第一步冻结现场防止二次污染发现误删之后第一件事不是马上恢复而是先不让线上系统继续产生新的写入。因为新写入也可能命中相同的主键造成恢复时主键冲突。稳妥做法是先把应用流量切换走或者锁住相关表。如果连DROP都发生了那就需要立刻停止整个实例的写入并备份现有数据目录。5.2 第二步定位binlog文件和截止位置先确认全量备份对应的binlog位置。备份文件里通常有CHANGE MASTER TO MASTER_LOG_FILEbinlog.000008, MASTER_LOG_POS123456;这样的记录这就是增量恢复的起始点。然后找到误删语句最后执行的位置作为增量恢复的截止点。这个截止位置必须是误操作语句开始之前的那个COMMIT位置。定位方法上面说过了用mysqlbinlog | grep或者干脆用时间范围过滤。5.3 第三步拼接增量日志并回放基础命令是这样的mysqlbinlog --start-position123456 --stop-position888888 binlog.000008 binlog.000009 binlog.000010 | mysql -uroot -p -h127.0.0.1 temp_db不过在实际操作里更稳妥的做法是先把binlog导成SQL文件人工检查一遍再执行mysqlbinlog --skip-gtids --start-position123456 --stop-position888888 binlog.000008 binlog.000009 binlog.000010 /tmp/incremental.sql打开/tmp/incremental.sql确认里面UPDATE和DELETE语句的目标确实是我们想要的。如果有几条无关事务混进来直接用文本编辑器把它们删掉再执行mysql -uroot -p -h127.0.0.1 temp_db /tmp/incremental.sql5.4 第四步针对误操作单独挖出来恢复有时候全量备份太老或者增量日志太多回放完整流程太慢。还有一种思路直接从binlog里把误删的那85000行的INSERT逆向出来。如果你的误删是通过DELETE执行的那么它在ROW格式binlog里对应的event会保存每一行的前像。也就是说binlog里其实存着每一行被删之前的样子这就是你能用来反向INSERT的数据。在解码出的伪SQL中你会看到### DELETE FROM db.t_order ### WHERE ### 11001 ### 22025-01-01 ### 399.00手动把每条DELETE FROM改写成INSERT INTO是个非常繁琐的体力活。实际操作中我一般先用文本编辑器或者写个简单的sed脚本提取这些行但真正高频使用的是现成的binlog回滚工具比如开源的binlog2sql。用binlog2sql的话一条命令就能把误删的DELETE转成对应的INSERTpython3 binlog2sql.py -h127.0.0.1 -P3306 -uroot -p密码 -d db -t t_order --start-position123456 --stop-position888888 -B rollback.sql生成的rollback.sql里就是可直接执行的INSERT语句。这个方案在误删、误更新场景下效率极高推荐大家提前在测试环境演练熟。5.5 恢复后的验证与复盘恢复不等于结束。恢复完成后必须比对总行数、关键业务字段的汇总值和误删前做的快照做一致性校验。比如SELECT COUNT(*) FROM t_order;是否等于备份加增量的预期值。另外恢复过程中如果发现Duplicate entry错误大概率是因为截止位置没选准恢复范围里包含了误删后的新插入语句。这种情况我会用--replace参数重新执行一遍增量SQL但注意这样会把误操作后发生的合法写入也覆盖掉。所以宁可在截止位置选点上多花时间也不要草率加replace。6. 主从复制中的关键角色binlog是怎样驱动从库的6.1 三条线程一条链MySQL主从复制的工作链路简单说就是主库生成binlog从库拉取binlog从库重放binlog。具体是三条线程在协作主库上的dump线程主库专门负责把binlog发送给从库的线程。从库上的I/O线程连接主库接收binlog并写入本地的relay log。从库上的SQL线程读取relay log按顺序执行日志中的事件。数据流向是主库的binlog - dump线程 - 网络 - IO线程 - relay log - SQL线程 - 从库数据。6.2 为什么要引入relay log很多人不理解从库直接执行binlog不就行了为什么还要多存一份relay log因为网络传输和本地执行的速度不一样。I/O线程从主库拉数据可以持续不断并提前拉取先把日志缓存在本地relay log里SQL线程根据自己的速度慢慢执行。这样就实现了接收和执行解耦主库的binlog不会因为从库执行慢而积压在网络传输层。6.3 binlog格式对复制的影响如果你用STATEMENT格式从库拿到的是SQL语句重新执行时有可能会出现上下文不同导致的不一致。比如主库执行UPDATE t SET create_time NOW()从库执行这条SQL时NOW()已经是另一个时间了导致两边数据不一致。用ROW格式从库拿到的是行变更的前后镜像不管从库环境怎么变执行结果都是确定的。这也是为什么我在第2章强烈建议核心业务用ROW格式。是主从数据一致性保障的第一道防线。6.4 半同步复制把可靠再提一档默认的异步复制模式下主库提交事务后不管从库有没有收到binlog直接返回成功。如果主库在binlog尚未传输到从库时宕机从库晋升为主库后会丢数据。半同步复制semi-sync让主库在事务提交时至少要等一个从库确认收到binlog并写入relay log之后才返回客户端成功。这个机制在金融、交易类系统中尤其重要。配置方式在MySQL 8.0中已经比较简洁INSTALL PLUGIN rpl_semi_sync_master SONAME semisync_master.so; INSTALL PLUGIN rpl_semi_sync_slave SONAME semisync_slave.so; SET GLOBAL rpl_semi_sync_master_enabled 1; SET GLOBAL rpl_semi_sync_slave_enabled 1;我的线上系统主从规模不大但我会坚持开启半同步复制因为多等待一个ACK的时间换来的是一层重要的数据安全保障。6.5 GTID让复制更简单也更健壮传统基于binlog文件名Position的复制方式切换主从时容易对错位置。GTIDGlobal Transaction Identifier给每个事务一个全局唯一ID从库只需记住自己执行到哪个GTID主从切换后自动续传。配置GTID复制后切换和故障转移的复杂度大幅降低。6.6 复制延迟怎么看从库执行SHOW SLAVE STATUS\G看Seconds_Behind_Master。数值为0说明跟上主库持续增大说明有延迟。排查延迟通常看三件事主库写并发是否过高SQL线程单线程执行跟不上。从库磁盘IO是否瓶颈relay log和数据文件落盘太慢。binlog是否包含超大事务比如一次批量更新百万行在从库上要执行很久很久。对于超大事务只要是单线程复制就必然延迟这是结构性问题。如果业务上确实无法避免超级大事务我会考虑对这类定时操作拆分成小批次提交这比从库开多线程复制更治本。MySQL 8.0的并发复制replica_parallel_workers能拉开一部分压力但对单个大事务依然无能为力。7. 刷盘策略、两阶段提交与面试高频考点7.1 sync_binlog参数至少要设置为1前面提过sync_binlog 1的含义是每个事务提交时都同步把binlog刷到磁盘。这个参数在生产环境帮助极大。如果把它设成0性能会好看很多但数据库崩溃后binlog可能没有完全落盘从库会缺少最近几个事务的日志这些事务在主库虽然提交成功但二进制日志里没有直接导致主从不一致。更严重的是没有binlog支撑的崩溃恢复主库可以把最新事务通过redo log恢复出来但因为没有binlog这个事务无法传给从库后续即使主库恢复从库也缺少这一段。所以这个性能优化的代价非常大。7.2 两阶段提交在执行路径上怎么看为了让你对两阶段提交有更直观的感受我们看一个小例子。假设事务T执行了UPDATE student SET score 90 WHERE id 1它提交时的流程是InnoDB层把id1这行数据页的修改写入redo log标记为prepare状态。MySQL Server层把这条语句或行变更写入binlog。InnoDB层把redo log标记为commit状态事务完成。这个机制保证了无论哪个环节崩溃重启后redo log和binlog都能对上账。如果崩溃发生在第2步之前重启后该事务被回滚binlog里不存在的记录就不会被从库执行如果崩溃发生在第3步之前重启后truncate掉没有完整binlog的事务保证主从一致。7.3 面试常问的binlog考点面试里最常出现的三个连环追问这里一并讲透。第一个问题binlog有哪几种格式各有什么优缺点。这在第2章已经详细讲过了记忆口诀是Statement省空间但不安全Row安全但费空间Mixed折中但不可控。第二个问题binlog和redo log有什么区别。从前面1.2节的表格里抽核心层级不同、内容不同、用途不同、写入时机不同。面试时把Server层 vs 存储引擎层和逻辑日志 vs 物理日志这两组关键词先抛出来再展开说。第三个问题主从复制中如果使用STATEMENT格式哪些函数会导致数据不一致。答NOW()、UUID()、RAND()这些非确定性函数以及LIMIT无ORDER BY的更新。再补一句所以生产建议用ROW格式。7.4 其他值得留意的binlog细节DDL也是记录在binlog中的CREATE TABLE、ALTER TABLE等DDL同样会记录所以binlog回放时表结构变更也能被复现。临时表对binlog不友好默认情况下临时表操作会写binlog但在基于ROW格式的复制中临时表本身不会被复制到从库这会导致从库执行相关操作时报错。所以带有临时表的存储过程在基于binlog的主从复制中尽量不要跨节点执行。binlog不会记录SELECT这点因为太基础反而容易被忽略回答面试题时主动说出来会显得你对日志边界定义很清楚。MySQL 8.0支持binlog加密binlog_encryption ON可以开启加密安全合规要求严格的企业建议打开。几个实践习惯最后一并交底写了这么多最后分享几个我自己多年养成的习惯希望能让你少走弯路。第一备份永远要有可恢复性演练。很多团队每天都跑全量备份但从没真正恢复过一次。三个月后真要恢复时才发现备份文件损坏、binlog被自动清理了。我会每季度找一台测试实例完整走一遍全量备份binlog增量恢复的流程整个过程控制在2小时以内跑通后整个团队对数据兜底能力心里都有底。第二binlog保留天数按业务恢复窗口来定。常见业务建议至少7天能够覆盖绝大多数行了到周五才发现周一的错这种故障场景。如果磁盘紧张宁可在云盘上挂大一点也不要擅自把保留时间压到3天以下。第三改完binlog相关配置后一定要做实际变更验证。配置了binlog_format ROW之后执行一条多行UPDATE然后用mysqlbinlog解出来看看是不是ROW格式的event。配置了bianlog_expire_logs_seconds 604800后不要只信SHOW VARIABLES最好手动算一下最老的binlog文件的修改时间和当前时间差确认清理机制真的在跑。第四恢复完成后千万别忘了校验数据完整性。把生产线上的traffic切回来之前至少做一次COUNT(*)对比、关键业务表SUM金额对比最好再抽查几条核心记录和备份前的快照进行比对。binlog是MySQL最底层也最可靠的数据安全网之一但它的价值只有在真正出事时才会兑现平时的每次配置验证、每次恢复演练都是在给自己留后路。下次再遇到还能不能恢复的灵魂拷问你可以笃定地回答能。
返回列表