MySQL 自动备份实战:从 Shell 脚本到定时任务


1. 备份方案怎么选

备份的核心不是 “有没有备份”,而是 “能不能恢复、有没有演练过“。方案选择主要分两类:

表格

类型代表工具特点适用
逻辑备份mysqldump导出 SQL,跨版本 / 跨平台可移植,易读易改中小数据量(GB 级)
物理备份Percona XtraBackup、MySQL Enterprise Backup文件级复制,备份 / 恢复速度快大数据量、要求低 RTO

推荐策略:每日业务低峰做一次全量(mysqldump)+ 持续归档 binlog 做增量,实现任意时间点恢复(PITR),本地保留 N 天 + 异地 / 对象存储一份。

2. 准备好备份账号(最小权限)

不要用 root 跑备份,按最小权限建专用账号:

CREATE USER 'backup'@'localhost' IDENTIFIED BY 'StrongPass_2026';

GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER, PROCESS, RELOAD
      ON *.* TO 'backup'@'localhost';

GRANT REPLICATION CLIENT ON *.* TO 'backup'@'localhost';   -- 供 SHOW MASTER STATUS 使用
FLUSH PRIVILEGES;

权限作用说明:

  • SELECT:读取数据
  • LOCK TABLES:对 MyISAM 表保证备份一致性
  • SHOW VIEW / EVENT / TRIGGER:导出视图、事件、触发器定义
  • PROCESS / RELOAD:查看线程状态、执行 FLUSH(binlog 切换 / 一致性快照)
  • REPLICATION CLIENTSHOW MASTER STATUS 取当前 binlog 位置

3. 核心:Shell 自动备份脚本

3.1 设计要点

  1. 配置与逻辑分离:库名、目录、保留天数集中在脚本顶部
  2. 防并发锁:crontab 可能因上次任务卡死而重叠执行,必须有锁
  3. 管道退出码检测mysqldump | gzip 中要用 PIPESTATUS 判断 mysqldump 真实成败,防止 “压缩成功但导出失败” 的假成功
  4. 临时文件 + 原子改名:先写 .tmp,成功后再 mv,避免恢复时拿到写了一半的备份
  5. 完整日志:每次执行记录时间、库、大小、清理结果
  6. 自动保留策略:只留 N 天,避免磁盘被撑爆

3.2 完整脚本(可直接保存为 /usr/local/bin/mysql_backup.sh

#!/usr/bin/env bash
###############################################################################
# MySQL 全量逻辑备份: mysqldump + gzip + 保留策略 + 防并发 + 日志
# 适用: MySQL 5.7 / 8.0, 建议 bash 4+
###############################################################################

# ---------- 配置区 ----------
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_USER="backup"
DB_PASS="StrongPass_2026"
DB_SOCKET=""                                  # 指定则走 socket, 留空走 TCP
DATABASES=""                                  # 留空=全库; 多库用空格分隔, 如 "db1 db2"
EXTRA_ARGS="--single-transaction --quick --routines --triggers --events"
BACKUP_BASE="/data/backup/mysql"
LOG_FILE="/var/log/mysql_backup.log"
KEEP_DAYS=7                                   # 本地保留天数
LOCK_FILE="/run/mysql_backup.lock"

# ---------- 日志与锁 ----------
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}

if [ -f "$LOCK_FILE" ]; then
    log "ERROR 已有锁文件 $LOCK_FILE, 上次任务可能未完成, 本次跳过"
    exit 1
fi
: > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT

# ---------- 组装 mysqldump 参数 ----------
MYSQL_ARGS=(-h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS")
[ -n "$DB_SOCKET" ] && MYSQL_ARGS=(-S"$DB_SOCKET")

if [ -n "$DATABASES" ]; then
    read -ra DB_LIST <<< "$DATABASES"
    DUMP_OBJ=("${DB_LIST[@]}")
else
    DUMP_OBJ=("--all-databases")
fi

# ---------- 执行备份 ----------
STAMP="$(date +%Y%m%d_%H%M%S)"
BACKUP_DIR="$BACKUP_BASE/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

OUT_FILE="$BACKUP_DIR/${STAMP}_mysql_backup.sql.gz"
TMP_FILE="${OUT_FILE}.tmp"

log "开始备份: host=$DB_HOST db=${DUMP_OBJ[*]}"

/usr/bin/mysqldump "${MYSQL_ARGS[@]}" $EXTRA_ARGS "${DUMP_OBJ[@]}" \
    | /usr/bin/gzip -9 > "$TMP_FILE"

# 关键: 检查 mysqldump 退出码, 失败则丢弃临时文件
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
    rm -f "$TMP_FILE"
    log "ERROR mysqldump 执行失败, 请查看日志定位原因"
    exit 1
fi

# 二次校验: gzip 退出码 + 输出非空
if [ "${PIPESTATUS[1]}" -ne 0 ] || [ ! -s "$TMP_FILE" ]; then
    rm -f "$TMP_FILE"
    log "ERROR gzip 或写文件失败"
    exit 1
fi

mv "$TMP_FILE" "$OUT_FILE"     # 原子落盘
SIZE="$(du -h "$OUT_FILE" | cut -f1)"
log "OK 备份完成: $OUT_FILE ($SIZE)"

# ---------- 保留策略: 清理 N 天前的备份 ----------
DELETED="$(find "$BACKUP_BASE" -type f -name "*.sql.gz" -mtime +"$KEEP_DAYS" -delete -print)"
if [ -n "$DELETED" ]; then
    log "清理过期备份: $(echo "$DELETED" | wc -l) 个文件"
fi
find "$BACKUP_BASE" -type d -empty -delete 2>/dev/null || true

log "全部完成"
exit 0

3.3 核心细节解读

为什么必须用 PIPESTATUSmysqldump | gzip > file 这条管道只返回最后一个命令(gzip)的退出码。若 mysqldump 中途报错,gzip 仍可能正常收尾,导致你拿到一个 “完整但没有内容” 的假备份。PIPESTATUS[0] 才是 mysqldump 的真实退出码。

--single-transaction 的一致性:对 InnoDB 生效,利用 MVCC 一致性快照,备份过程不锁表、不阻塞写入。但如果库里还有 MyISAM 表,需要额外加 --lock-tables(会锁表)。生产建议全库 InnoDB,这也是为什么新表一律 InnoDB 很重要。

防并发锁的边界kill -9 强杀会导致锁文件残留,下次任务会误判。生产版可改为记录 PID:锁文件存在时读取 PID 用 kill -0 判断进程是否还活着,进程不存在则视为过期锁。文中版本作为基础用法足够,如需我可在脚本上再加一层。

4. 挂到 crontab

# 每天凌晨 2:00 执行(业务低峰)
0 2 * * * /usr/local/bin/mysql_backup.sh

两个生产要点:

  1. cron 的 PATH 极简(通常只有 /usr/bin:/bin),脚本内已用 /usr/bin/mysqldump/usr/bin/gzip 绝对路径规避了这个问题。
  2. 脚本自带日志,cron 的 stdout/stderr 可丢弃或接邮件监控:MAILTO=you@company.com

5. 增量备份与任意时间点恢复(binlog)

先确保开启了 binlog(my.cnf):

[mysqld]
server-id=1
log_bin=mysql-bin
binlog_format=ROW
binlog_expire_logs_seconds=604800   # 8.0 写法(保留7天)
# expire_logs_days=7                # 5.7 写法

用一个小时级定时任务把已写完的 binlog 归档走,全量备份 + binlog 归档合起来就是完整的恢复链:

#!/usr/bin/env bash
# binlog_backup.sh: 归档非当前正在写的 binlog
BINLOG_DIR="/var/lib/mysql"
BACKUP_DIR="/data/backup/mysql/binlog"
MYSQL_USER="backup"
MYSQL_PASS="StrongPass_2026"

mkdir -p "$BACKUP_DIR"

CURRENT=$(mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -N -e "SHOW MASTER STATUS;" 2>/dev/null | awk '{print $1}')

for f in "$BINLOG_DIR"/mysql-bin.*; do
    b="$(basename "$f")"
    [ "$b" = "$CURRENT" ] && break        # 正在写的文件不备份
    [ -f "$BACKUP_DIR/$b" ] && continue
    cp "$f" "$BACKUP_DIR/$b"
done

挂 cron:0 * * * * /usr/local/bin/binlog_backup.sh

恢复到误删数据的某个时间点

# 1) 先恢复最近一次全量
gunzip < /data/backup/mysql/20260826/20260826_020000_mysql_backup.sql.gz \
    | mysql -uroot -p

# 2) 再重放该全量之后的 binlog, 截止到误删前一秒
mysqlbinlog --start-datetime="2026-08-26 02:00:00" \
            --stop-datetime="2026-08-26 09:29:59" \
            /data/backup/mysql/binlog/mysql-bin.000012 \
    | mysql -uroot -p

6. 恢复演练(必须做)

备份之后一定要验证:

# 压缩包完整性
gzip -t /data/backup/mysql/20260826/*.sql.gz

# 恢复后校验表
mysqlcheck -uroot -p --all-databases

# 业务侧冒烟: 关键表 count / 最新一条记录对比
mysql -uroot -p -N -e "SELECT COUNT(*) FROM yourdb.orders;" 

7. 生产清单

  • 备份目录所在磁盘做空间监控(备份体积 = 库大小 × 压缩比,提前预留 2~3 倍余量)
  • 脚本退出码接入告警(cron 失败即通知)
  • 备份文件异地 / 对象存储一份,重要数据建议加密
  • 密码不要硬编码:可改用 my.cnf[client] 段 + --defaults-extra-file,避免凭据出现在进程列表
  • 大库(> 几十 GB)换用 Percona XtraBackup 物理备份,mysqldump 恢复时间会不可接受
  • 每季度至少做一次完整恢复演练

8. 常见坑速查

表格

现象原因解决
备份文件有但恢复后数据不全管道淹没了 mysqldump 失败退出码PIPESTATUS 判定
Unknown table 'COLUMN_STATISTICS'mysql 8.0 服务器被旧版本客户端 dumpmysqldump 加 --column-statistics=0
Access denied备份账号权限不足按第 2 节授权
cron 里执行报 command not foundcron PATH 太简脚本内用绝对路径
MyISAM 表备份时数据不一致--single-transaction 只对 InnoDB 有效表迁移到 InnoDB 或加 --lock-tables
GTID 环境恢复报错GTID_PURGED 冲突dump 时加 --set-gtid-purged=OFF,恢复前评估

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注