MySQL 备份恢复实战:全量备份与 binlog 恢复

MySQL 备份恢复是每个后端和运维绕不开的底线工作。真正出事的时候,决定你能不能睡着觉的不是监控大盘,而是昨晚那份备份到底能不能恢复、能恢复到哪一秒。这篇文章把生产环境最常用的两条路线讲透:mysqldump 逻辑备份、XtraBackup 物理备份,再用 binlog 做时间点恢复(PITR),把误删数据追回到事故发生前一秒。所有命令都可直接在 MySQL 8.0 上照抄执行。

为什么”备份成功”经常是假的

我见过太多”每天凌晨三点跑了 mysqldump、日志里也没报错”的团队,真到要恢复时才发现备份根本用不了。常见的三类翻车方式如下:

翻车场景根本原因后果
备份文件能生成,恢复时报错中断没加 --single-transaction,导出期间有 DDL,dump 文件逻辑不一致恢复到一半失败,数据半残
备份齐全,但只能恢复到昨天凌晨没开 binlog 或 binlog 被过早清理,缺少增量丢失一整天业务数据
备份和数据库在同一台机器同一块盘没有异地/异盘副本磁盘损坏或误删目录时备份一起没了

结论很朴素:一份可用的备份 = 一致性的全量 + 连续的增量(binlog)+ 独立存储 + 定期演练。四个条件缺一个,备份就只是心理安慰。

三种备份方式怎么选

方式原理适合数据量优点短板
mysqldump导出 SQL 语句(逻辑备份)< 50 GB跨版本、跨平台,可只恢复单表;文件可读恢复慢(要逐条执行 SQL)
XtraBackup物理拷贝 InnoDB 数据文件 + 重放 redo50 GB ~ TB 级备份/恢复都快,支持增量、几乎不锁表版本目录结构强耦合,不能只挑一张表
云快照 / LVM 快照块设备级快照任意秒级完成,运维成本低依赖云厂商;恢复粒度是整盘

实践中最稳的组合是:XtraBackup 做每日全量 + binlog 做秒级增量 + mysqldump 每周补一份逻辑备份。逻辑备份的价值在于”只误删了一张表”时可以精准单表恢复,不用把整个实例回滚。如果你的库是分库分表架构,恢复前还要先理清路由规则,可以参考站内的分库分表实战:ShardingSphere 海量数据架构

逻辑备份:mysqldump 的正确姿势

一致性全库备份命令

# InnoDB 一致性全库备份(推荐模板)
mysqldump -h 127.0.0.1 -u backup -p \
  --single-transaction \
  --source-data=2 \
  --routines --events --triggers \
  --default-character-set=utf8mb4 \
  --hex-blob \
  --databases shop_db user_db \
  | gzip > /data/backup/full_$(date +%F_%H%M).sql.gz

# 关键参数说明
# --single-transaction : 开启一致性快照读,InnoDB 表备份期间不锁表
# --source-data=2      : 把 binlog 文件名与位点以注释写入 dump(8.0.26+ 语法,
#                        旧版本用 --master-data=2),这是后续 PITR 的起点
# --routines/--events/--triggers : 存储过程、定时事件、触发器一并备份(默认不含!)
# --hex-blob           : 二进制字段用十六进制导出,避免字符集损坏

三个最容易被忽略的点:一是 --single-transaction 只对 InnoDB 有效,若库里还有 MyISAM 表就得配合 --lock-tables;二是不加 --routines --events --triggers,存储过程和定时任务会静默丢失;三是 --source-data=2 必须加,否则你拿不到全量对应的 binlog 位点,增量就接不上去。

恢复与单表抽取

# 1) 整库恢复
gunzip < /data/backup/full_2026-08-30_0300.sql.gz | mysql -u root -p

# 2) 只恢复一张表:先从 dump 里把该表的段落抽出来
zcat full_2026-08-30_0300.sql.gz \
  | sed -n '/^-- Table structure for table `orders`/,/^-- Table structure for table `order_item`/p' \
  > orders_only.sql
mysql -u root -p shop_db < orders_only.sql

# 3) 恢复前先看全量对应的 binlog 位点(PITR 起点)
zcat full_2026-08-30_0300.sql.gz | head -30 | grep CHANGE
# 输出示例:
# -- CHANGE REPLICATION SOURCE TO SOURCE_LOG_FILE='mysql-bin.000042',
#    SOURCE_LOG_POS=1547832;

恢复大 dump 时可以临时把 innodb_flush_log_at_trx_commit=2、关闭 unique_checksforeign_key_checks 提速,恢复完再改回来。这类参数含义在MySQL 深度调优:核心参数与执行计划解读里有系统梳理。

物理备份:XtraBackup 全量与增量

数据量上到几百 GB,mysqldump 恢复可能要跑一整天,这时必须换物理备份。MySQL 8.0 请使用 Percona XtraBackup 8.0(版本要与 MySQL 大版本对应,8.0 备份不能用 2.4 工具恢复)。

# 全量备份
xtrabackup --backup \
  --user=backup --password='***' \
  --target-dir=/data/xtra/full_2026-08-30

# 增量备份(--incremental-basedir 指向上一次备份目录,形成增量链)
xtrabackup --backup \
  --user=backup --password='***' \
  --target-dir=/data/xtra/inc1 \
  --incremental-basedir=/data/xtra/full_2026-08-30

# 恢复:prepare 阶段先合并增量,中间步骤必须加 --apply-log-only
xtrabackup --prepare --apply-log-only --target-dir=/data/xtra/full_2026-08-30
xtrabackup --prepare --apply-log-only \
  --target-dir=/data/xtra/full_2026-08-30 --incremental-dir=/data/xtra/inc1
# 最后一次 prepare 不要加 --apply-log-only,让它回滚未提交事务
xtrabackup --prepare --target-dir=/data/xtra/full_2026-08-30

# 回填数据目录(必须先停 MySQL 且 datadir 为空)
systemctl stop mysqld
rm -rf /var/lib/mysql/*
xtrabackup --copy-back --target-dir=/data/xtra/full_2026-08-30
chown -R mysql:mysql /var/lib/mysql
systemctl start mysqld

这里有两个高频踩坑点:中间 prepare 漏加 --apply-log-only,会导致后续增量无法再叠加(因为未提交事务已被回滚);--copy-back 前 datadir 没清空,会直接报错退出。另外恢复完记得核对 xtrabackup_binlog_info 文件,里面记录了物理备份对应的 binlog 位点。

binlog 时间点恢复(PITR):把误删追回来

典型事故:下午 15:37 有人在生产执行了一条漏写 WHERE 的 DELETE。你有凌晨 3:00 的全量备份,目标是恢复到 15:36:59 的状态——这就是 PITR,整条链路是"全量恢复 + 重放 binlog 到事故前一刻"。

前置条件:binlog 必须已开启且保留足够

# 检查 binlog 是否开启与格式(生产必须为 ROW)
SHOW VARIABLES LIKE 'log_bin';                 -- 应为 ON
SHOW VARIABLES LIKE 'binlog_format';           -- 应为 ROW
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';  -- 8.0 保留时长,建议 >= 604800(7天)
SHOW BINARY LOGS;                              -- 当前有哪些 binlog 文件

# my.cnf 推荐配置
[mysqld]
log_bin = /var/log/mysql/mysql-bin
binlog_format = ROW
binlog_row_image = FULL
binlog_expire_logs_seconds = 1209600   # 保留 14 天
sync_binlog = 1                        # 每次提交刷盘,最高安全性

binlog_format=ROW 是能做精确恢复的前提:STATEMENT 格式下重放带 NOW()UUID() 的语句结果会漂移。binlog_row_image=FULL 则保证被删除行的完整镜像都在日志里,反向解析才能还原数据。这套 binlog 机制同时也是主从复制的基础,可对照MySQL 主从复制与读写分离:高可用架构实战理解。

定位事故位点并重放

# 1) 先把误操作找出来:解析 binlog 为可读文本,按时间窗口过滤
mysqlbinlog --base64-output=decode-rows -vv \
  --start-datetime="2026-08-30 15:30:00" \
  --end-datetime="2026-08-30 15:45:00" \
  /var/log/mysql/mysql-bin.000043 > /tmp/incident.sql
grep -n -B5 "DELETE FROM \`orders\`" /tmp/incident.sql | head -40
# 找到事故事务的起始 GTID 或 "# at 位点" 行,例如 # at 8834217

# 2) 在恢复实例上:先导入全量,再重放 binlog 到事故位点前
gunzip < /data/backup/full_2026-08-30_0300.sql.gz | mysql -u root -p

# 从全量记录的位点开始,重放到事故位点截止(--stop-position 精确到事故前)
mysqlbinlog --start-position=1547832 /var/log/mysql/mysql-bin.000042 \
  | mysql -u root -p
mysqlbinlog --stop-position=8834217 /var/log/mysql/mysql-bin.000043 \
  | mysql -u root -p

# 3) 若要跳过误删事务、继续重放之后的正常业务,则分两段重放
mysqlbinlog --stop-position=8834217  /var/log/mysql/mysql-bin.000043 > part1.sql
mysqlbinlog --start-position=8836902 /var/log/mysql/mysql-bin.000043 > part2.sql
cat part1.sql part2.sql | mysql -u root -p

三条铁律:第一,恢复永远在临时实例上做,验证数据正确后再把结果表同步回生产,绝不直接在故障库上重放;第二,动手前立刻停止业务写入或把 binlog 文件复制一份,防止事故日志被轮转清理;第三,用 --stop-position 而不是只用 --stop-datetime,因为同一秒内可能有多个事务,时间粒度不足以精确切分。开启 GTID 的实例还可以用 --exclude-gtids 直接排除某个事务,比算位点更直观。

把备份变成不用惦记的自动化

#!/bin/bash
# /opt/scripts/mysql_backup.sh  —— 每日全量 + 保留 14 天 + 校验 + 异地同步
set -euo pipefail
BACKUP_DIR=/data/backup
DATE=$(date +%F_%H%M)
FILE="$BACKUP_DIR/full_${DATE}.sql.gz"
LOG=/var/log/mysql_backup.log

mysqldump --defaults-extra-file=/etc/my.backup.cnf \
  --single-transaction --source-data=2 \
  --routines --events --triggers --hex-blob \
  --all-databases | gzip > "$FILE"

# 关键:验证备份完整性(gzip 校验 + 结尾标记检查),别只看退出码
gzip -t "$FILE"
zcat "$FILE" | tail -5 | grep -q "Dump completed" \
  || { echo "$(date) 备份不完整: $FILE" >> "$LOG"; exit 1; }

# 异地同步(对象存储或备份机),备份绝不能只留在本机
rclone copy "$FILE" oss:mysql-backup/$(date +%Y/%m)/

# 清理 14 天前的本地备份
find "$BACKUP_DIR" -name 'full_*.sql.gz' -mtime +14 -delete
echo "$(date) 备份成功 $FILE $(du -h "$FILE" | cut -f1)" >> "$LOG"

# crontab:每天 03:00 全量,binlog 依赖 MySQL 自身滚动
# 0 3 * * * /opt/scripts/mysql_backup.sh

注意脚本里用 --defaults-extra-file 存放账号密码而不是写在命令行,避免密码出现在 ps 输出中。备份账号只需要 SELECT, RELOAD, PROCESS, LOCK TABLES, REPLICATION CLIENT 这几个最小权限。如果你希望把备份校验结果接入告警,可以把这个脚本挂到 CI 或定时流水线里跑,思路参考GitHub Actions 实战:从零搭建 CI/CD 流水线。备份磁盘 I/O 压力大时,也别忘了检查系统层参数,见Linux 内核参数调优:高并发服务器 sysctl 实战

没演练过的备份等于没有备份

演练项频率验收标准
全量备份还原到临时实例每月一次能启动、表数量与行数与源库一致
PITR 恢复到指定时间点每季度一次能恢复到误操作前一秒,记录实际 RTO
单表恢复每季度一次不影响其他表,30 分钟内完成
异地备份可下载可解压每月一次gzip -t 通过且能成功导入

演练时一定要把两个数字写进文档:RPO(最多能丢多少数据,取决于 binlog 是否实时归档)和 RTO(多久能恢复服务,取决于备份方式和数据量)。只有真正跑过一遍完整恢复,你才知道"3 小时内恢复"是承诺还是幻想。跨引擎对比也值得一看:PostgreSQL 的时间点恢复思路与 MySQL 高度相似但工具链不同,可参考PostgreSQL 备份与 PITR 时间点恢复实战

小结

MySQL 备份恢复的核心不复杂:小库用 mysqldump--single-transaction --source-data=2,大库用 XtraBackup 做全量加增量,两者都必须配合 ROW 格式的 binlog 才能实现秒级 PITR。真正拉开差距的是三件小事——备份后校验完整性、备份异地存一份、定期做恢复演练。把这三件事做成自动化,误删数据就从"事故"降级成"一次有记录的恢复操作"。如果你还想让恢复更快,可以顺着MySQL 索引底层原理:B+树、覆盖索引与最左前缀继续优化恢复后的重建索引环节。

上一篇 Linux 内核参数调优:高并发服务器 sysctl 实战
下一篇 React 性能优化实战:Hooks 陷阱与重渲染治理