Appearance
备份与恢复
备份的目标不是在某个时间点生成一个文件——而是在需要的时候能把数据恢复到指定的状态。备份文件存在、能恢复、恢复后数据正确,这三件事要分开验证。很多人只做了第一步(产生备份文件)就以为万事大吉,真出事的时候才发现备份根本恢复不了。
一、备份组合
常见备份方式各有分工:逻辑备份(mysqldump)适合小库、跨版本迁移、跨环境恢复,但几百 GB 以上的库恢复很慢;物理热备(XtraBackup)适合大库、追求恢复速度,但跨大版本恢复可能有问题;binlog 增量(mysqlbinlog)回放到指定时间点,但依赖日志连续性,不能单独当备份用;存储快照(LVM/云盘快照)快速备份和克隆,但需要处理数据库文件的一致性。
常规备份策略是这样搭配的:
text
全量备份(昨晚的 mysqldump 或 XtraBackup)+ binlog(全量之后当天所有的变更)
= 能恢复到今天上午误删之前的任意时间点几个概念的关键区分要理清:全量备份能还原到一个完整的时间点状态,但只能恢复到备份完成那一刻;binlog能重放备份之后的变更,但必须基于一个全量备份才能起步;两者配合起来才能做时间点恢复(PITR),如果 binlog 断了、丢了就有限制。
二、mysqldump 逻辑备份
备份单库:
bash
mysqldump \
-uroot -p \
--single-transaction \
--routines \
--triggers \
--events \
app_db > app_db.sql几个参数:--single-transaction 导出前开启一个一致性读事务,InnoDB 表在导出过程中看到的是同一时刻的数据;--routines 带上存储过程和函数;--triggers 带上触发器;--events 带上定时事件。
备份所有库:
bash
mysqldump -uroot -p --all-databases --single-transaction \
--routines --triggers --events > all.sql仅备份某几张表:
bash
mysqldump -uroot -p app_db orders order_items > orders.sql压缩节省空间:
bash
mysqldump -uroot -p --single-transaction app_db \
| gzip > app_db-$(date +%F).sql.gz--single-transaction 的"一致性"只对 InnoDB 这类事务表有效。如果库里有 MyISAM 表,它们在 mysqldump 过程中仍然可能发生变化,导致备份文件内的数据并非同一时刻的快照。所以生产库建表基本都用 InnoDB 而不是 MyISAM,备份一致性也是原因之一。
三、记录 binlog 位点
备份时把当时的 binlog 位点写进 dump 文件,这是为时间点恢复做准备:
bash
mysqldump -uroot -p --single-transaction --master-data=2 \
app_db > app_db.sql查看位点:
bash
grep -m1 'CHANGE MASTER' app_db.sql输出里有 binlog 文件名和位置。这个位点就是全量备份对应的时间戳——恢复时,全量到这一步,binlog 从这个位点开始回放。没有这个位点,全量备份和 binlog 就接不上。
四、mysqldump 恢复
首先创建目标库:
sql
CREATE DATABASE app_db DEFAULT CHARACTER SET utf8mb4;导入:
bash
mysql -uroot -p app_db < app_db.sql压缩文件的恢复:
bash
gunzip -c app_db.sql.gz | mysql -uroot -p app_db恢复所有库:
bash
mysql -uroot -p < all.sql恢复前确认目标——是恢复到生产库、测试库,还是临时实例。导错目标是比备份本身失败更严重的操作,把测试数据导进生产库等于一次数据事故。
临时实例恢复验证:
bash
mysql -h 127.0.0.1 -P 3307 -uroot -p < app_db.sql五、binlog 回放
先看有哪些 binlog 文件:
sql
SHOW BINARY LOGS;查看 binlog 内容(raw 格式的人类可读版本):
bash
mysqlbinlog /data/mysql/logs/mysql-bin.000123 | less按时间范围回放到临时实例:
bash
mysqlbinlog \
--start-datetime='2026-05-21 10:00:00' \
--stop-datetime='2026-05-21 10:30:00' \
/data/mysql/logs/mysql-bin.000123 \
| mysql -uroot -p按位点(更精确):
bash
mysqlbinlog \
--start-position=154 \
--stop-position=98231 \
/data/mysql/logs/mysql-bin.000123 \
| mysql -uroot -p做时间点恢复时,核对这四件事:备份可用性(全量备份能不能恢复成功,备份里 binlog 位点是否清楚)、binlog 连续性(从备份位点到目标时间之间,binlog 文件是否连续完整)、数据正确性(恢复后关键表行数、重要数据是否符合预期)、回写方案(是从临时实例导出少量数据补回生产,还是整库切换)。
少量数据误删的典型恢复流程:
text
1. 确认误删发生的时间
2. 找到这个时间之前最近的一次全量备份
3. 在临时实例恢复全量备份
4. 回放 binlog 到误删发生前的那一刻
5. 从临时实例导出需要恢复的数据
6. 导入生产库导出指定行的数据:
bash
mysqldump -uroot -p --where="id in (1001,1002,1003)" \
app_db orders > restore_orders.sql导入前看一眼 SQL 内容确认:
bash
less restore_orders.sql六、XtraBackup 物理热备
mysqldump 导出的是 SQL 文本,上 GB 就开始吃力。几百 GB 以上的库,用 XtraBackup 做物理备份——直接复制 InnoDB 数据文件,恢复时也是文件级恢复,速度比 SQL 逐行导入快一个量级。
XtraBackup 不能像 mysqldump 那样单库单表导出——它针对的是整个实例的数据目录。版本要和 MySQL 版本匹配,8.0 的 XtraBackup 不能备份 5.7 的实例。
全量备份:
bash
xtrabackup \
--backup \
--target-dir=/backup/mysql/full-$(date +%F) \
--user=backup \
--password='BackupPassword_123!'备份完成后需要 --prepare——应用备份期间产生的 redo,让备份目录内的数据文件变成一致可用的状态:
bash
xtrabackup \
--prepare \
--target-dir=/backup/mysql/full-2026-05-21恢复时,先停 MySQL,然后拷贝回去:
bash
systemctl stop mysqld
mv /data/mysql/data /data/mysql/data.bak.$(date +%F-%H%M%S)
mkdir -p /data/mysql/data
xtrabackup --copy-back --target-dir=/backup/mysql/full-2026-05-21
chown -R mysql:mysql /data/mysql/data
systemctl start mysqld物理恢复会覆盖整个 datadir。恢复前确认目标实例是对的——把测试恢复的命令用在生产环境上,恢复就会变成一次数据丢失事故。这种事发生过,而且不可逆。
七、备份账号
备份账号和普通应用账号分开:
sql
CREATE USER 'backup'@'10.0.%' IDENTIFIED BY 'BackupPassword_123!';
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES, RELOAD, PROCESS, REPLICATION CLIENT
ON *.* TO 'backup'@'10.0.%';XtraBackup 可能需要更多权限(如 BACKUP_ADMIN),按实际版本文档确认。备份账号的好处:权限范围明确,即使备份工具或脚本被攻击,也不会泄露高权限账号——纵深防御的一环。
八、备份校验与演练
备份文件不是"产生了"就可靠,要做校验:
bash
sha256sum app_db.sql.gz > app_db.sql.gz.sha256
sha256sum -c app_db.sql.gz.sha256定期做恢复演练——在一个临时实例上恢复全量备份,跑几条核心查询:
sql
SHOW TABLES;
SELECT COUNT(*) FROM orders;
CHECK TABLE orders;验证的目的是确认:备份文件能恢复、恢复后表能正常读、关键表行数和业务预期一致。备份任务成功日志不一定能证明"恢复能做"——日志说成功,文件可能损坏、字符集弄错、或有表被漏掉没导出来。不演练的备份等于没有备份。
九、备份保留与清理
| 备份类型 | 保留时间 | 为什么 |
|---|---|---|
| 每日全量 | 7-14 天 | 能回到最近一两周内任意一天 |
| 每周全量 | 4-8 周 | 更长时间跨度的完整恢复点 |
| 每月全量 | 6-12 月 | 合规和审计需要 |
| binlog | 覆盖全量备份间隔 | 确保 binlog 不断链 |
清理前先看要删什么:
bash
find /backup/mysql -type f -mtime +30 -print确认输出范围没问题后再真正删除:
bash
find /backup/mysql -type f -mtime +30 -delete备份清理的节奏和恢复需求直接相关。binlog 清太快(比如只保留两天),全量备份间隔却是一周——那中间好几天无法做时间点恢复。全量频率和 binlog 保留时间要匹配,这是设计备份策略时要算清楚的关键点。
十、定时备份脚本
前面几节讲的备份方式、账号、校验、保留策略,最后得串成一个能自动跑、能留下证据、能回溯的定时任务,不然全靠人手备份迟早会忘。这一节给一个生产可用的完整方案:一个 mysqldump 备份脚本 + crontab 定时执行,把前面讲的 --single-transaction、binlog 位点、压缩、校验、轮转全部落地。
备份凭据单独存
密码不要写死在脚本里,更不要出现在命令行——ps 能看到其他进程的命令行参数,命令行带 --password 等于明文暴露。用单独的 .my.cnf 存凭据:
ini
# /root/.mysql-backup.cnf,权限必须是 600,否则 mysqldump 会警告
[client]
user=backup
password=BackupPassword_123!bash
chmod 600 /root/.mysql-backup.cnfmysqldump 用 --defaults-extra-file 读这个文件,命令行里就没有密码了。
备份脚本
bash
#!/usr/bin/env bash
# /usr/local/sbin/mysql-backup.sh
set -euo pipefail
# ---- 配置区 ----
mysql_host="127.0.0.1"
mysql_port=3306
defaults_file="/root/.mysql-backup.cnf"
databases="app_db" # 多个库用空格分隔,备份全部用 --all-databases
backup_root="/backup/mysql"
retention_days=7 # 保留天数
log_file="/var/log/mysql-backup.log"
# ---- 锁,防止上一次没跑完下一次又触发 ----
lock_file="/var/run/mysql-backup.lock"
exec 9>"$lock_file"
flock -n 9 || { echo "[$(date '+%F %T')] 上一次备份还在跑,跳过" >> "$log_file"; exit 0; }
# ---- 初始化 ----
today="$(date +%F)"
backup_dir="$backup_root/$today"
mkdir -p "$backup_dir"
log() {
printf '[%s] %s\n' "$(date '+%F %T')" "$*" | tee -a "$log_file"
}
log "开始备份:host=$mysql_host databases=$databases"
# ---- 执行备份 ----
for db in $databases; do
dump_file="$backup_dir/${db}-${today}.sql.gz"
log "备份 $db -> $dump_file"
mysqldump \
--defaults-extra-file="$defaults_file" \
--host="$mysql_host" \
--port="$mysql_port" \
--single-transaction \
--master-data=2 \
--routines \
--triggers \
--events \
"$db" | gzip > "$dump_file"
# 生成校验文件,方便事后验证备份完整性
sha256sum "$dump_file" > "${dump_file}.sha256"
log "$db 备份完成,大小 $(du -h "$dump_file" | cut -f1)"
done
# ---- 顺便记一下当前 binlog 位点,留作时间点恢复的参考 ----
mysqldump \
--defaults-extra-file="$defaults_file" \
--host="$mysql_host" \
--port="$mysql_port" \
-N -B \
-e "SHOW MASTER STATUS" > "$backup_dir/binlog-position-${today}.txt" 2>/dev/null || true
# ---- 清理过期备份 ----
log "清理 ${retention_days} 天前的旧备份"
find "$backup_root" -maxdepth 1 -type d -mtime +"$retention_days" -print >> "$log_file"
find "$backup_root" -maxdepth 1 -type d -mtime +"$retention_days" -exec rm -rf {} +
log "备份流程结束"脚本要点逐条对应前面讲的内容:--single-transaction 保证一致性快照(第二节)、--master-data=2 把 binlog 位点写进 dump 文件(第三节)、--routines/--triggers/--events 带全存储过程触发器和事件、gzip 压缩省空间、sha256sum 生成校验文件(第八节)、find -mtime 清理过期备份(第九节)、flock 防止并发。每个细节都不是多余的,去掉任何一个都可能在实际出事的时候露馅。
crontab 定时执行
脚本放好后,用 cron 每天凌晨跑一次:
bash
chmod +x /usr/local/sbin/mysql-backup.sh
crontab -ecron
# 每天凌晨 2:30 执行 MySQL 备份
30 2 * * * /usr/local/sbin/mysql-backup.sh时间选凌晨 2 点到 4 点之间,这个时段业务低峰、写入压力小,--single-transaction 的一致性快照更稳定。别选整点(0 点、2 点整)——太多任务挤在整点跑,容易撞车,错开几分钟(比如 2:30)更稳。
cron 环境的几个坑
脚本手动跑没问题、放进 cron 就失败,十有八九是环境差异:
- PATH 不一样:cron 的 PATH 通常只有
/usr/bin:/bin,mysqldump如果装在/usr/local/mysql/bin/就找不到。要么脚本里写绝对路径,要么在脚本开头export PATH:bashexport PATH="/usr/local/mysql/bin:/usr/bin:/bin:$PATH" .my.cnf找不到:cron 执行时HOME可能不是/root,--defaults-extra-file用绝对路径最保险。- 权限问题:cron 以哪个用户跑,这个用户就得能读
.my.cnf、能写备份目录和日志文件。生产备份一般用 root 的 crontab 跑,省得调权限。 - 错误没察觉:
set -euo pipefail让脚本失败即退出,但 cron 不会主动告诉你失败了。配置邮件告警,或者在脚本失败时主动发通知(比如调企业微信/钉钉 webhook),不然备份静默失败几天你都不知道。
验证备份真的能用
定时备份跑起来之后,一定要做一次恢复演练(第八节讲过)。把某天的备份文件恢复到一台测试实例,跑几条核心查询,确认数据完整。光看 mysqldump 退出码是 0、日志写了"备份完成",不代表备份文件没坏——磁盘故障、压缩损坏、字符集问题都可能让备份文件看起来正常但实际恢复不了。没演练过的备份不能信任,这条得当成铁律。