Linux服务器如何配置自动备份数据库?MySQL定时备份教程

"数据丢了怎么办"这个问题,答案只有一个:从备份恢复。但很多人的备份其实是假的——脚本写了,crontab 也配了,可从没验证过能不能恢复;或者备份脚本一直在静默失败,日志里全是报错,直到某天真的需要恢复时才发现最近一次成功的备份是三个月前。

这篇文章从备份策略讲起,给出一个可以直接用的 mysqldump 自动备份脚本(含压缩、校验、日志、错误处理、保留周期),然后讲 crontab 配置、异地备份、恢复演练的完整步骤,以及大库场景下的 xtrabackup 物理备份和基于 binlog 的时间点恢复。所有命令都区分 RHEL/CentOS/Rocky 与 Debian/Ubuntu 两套。文中的数字是经验值,最终要按你的数据量和业务容忍度调整。

一、先把备份策略定清楚

全量、增量与 binlog

全量备份:某一时刻的完整数据副本。mysqldump 导出的是逻辑备份(SQL 语句),xtrabackup 产出的是物理备份(数据文件副本)。

增量备份:只备份上次备份之后变化的部分。通常基于 binlog 实现。

binlog(二进制日志):记录所有数据变更操作。开启后可以基于全量备份 + binlog 重放恢复到任意时间点。

最实用的组合是:定期全量 + 持续保存 binlog。 比如每天一次全量,binlog 保留 7 天。这样理论上可以恢复到 7 天内的任意一秒。

3-2-1 备份原则

这是业界通用的备份准则:

  • 3 份副本:原始数据 + 至少 2 份备份
  • 2 种不同介质:比如本地磁盘 + 对象存储,或者本地 + 另一台机器
  • 1 份异地:至少一份在物理上分离的地方(不同机房、不同城市、或云上对象存储)

为什么异地很重要:火灾、水淹、盗窃、机房整体故障、云厂商区域故障,这些都会让同机房的备份和数据一起没。另外,误操作(比如 rm -rf)通常会同时删掉数据和同机备份。

保留周期

一个常见的经验配置:

  • 本地保留 7 天:用于快速回滚近期误操作
  • 异地保留 30 天:应对更晚才发现的逻辑错误
  • 月度归档保留 12 个月:合规需求或历史追溯

具体保留多久取决于你的业务:数据变更频繁、问题发现快的可以短一些;有合规要求的按合规来。关键是明确写出来并让脚本自动执行,而不是靠人记得清理。

RPO 与 RTO

这两个指标决定了你的备份方案该投入多少:

RPO(恢复点目标):能容忍丢失多长时间的数据。RPO = 1 小时意味着最坏情况丢一小时的数据。每天一次全量 + binlog 的 RPO 可以做到接近 0(只要 binlog 已落盘并同步走)。

RTO(恢复目标时间):需要多快恢复服务。RTO 决定了你用逻辑备份还是物理备份——mysqldump 恢复 100G 数据可能要几小时,xtrabackup 恢复同样大小只要几十分钟(主要是拷贝文件的时间)。

先把这两个数字定下来,再选方案。 很多争论(用不用 xtrabackup、多久备一次)本质上都是 RPO/RTO 的取舍。

二、mysqldump 的关键参数

最基础的用法

mysqldump -uroot -p mydb > /backup/mydb.sql

但这个命令在生产上有严重问题:它会在备份期间锁表(对 MyISAM 是锁全表,对 InnoDB 在不加参数时也可能因为一致性需要而加锁),导致业务写入阻塞。

正确的基础命令

mysqldump -uroot -p \
  --single-transaction \
  --routines \
  --triggers \
  --events \
  --set-gtid-purged=OFF \
  --databases mydb > /backup/mydb.sql

逐个解释:

参数 作用 说明
--single-transaction 在单个事务内做一致性快照 InnoDB 必备,不锁表。只对 InnoDB 有效
--routines 导出存储过程和函数 默认不导出,容易漏
--triggers 导出触发器 默认导出,显式写上更清楚
--events 导出事件调度器 默认不导出
--set-gtid-purged 控制是否写 GTID 信息 见下文
--master-data=2 记录备份时刻的 binlog 位点 用于时间点恢复,以注释形式写入
--databases 导出多个库,带 CREATE DATABASE 不带这个参数则不含建库语句
--all-databases 导出所有库 包含系统库,恢复时要小心
--default-character-set=utf8mb4 指定字符集 避免乱码
--hex-blob 二进制字段用十六进制导出 有 BLOB 时建议加
--no-data 只导出结构 用于结构对比
--where 按条件导出部分数据 大表分批导出用

--single-transaction 的原理与限制

它开启一个事务,利用 InnoDB 的 MVCC 拿到一致性快照,备份期间不阻塞读写。但有重要限制:

  • 只对 InnoDB 表有效。 如果库里有 MyISAM 表,它们仍会被锁,且拿不到一致快照。先用第 061 篇的 SQL 检查有没有非 InnoDB 表
  • 备份期间不能有 DDL。 ALTER TABLE、DROP TABLE、TRUNCATE 会导致一致性快照失效,甚至让 mysqldump 报错。所以备份要避开 DDL 窗口
  • 长事务会拖慢备份。备份开始时如果有长时间未提交的事务,mysqldump 可能等待

--set-gtid-purged 怎么选

这个参数在开启 GTID 的实例上很关键:

  • --set-gtid-purged=ON(默认):导出文件里带 SET @@GLOBAL.GTID_PURGED,恢复时会设置 GTID 集合。用于搭建从库时使用
  • --set-gtid-purged=OFF:不写 GTID 信息,恢复后 GTID 集合不受影响。普通备份恢复用这个
  • --set-gtid-purged=AUTO:如果实例开了 GTID 就等同 ON

普通备份场景建议显式写 OFF。 如果用 ON 的文件恢复到已有数据的实例,可能因为 GTID 冲突导致后续复制出错。

未开启 GTID 的实例则不需要这个参数,可以用 --master-data=2 记录 binlog 位点:

mysqldump -uroot -p --single-transaction --master-data=2 --routines --triggers --events mydb > backup.sql

恢复后可以从文件头部看到位点:

head -30 backup.sql | grep -i 'CHANGE MASTER'

分库备份 vs 整库备份

推荐分库备份。 好处是:单库恢复不用动其他库、单个文件更小更好传输、某个库失败不影响其他库。

# 列出所有业务库(排除系统库)
mysql -uroot -p -N -e "SELECT schema_name FROM information_schema.schemata \
  WHERE schema_name NOT IN ('mysql','sys','performance_schema','information_schema')"

风险提示:把密码直接写在命令行会被 ps 看到并进入 history。正确做法是配置 ~/.my.cnf:

[client]
user=backup_user
password=你的密码
host=localhost
chmod 600 ~/.my.cnf

或者用 MySQL 自带的凭据存储:

mysql_config_editor set --login-path=backup --host=localhost --user=backup_user --password
# 之后用 mysql --login-path=backup 连接

备份账号需要什么权限

不要直接用 root。创建一个专用备份账号:

CREATE USER 'backup_user'@'localhost' IDENTIFIED BY '强密码';
GRANT SELECT, RELOAD, PROCESS, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER, REPLICATION CLIENT
  ON *.* TO 'backup_user'@'localhost';
FLUSH PRIVILEGES;
  • SELECT:读数据
  • RELOAD:执行 FLUSH TABLES、FLUSH LOGS
  • PROCESS:查看当前会话(配合 --single-transaction)
  • LOCK TABLES:非 InnoDB 表需要
  • REPLICATION CLIENT:读取 binlog 位点
  • SHOW VIEW、EVENT、TRIGGER:导出视图、事件、触发器

三、完整的备份脚本

下面是一个可直接使用的脚本,包含压缩、校验、日志、错误处理和保留清理。

#!/bin/bash
# /opt/scripts/mysql_backup.sh
# MySQL 自动备份脚本:分库全量备份 + gzip 压缩 + 校验 + 日志 + 保留清理

set -uo pipefail

# ============ 配置区 ============
BACKUP_ROOT="/data/backup/mysql"
DATE=$(date +%Y%m%d)
TIME=$(date +%H%M%S)
DAY_DIR="${BACKUP_ROOT}/${DATE}"
LOG_FILE="${BACKUP_ROOT}/backup.log"
KEEP_DAYS=7                       # 本地保留天数
MYSQL_CNF="/root/.my.cnf"         # 凭据文件,权限必须 600
EXCLUDE_DB="mysql sys performance_schema information_schema"
# ===============================

mkdir -p "${DAY_DIR}"

log() {
    echo "[$(date '+%F %T')] $*" | tee -a "${LOG_FILE}"
}

log "===== 备份开始 ====="

# 1. 检查凭据文件权限
if [ ! -f "${MYSQL_CNF}" ]; then
    log "ERROR: 凭据文件 ${MYSQL_CNF} 不存在,退出"
    exit 1
fi
PERM=$(stat -c %a "${MYSQL_CNF}")
if [ "${PERM}" != "600" ] && [ "${PERM}" != "400" ]; then
    log "WARN: 凭据文件权限为 ${PERM},建议改为 600"
fi

# 2. 确认数据库可连接
if ! mysql --defaults-file="${MYSQL_CNF}" -e "SELECT 1" >/dev/null 2>&1; then
    log "ERROR: 无法连接 MySQL,备份中止"
    exit 1
fi

# 3. 逐个库备份
FAILED=0
DB_LIST=$(mysql --defaults-file="${MYSQL_CNF}" -N -e \
    "SELECT schema_name FROM information_schema.schemata" 2>/dev/null)

for DB in ${DB_LIST}; do
    # 跳过系统库
    case " ${EXCLUDE_DB} " in
        *" ${DB} "*) continue ;;
    esac

    OUTFILE="${DAY_DIR}/${DB}_${TIME}.sql.gz"
    log "开始备份库: ${DB}"

    if mysqldump --defaults-file="${MYSQL_CNF}" \
        --single-transaction \
        --routines --triggers --events \
        --set-gtid-purged=OFF \
        --default-character-set=utf8mb4 \
        --hex-blob \
        --databases "${DB}" 2>>"${LOG_FILE}" | gzip > "${OUTFILE}"; then

        # 压缩完成后做完整性校验
        if gzip -t "${OUTFILE}" 2>/dev/null; then
            SIZE=$(du -h "${OUTFILE}" | cut -f1)
            MD5=$(md5sum "${OUTFILE}" | awk '{print $1}')
            echo "${MD5}  ${OUTFILE}" >> "${DAY_DIR}/checksums.txt"
            log "备份成功: ${DB} -> ${OUTFILE} (${SIZE})"
        else
            log "ERROR: ${OUTFILE} 压缩文件校验失败"
            FAILED=$((FAILED+1))
        fi
    else
        log "ERROR: 库 ${DB} 备份失败"
        FAILED=$((FAILED+1))
    fi
done

# 4. 清理过期备份(按目录名日期删除,务必确认路径正确)
if [ -n "${BACKUP_ROOT}" ] && [ -d "${BACKUP_ROOT}" ]; then
    find "${BACKUP_ROOT}" -maxdepth 1 -type d -name '20*' -mtime +${KEEP_DAYS} \
        -print -exec rm -rf {} \; >> "${LOG_FILE}" 2>&1
    log "已清理 ${KEEP_DAYS} 天前的备份"
fi

if [ ${FAILED} -eq 0 ]; then
    log "===== 备份完成,全部成功 ====="
else
    log "===== 备份完成,有 ${FAILED} 个库失败 ====="
    exit 1
fi

脚本要点说明:

用 set -uo pipefail 而不是 set -e:因为 mysqldump | gzip 的管道中,set -e 的行为容易误判,这里改成显式检查每个命令的返回值更可靠。

gzip -t 做完整性校验:能检测出压缩过程中断、磁盘写满等问题导致的残缺文件。这一步不能省,否则你可能存了一堆解压就报错的"备份"。

md5sum 写入 checksums 文件:恢复前可以先校验文件是否被篡改或损坏。

find ... -mtime +N:按修改时间删除 N 天前的目录。rm -rf 是高危操作,脚本里加了路径非空判断,但仍然要确认 BACKUP_ROOT 配置正确。建议第一次运行前把 -exec rm -rf {} \; 改成 -print 先看看会删什么。

退出码:有失败就返回非 0,这样配合监控可以告警。

部署脚本

sudo mkdir -p /opt/scripts /data/backup/mysql
sudo vi /opt/scripts/mysql_backup.sh        # 粘贴上面的内容
sudo chmod 700 /opt/scripts/mysql_backup.sh  # 含凭据路径,权限收紧
sudo chown root:root /opt/scripts/mysql_backup.sh

手动先跑一次验证:

sudo /opt/scripts/mysql_backup.sh
echo $?          # 应为 0
ls -lh /data/backup/mysql/$(date +%Y%m%d)/
tail -30 /data/backup/mysql/backup.log

一定要手动跑通再放进 crontab。 直接放 crontab 的话,环境变量缺失导致的失败往往要几天后才发现。

四、crontab 定时配置

sudo crontab -e

每天凌晨 2 点执行:

0 2 * * * /opt/scripts/mysql_backup.sh >> /data/backup/mysql/cron.log 2>&1

每 6 小时一次(数据变更频繁时):

0 */6 * * * /opt/scripts/mysql_backup.sh >> /data/backup/mysql/cron.log 2>&1

crontab 的几个坑:

环境变量问题。 cron 的环境 PATH 很短,可能找不到 mysqldump、gzip。解决办法:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 2 * * * /opt/scripts/mysql_backup.sh >> /data/backup/mysql/cron.log 2>&1

或者在脚本里写绝对路径(用 which mysqldump 查)。这是 crontab 最常见的失败原因——手动跑成功、放进 cron 就失败。

百分号要转义。 crontab 里 % 有特殊含义(表示换行),如果命令里有 date +%Y%m%d 要写成 date +\%Y\%m\%d。我们把它写在脚本里就避开了这个问题。

确认 cron 服务在跑:

# RHEL/CentOS/Rocky
sudo systemctl status crond
# Debian/Ubuntu
sudo systemctl status cron

看执行记录:

# RHEL/CentOS/Rocky
sudo grep CRON /var/log/cron
# Debian/Ubuntu
sudo grep CRON /var/log/syslog

备份时间要避开业务高峰和 DDL 窗口。 凌晨 2 点通常是业务低峰,但如果你的备份要跑几小时,要确认不会延续到早高峰。可以用 time 测一下实际耗时。

五、异地备份

用 rsync 同步到另一台机器

# 安装 rsync
# RHEL/CentOS/Rocky
sudo dnf install -y rsync
# Debian/Ubuntu
sudo apt-get install -y rsync

配置 SSH 免密(在数据库服务器生成密钥,把公钥放到备份机):

ssh-keygen -t ed25519 -f /root/.ssh/backup_rsa -N ''
ssh-copy-id -i /root/.ssh/backup_rsa.pub backupuser@备份机IP

安全建议:为备份专用账号创建单独的密钥,并在备份机的 authorized_keys 里用 command= 和 from= 限制它能执行的命令和来源 IP。

同步脚本:

rsync -avz --delete \
  -e "ssh -i /root/.ssh/backup_rsa -o StrictHostKeyChecking=yes" \
  /data/backup/mysql/ backupuser@备份机IP:/data/mysql-backup/

--delete 会删除目标端多余的文件,用之前先加 --dry-run 看看会发生什么:这个参数配错目录会删掉备份机上的数据。

同步到对象存储

主流云厂商都提供对象存储,配合命令行工具上传:

# 以 AWS S3 兼容接口为例,具体工具按你的云厂商选择
aws s3 sync /data/backup/mysql/ s3://your-bucket/mysql-backup/ \
  --storage-class STANDARD_IA

上传要走内网端点(云厂商一般提供内网域名),否则备份大的话公网流量费用可观,速度也慢。

异地保留 30 天的实现

异地端的清理逻辑独立实现(不要在源端用 rsync 的 --delete 来管理异地保留周期)。在备份机上另跑一个清理任务:

find /data/mysql-backup/ -maxdepth 1 -type d -name '20*' -mtime +30 -exec rm -rf {} \;

同样要先去掉 -exec 用 -print 验证一遍。

加密备份(含敏感数据时)

如果备份里包含用户手机号、身份证等敏感信息,异地存储应该加密。用 GPG 对称加密:

# 加密
gpg --batch --yes --passphrase-file /root/.backup-pass \
    --symmetric --cipher-algo AES256 -o backup.sql.gz.gpg backup.sql.gz

# 解密
gpg --batch --yes --passphrase-file /root/.backup-pass \
    --decrypt -o backup.sql.gz backup.sql.gz.gpg

密钥管理是难点:密码文件权限必须 600,且不要把密码文件和备份放在同一个地方(否则加密就没意义了)。更规范的做法是非对称加密(用公钥加密,私钥离线保管),这样备份机上只有公钥,即使被攻破也解不开备份。

六、恢复演练:备份能不能用,只有恢复了才知道

逻辑备份的恢复步骤

1. 先找对文件并校验:

ls -lh /data/backup/mysql/20261010/
cd /data/backup/mysql/20261010/
md5sum -c checksums.txt          # 校验完整性
gzip -t xxx.sql.gz               # 校验压缩文件

2. 恢复到测试实例(强烈建议先这么做):

# 在测试机上创建空库,导入验证
gunzip < /data/backup/mysql/20261010/mydb_020000.sql.gz | mysql -uroot -p

# 或者先解压再导入
gunzip -c backup.sql.gz > /tmp/restore.sql
mysql -uroot -p < /tmp/restore.sql

3. 验证数据:

USE mydb;
SELECT COUNT(*) FROM 核心表;
SELECT MAX(id), MAX(created_at) FROM 核心表;
SHOW TABLES;

4. 正式恢复:先备份当前状态!

这是最重要的一步:恢复操作本身是有风险的,执行前必须先把当前数据库再备份一次(哪怕它有问题),避免恢复失败后连原来的数据都没了。

mysqldump -uroot -p --single-transaction --all-databases > /tmp/before_restore.sql

风险提示:导入备份会覆盖现有数据。如果备份文件里有 DROP TABLE 或 CREATE DATABASE 语句(用 --databases 导出时就包含 CREATE DATABASE 和 USE,不含 DROP),会直接影响目标库。恢复前务必确认目标实例是对的,最好先在测试实例演练一遍。

5. 大文件导入加速:

SET GLOBAL foreign_key_checks = 0;
SET GLOBAL unique_checks = 0;
SET GLOBAL sql_log_bin = 0;      -- 不把恢复过程写进 binlog

导入完成后记得改回来。sql_log_bin = 0 在从库上要特别谨慎,它会导致从库数据与主库不一致。

单表恢复

如果只需要恢复一张表,可以从全量备份里抽取:

# 用 sed 抽取单表的结构和数据(表结构在 CREATE TABLE 到下一个 CREATE 之间)
gunzip -c backup.sql.gz | sed -n '/^-- Table structure for table `user`/,/^-- Table structure for table `order`/p' > /tmp/user.sql

更可靠的方式是用 mysqldump 备份时就做分表导出,或者用 mydumper(天然按表分文件)。

定期演练

建议每季度至少做一次完整恢复演练,并记录:

  • 恢复耗时(这就是你的 RTO 实测值)
  • 遇到的问题(缺文件、权限、磁盘空间、版本不兼容)
  • 数据校验结果

演练可以在独立的测试环境做,不影响生产。不做演练的备份等于没有备份——这句话听起来夸张,但它是无数事故的总结。

七、大库用 xtrabackup 做物理备份

mysqldump 是逻辑备份,导出和导入都要做格式转换,大数据量时非常慢。经验上:

  • 数据量小于 10G:mysqldump 完全够用,简单可靠
  • 10G 到 100G:两者都可以,mysqldump 恢复可能要几十分钟到几小时
  • 超过 100G:建议 xtrabackup(Percona XtraBackup),备份和恢复都快得多

安装

# RHEL/CentOS/Rocky
sudo dnf install -y percona-xtrabackup-80    # 版本号按 MySQL 版本选

# Debian/Ubuntu
sudo apt-get install -y percona-xtrabackup-80

版本必须匹配:MySQL 5.7 用 xtrabackup 2.4,MySQL 8.0 用 8.0 版本。版本不匹配会报各种奇怪的错误。

全量备份

xtrabackup --backup \
  --user=backup_user --password=xxx \
  --target-dir=/data/backup/xtrabackup/full_$(date +%Y%m%d)

增量备份

xtrabackup --backup \
  --user=backup_user --password=xxx \
  --target-dir=/data/backup/xtrabackup/inc_$(date +%Y%m%d%H) \
  --incremental-basedir=/data/backup/xtrabackup/full_20261010

恢复

# 1. 准备(应用 redo log,让数据文件一致)
xtrabackup --prepare --target-dir=/data/backup/xtrabackup/full_20261010

# 增量要先准备全量再加 --apply-log-only,然后依次应用增量
xtrabackup --prepare --apply-log-only --target-dir=/data/backup/xtrabackup/full_20261010
xtrabackup --prepare --apply-log-only --target-dir=/data/backup/xtrabackup/full_20261010 \
  --incremental-dir=/data/backup/xtrabackup/inc_2026101012

# 2. 停止 MySQL,替换数据目录
sudo systemctl stop mysqld
sudo mv /var/lib/mysql /var/lib/mysql_old     # 保留旧数据以便回滚
sudo xtrabackup --copy-back --target-dir=/data/backup/xtrabackup/full_20261010
sudo chown -R mysql:mysql /var/lib/mysql
sudo systemctl start mysqld

风险提示:--copy-back 要求目标数据目录为空。上面的流程用 mv 保留了旧目录(这是回滚手段),不要直接 rm -rf。确认新库正常后再清理旧目录。

xtrabackup 的优缺点

优点:备份快(直接拷文件)、恢复快、支持增量、备份期间不阻塞写入(虽然仍有短暂的 FTWRL 锁)。

缺点:产出的文件大(等于数据量)、不能跨版本恢复、不能只恢复单表(8.0 部分版本支持单表导出)、操作比 mysqldump 复杂、与 MySQL 版本强绑定。

八、binlog 增量与时间点恢复

开启 binlog

[mysqld]
server-id        = 1
log_bin          = /data/mysql-bin/mysql-bin
binlog_format    = ROW
expire_logs_days = 7            # MySQL 8.0 用 binlog_expire_logs_seconds
max_binlog_size  = 1G
sync_binlog      = 1
SHOW VARIABLES LIKE 'log_bin';
SHOW BINARY LOGS;

binlog_format = ROW 是推荐设置,它记录行的前后镜像,恢复和数据同步都比 STATEMENT 更可靠。

时间点恢复的场景

典型场景:有人误执行了 DELETE FROM orders WHERE ...,没有 WHERE 的 UPDATE,或者 DROP 了表。需要恢复到误操作之前的那一刻。

步骤:

1. 立即停止写入(保护现场)

这一步极其重要:新的写入会与恢复冲突,也会覆盖你想要找回的数据。 具体做法是:把应用切到维护模式、停掉应用服务、或者对表加锁。

-- 紧急情况下先锁表(只锁需要的表)
LOCK TABLES orders WRITE;

2. 找到误操作的时间点

如果有监控或应用日志,先确定大致时间。然后用 mysqlbinlog 查看:

mysqlbinlog --start-datetime='2026-10-10 09:00:00' \
            --stop-datetime='2026-10-10 10:00:00' \
            --base64-output=DECODE-ROWS -v \
            /data/mysql-bin/mysql-bin.000123 | less

-v 参数把 ROW 格式的 binlog 解码成可读的伪 SQL,方便定位。

3. 用最近一次全量备份恢复到基础状态

gunzip < /data/backup/mysql/20261010/mydb_020000.sql.gz | mysql -uroot -p

4. 重放 binlog 到误操作前

mysqlbinlog --start-datetime='2026-10-10 02:00:00' \
            --stop-datetime='2026-10-10 09:35:00' \
            /data/mysql-bin/mysql-bin.000122 /data/mysql-bin/mysql-bin.000123 \
  | mysql -uroot -p

--stop-datetime 就是误操作发生的时刻。多个 binlog 文件要按顺序、一次性传给 mysqlbinlog(不要分开执行,否则事务边界会出错)。

5. 导出误删的那部分数据,再导回生产库

这是关键的最佳实践:不要在恢复出来的实例上直接对外服务,而是把误删的数据单独导出,再导入生产库。 这样能避免"恢复到 09:35"导致 09:35 之后所有正常写入丢失。

mysqldump -uroot -p --where="id BETWEEN 1000 AND 2000" mydb orders > /tmp/orders_restore.sql
mysql -uroot -p mydb < /tmp/orders_restore.sql

单表的时间点恢复限制

基于全量备份的时间点恢复,最小粒度是整个备份集。 也就是说,你没法只恢复一张表到某个时间点——除非你的备份本身就是按表做的(用 mydumper 或者在备份脚本里分表导出)。

这也是为什么推荐分库、甚至分表备份:误删单表时,只恢复那一张表的代价远低于恢复整个库。

常见问题

备份文件应该放在哪里?

绝对不要和数据库放在同一块盘上。 理由有两个:盘坏了备份和数据一起没;备份写满磁盘会导致数据库无法写入甚至崩溃(这是真实的常见事故)。正确做法是单独挂载一块盘,或者放到另一台机器/对象存储。

为什么 crontab 里脚本不执行?

最常见的三个原因:环境变量 PATH 太短找不到命令(手动跑成功、cron 失败就是这个);脚本没有执行权限(chmod +x);脚本里的相对路径在 cron 的工作目录下不成立(用绝对路径)。检查 /var/log/cron 或 /var/log/syslog 看有没有执行记录,并把脚本的输出重定向到日志文件看具体报错。

备份会锁表吗?

用 --single-transaction 且全是 InnoDB 表时,不会阻塞读写。但如果库里有 MyISAM 表、或者备份期间有人执行 DDL,就可能出现锁等待甚至备份失败。建议把 DDL 操作安排在非备份窗口,并在备份脚本里记录失败原因。

备份文件权限要注意什么?

备份文件包含数据库的全部数据,权限必须收紧:文件 600、目录 700,属主 root 或专门的备份用户。不要放在 Web 目录下(否则可能被直接下载),也不要给其他用户读权限。另外传输时用 scp/rsync over SSH 或 HTTPS,不要用明文协议。

多久备份一次合适?

看你能接受丢多少数据(RPO)。每天一次全量 + binlog 是最常见的配置,RPO 可以做到分钟级甚至秒级。数据变更非常频繁且 RPO 要求高的,可以每 6 小时一次全量,或者用 xtrabackup 做"周日全量 + 每日增量"。

怎么确认备份真的可用?

唯一可靠的方法是恢复演练:定期(比如每季度)把备份文件恢复到一个测试实例,跑一遍数据校验(行数、最大 ID、关键业务查询)。只检查"文件大小非 0"是完全不够的——一个截断的文件也可能有几百 MB。

备份占用太多磁盘怎么办?

几个办法:用 gzip 或 zstd 压缩(SQL 文本通常能压到 1/5 到 1/10);缩短本地保留周期,把长期归档放对象存储(成本低得多);开启 binlog 后可以减少全量备份频率;定期归档冷数据以缩小备份体积。

主从架构下在哪里做备份?

在从库上做备份,完全不影响主库性能。步骤是:先在从库停复制(STOP SLAVE),记录当前的 binlog 位点,做备份,再启动复制(START SLAVE)。这样拿到的备份是一致性的,且能对应到明确的位点。

总结

  • 备份方案由 RPO 和 RTO 倒推:每天全量 + binlog 保留 7 天是通用配置,能做到分钟级 RPO;RTO 要求高或数据量超过 100G 时用 xtrabackup 物理备份,恢复速度快一个量级。遵循 3-2-1 原则(3 份副本、2 种介质、1 份异地)。
  • mysqldump 必带 --single-transaction --routines --triggers --events,GTID 实例的普通备份要加 --set-gtid-purged=OFF(搭建从库才用 ON)。它对 InnoDB 有效且不锁表,但备份期间不能有 DDL,且库里不能有 MyISAM 表。
  • 脚本要包含校验和错误处理:gzip -t 验证压缩文件完整性、md5sum 留档、失败时返回非 0 退出码便于告警。凭据不要写在命令行(会被 ps 和历史记录看到),用权限 600 的 ~/.my.cnf 或 mysql_config_editor。
  • crontab 最常见的坑是环境变量:手动跑成功、定时任务失败,八成是 PATH 问题。在 crontab 里显式设置 PATH 或在脚本里用绝对路径,并把输出重定向到日志文件。放进 cron 之前一定先手动跑通。
  • 备份绝不能放在数据库同一块盘上:盘坏了备份一起没,备份写满磁盘还会导致数据库崩溃。异地用 rsync(注意 --delete 先 --dry-run 验证)或对象存储(走内网端点省流量费)。敏感数据异地存储要加密,且密钥不要和备份放一起。
  • 恢复前先备份当前状态,并优先在测试实例演练。时间点恢复的标准流程是:停止写入保护现场 → 用 mysqlbinlog -v 定位误操作时刻 → 恢复全量备份 → 重放 binlog 到误操作前 → 只导出误删的那部分数据再导回生产,避免丢失误操作之后的正常写入。
  • 不做演练的备份等于没有备份:每季度做一次完整恢复演练并实测 RTO。保留周期(本地 7 天 + 异地 30 天)要用脚本自动执行,清理用的 rm -rf 先去掉 -exec 用 -print 验证一遍再正式启用。