☰
MySQL定时备份实战:用mysqldump和crontab实现自动化备份
2026/10/1 3:56:32 网站建设 项目流程

1. 备份方案怎么选:逻辑备份还是物理备份

先说结论:绝大多数业务场景,用 mysqldump 做逻辑备份 + crontab 定时执行,就是最稳妥、最不容易出错、也最容易上手恢复的方案。别一上来就想着上 xtrabackup,没必要。

在聊定时备份之前,得先回答一个问题:你备份到底是为了防什么?

  • 防的是误操作,比如 DELETE 忘加 WHERE、DROP TABLE 手滑、UPDATE 覆盖了整张表——这种情况 90% 靠逻辑备份就能救回来。
  • 防的是磁盘损坏、机房断电、服务器被格式化——这种情况逻辑备份救不回来,需要物理备份 + Binlog 配合。
  • 防的是版本迁移、换服务器、环境重建——逻辑备份完胜,因为它是纯 SQL 文本,跨版本、跨平台都能导入。

所以备份方案的取舍逻辑其实很简单:

数据量在 50GB 以内、单库不超过 20GB,优先逻辑备份(mysqldump)。数据量大了,mysqldump 慢、恢复也慢,再考虑 Percona XtraBackup 这种物理备份工具,或者做基于 Binlog 的增量备份。

备份类型对比表:

对比项逻辑备份(mysqldump)物理备份(xtrabackup)冷备份(停库拷贝)
备份内容SQL 语句文本数据文件物理拷贝数据文件拷贝
备份速度慢,逐条导出快,文件级别最快
恢复速度慢,逐条执行快最快
对业务影响低,可在线备份低,可在线备份必须停机
跨版本迁移支持良好需同版本或兼容一般
误操作恢复方便较麻烦必须全量回退
适用场景中小数据量、日常备份大库、企业级备份极端场景

再啰嗦一句:冷备份(停库后直接 cp 数据目录)虽然简单粗暴,但你得停 MySQL,线上环境根本不现实。日常定时备份就老老实实用 mysqldump。

另外,备份策略上,我强烈建议你搞一个三层组合:

  1. 每日全量备份:每天凌晨用 mysqldump 导出全库,压缩后保留 7 天或 30 天。
  2. Binlog 实时记录:开启 MySQL 的 Binlog,用来做时间点恢复(比如今天中午误删了,拿昨天凌晨的全量 + 今天凌晨到现在 Binlog 可以恢复)。
  3. 每周异地/离线备份:把备份文件同步一份到另一台机器、对象存储或者移动硬盘,防止服务器本身一起挂掉。

这三层里,第 1 层是本文的主角,第 2、3 层我后面也会展开讲怎么配合。

2. 搭建备份环境:账号、目录、权限一个都不能少

在写脚本之前,先把基础环境准备好。很多人上来就写 crontab,结果第二天发现备份文件是 0 字节,一查是权限不对或者密码没配好,这种坑我踩得太多。

2.1 创建专用备份账号

我强烈建议别用 root 账号做备份,特别是 root 被 MySQL 8.0 默认禁止远程连接之后,你会莫名踩坑。建一个最小权限的账号,只给它 SELECT、LOCK TABLES、SHOW VIEW、EVENT、TRIGGER 相关权限就够。

CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'StrongPassword2024!'; GRANT SELECT, RELOAD, LOCK TABLES, SHOW VIEW, TRIGGER, EVENT ON *.* TO 'backup_user'@'localhost'; FLUSH PRIVILEGES;

其中RELOAD权限是给--single-transaction用 FLUSH TABLES WITH READ LOCK 时用的,EVENT 和 TRIGGER是备份存储过程、定时器、触发器必需的,没有这两个权限,导出来的备份会默认不带这些对象,恢复后你会发现存储过程全丢了。

2.2 准备备份目录

目录规划直接决定后面脚本的简洁程度。我的习惯是:

/data/backup/mysql/ ├── daily/ # 每日全量备份 │ ├── 2024-06-01/ │ ├── 2024-06-02/ │ └── ... ├── weekly/ # 每周归档备份(可选) └── logs/ # 备份日志

创建命令:

mkdir -p /data/backup/mysql/{daily,weekly,logs} chown -R mysql:mysql /data/backup/mysql

把备份目录属主改成 mysql 用户,防止脚本以 mysql 身份运行(后面我会讲为什么要用 mysql 用户跑 cron)时没有写权限。

2.3 配置免密登录

脚本要放到 crontab 里自动跑,总不能每次都在脚本里写明文密码,更不能手动输入密码。很多人会直接在脚本里写-p'密码',但这样一来,用ps aux查看进程时密码就暴露了,同时密码也会写进 shell 历史记录里。

正确做法是配置 MySQL 的~/.my.cnf文件,自动读取登录凭据。

sudo vi /root/.my.cnf

内容如下:

[client] host=localhost user=backup_user password=StrongPassword2024!

然后设置文件权限:

chmod 600 /root/.my.cnf

这是关键一步!.my.cnf权限必须是 600,如果权限过宽(比如 644),MySQL 会直接忽略这个文件并报错Warning: World-writable config file is ignored,接下来你会百思不得其解为什么 mysqldump 还是要密码。

配置好后测试一下:

mysql -e "SELECT 1;"

能正常返回 1 就说明免密登录生效了。

提示:如果你的备份用户和 crontab 执行用户不是 root,记得把.my.cnf放到对应用户的家目录下,比如 mysql 用户就放/var/lib/mysql/.my.cnf,并且chown给 mysql 用户。

3. mysqldump 核心参数拆解:每个参数都是一条血泪经验

mysqldump 参数很多,但真正生产环境天天用的就那几个。我给你逐个解释清楚,免得你照着网上老旧的教程复制粘贴,备份出来不能用。

3.1 基础导出命令

一条比较完备的全库导出命令长这样:

mysqldump \ --single-transaction \ --quick \ --routines \ --triggers \ --events \ --hex-blob \ --set-gtid-purged=OFF \ --default-character-set=utf8mb4 \ --databases your_database > /data/backup/mysql/daily/$(date +%F)/your_database_$(date +%F).sql

逐项拆解一下:

  • --single-transaction:这是 InnoDB 表的核心参数。它基于事务机制,导出一致性快照,不会锁表。没有这个参数,导出的过程中其他会话的写入操作会导致数据不一致。
  • --quick:逐行读取并立即输出,防止一次性把所有数据加载到内存导致内存暴涨。
  • --routines --triggers --events:备份存储过程、触发器、事件调度器。这三个默认情况下是不备份的,很多人恢复后才发现业务逻辑丢了,欲哭无泪。
  • --hex-blob:二进制字段(BLOB、BINARY)以十六进制形式导出,防止直接输出乱码导致恢复失败。
  • --set-gtid-purged=OFF:MySQL 5.7 及以后,开启 GTID 的实例导出时,如果不加这个参数,备份文件会带上SET @@GLOBAL.GTID_PURGED语句,恢复时容易报错GTID_PURGED can only be set when GTID_EXECUTED is empty。
  • --default-character-set=utf8mb4:MySQL 8.0 默认就是 utf8mb4,但老库可能一直是 latin1 或 utf8,导出时指定字符集,恢复时就不会出现中文乱码。
  • --databases your_database:带上这个参数,导出文件里会包含CREATE DATABASE和USE your_database语句,恢复时不用手动选库。

3.2 MyISAM 表的重大区别

如果你库里有 MyISAM 表,注意!--single-transaction对 MyISAM 表完全不生效。InnoDB 靠事务实现一致性快照,但 MyISAM 不支持事务,mysqldump 会退回使用--lock-tables参数,也就是锁表。

锁表的后果是:备份期间这张表不可写,业务会短暂卡顿。所以我的建议是:

  • 如果全库都是 InnoDB,放心用--single-transaction。
  • 如果确实有 MyISAM 表,评估数据量,要么改成 InnoDB(ALTER TABLE ... ENGINE=InnoDB),要么接受备份期间的短暂锁表。

3.3 压缩:默认必须做

SQL 文本文件的压缩率相当可观,通常能压到原来的 1/5 到 1/10。我习惯用 gzip,压缩率不错,而且所有 Linux 发行版默认都有。

mysqldump [参数] your_database | gzip > /data/backup/mysql/daily/2024-06-01/your_database_2024-06-01.sql.gz

注意:用了管道之后,mysqldump 的退出状态码会被 gzip 覆盖,所以后面写脚本判断成败时,需要借助PIPESTATUS数组(Bash)或者set -o pipefail,否则脚本可能漏报备份失败。

3.4 单表备份命令

有时候只想备份某张关键表,比如订单表、用户表,命令是:

mysqldump --single-transaction --hex-blob your_database your_table | gzip > table_2024-06-01.sql.gz

4. 完整备份脚本:拿来即用

环境准备就绪后,核心就是写备份脚本了。这里我直接给你一个亲测稳定跑了两年的完整脚本,注释都已加好。

#!/bin/bash #========================================= # MySQL 每日全量备份脚本 # 文件:/usr/local/bin/mysql_backup.sh # 说明:备份所有指定库,压缩后按日期归档,保留 N 天 #========================================= set -o pipefail # ---------- 配置区 ---------- MYSQL_BIN="/usr/bin/mysqldump" BACKUP_DIR="/data/backup/mysql/daily" LOG_DIR="/data/backup/mysql/logs" RETENTION_DAYS=7 DATABASES=("your_database" "another_database") # 要备份的库,可多个 MYSQL_USER="backup_user" MYSQL_HOST="localhost" # ---------------------------- TIMESTAMP=$(date +%Y%m%d_%H%M%S) DATE_STR=$(date +%F) TODAY_DIR="${BACKUP_DIR}/${DATE_STR}" LOG_FILE="${LOG_DIR}/backup_${DATE_STR}.log" # 日志函数 log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*" >> "${LOG_FILE}" } log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >> "${LOG_FILE}" } # 创建今日备份目录 if [ ! -d "${TODAY_DIR}" ]; then mkdir -p "${TODAY_DIR}" fi log_info "备份任务开始" # 检查 mysqldump 是否可用 if [ ! -x "${MYSQL_BIN}" ]; then log_error "找不到 mysqldump,请检查 MYSQL_BIN 路径" exit 1 fi # 对每个库执行备份 for db in "${DATABASES[@]}"; do BACKUP_FILE="${TODAY_DIR}/${db}_${TIMESTAMP}.sql.gz" log_info "开始备份数据库:${db}" ${MYSQL_BIN} \ --single-transaction \ --quick \ --routines \ --triggers \ --events \ --hex-blob \ --set-gtid-purged=OFF \ --default-character-set=utf8mb4 \ --databases "${db}" 2>>"${LOG_FILE}" | gzip > "${BACKUP_FILE}" if [ ${PIPESTATUS[0]} -eq 0 ] && [ -s "${BACKUP_FILE}" ]; then log_info "数据库 ${db} 备份成功,文件大小:$(du -h "${BACKUP_FILE}" | cut -f1)" else log_error "数据库 ${db} 备份失败,请检查!" # 如果有告警机器人,在这里调用 curl 通知 fi done # 清理超过保留天数的备份目录 find "${BACKUP_DIR}" -maxdepth 1 -type d -name "20*" -mtime +${RETENTION_DAYS} -exec rm -rf {} \; log_info "已清理 ${RETENTION_DAYS} 天前的旧备份目录" log_info "备份任务结束" exit 0

把脚本保存到/usr/local/bin/mysql_backup.sh,然后赋予执行权限:

chmod +x /usr/local/bin/mysql_backup.sh

关于脚本的几个设计细节,值得说道说道:

第一,set -o pipefail很关键。前面说了,mysqldump 走管道给 gzip 后,shell 默认只看最后一个命令(gzip)的退出状态。加了set -o pipefail,只要管道里任何一个命令失败,整个管道的退出状态就是失败。否则你会遇到 mysqldump 中途报错,但脚本依然认为备份成功的情况。

第二,脚本里用了日志函数,每步都写日志。在生产环境里,没有日志的备份脚本等于没写。出问题时你可以直接看今天日志的最后几行,快速定位失败原因。

第三,我用find ... -mtime +${RETENTION_DAYS} -exec rm -rf来清理过期目录,而不是简单粗暴全删。这样即使某天备份失败导致当天目录没生成,也不会误删其他天的数据。

5. 配置 crontab 定时任务:真正实现“无人值守”

脚本写好了,手动跑一次确认没问题,然后配置定时任务。

5.1 crontab 基础语法

Linux 的 crontab 语法格式是五段式:

分 时 日 月 周 命令

比如:

30 2 * * * /usr/local/bin/mysql_backup.sh >> /data/backup/mysql/logs/cron.log 2>&1

这段的意思是:每天凌晨 2 点 30 分执行备份脚本。

为什么选凌晨 2 点 30 分?一般业务低峰期在凌晨 2 点到 4 点之间,2 点半是个比较合适的窗口。而且最好避开整点,因为可能多个定时任务都挤在整点跑,会造成资源争抢。

5.2 编辑 crontab

crontab -e

如果你是 root 用户,默认编辑的是 root 的定时任务列表。如果想让备份脚本以专用用户身份运行(更安全、权限更可控),可以用:

crontab -u mysql -e

但注意:如果用 mysql 用户跑 crontab,脚本里所有目录的写权限、.my.cnf的位置都要对应调整,建议还是直接用 root 跑,然后脚本里做好权限控制。

5.3 验证 crontab 是否真的执行了

配置完之后,很多人不验证,第二天才发现根本没跑。这里有三个验证步骤:

  1. 查看 crontab 列表确认:

    crontab -l
  2. 查看系统 cron 服务的运行状态:

    systemctl status crond # CentOS/RHEL systemctl status cron # Debian/Ubuntu
  3. 等一天或者手动把系统时间调近测试(不建议生产环境改时间),更推荐的办法是临时缩短间隔,比如先配置成每分钟执行一次,确认能生成备份文件后,再把 crontab 改回凌晨执行。

有个经典坑是:crontab 里的环境变量和交互式 shell 完全不同。比如你在终端里 mysqldump 能跑通,但 crontab 里却报command not found,原因就是 cron 执行时 PATH 环境变量不包含/usr/local/bin等路径。

解决方法是:脚本里要么使用绝对路径(我上面的脚本就是),要么在脚本开头加上:

export PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/mysql/bin

6. 恢复演练:不演练的备份等于没备份

这是我在这篇文章里最想强调的一件事。定时备份跑了大半年,数据库真的出问题时,才发现备份文件是坏的或者恢复方式不对,这才是最痛苦的事。

所以我强烈建议,每完成一次备份方案的部署,就立刻做一次全量恢复演练。

6.1 恢复的基本操作

# 解压备份文件 gunzip -c /data/backup/mysql/daily/2024-06-01/your_database_2024-06-01_020001.sql.gz > /tmp/restore.sql # 恢复到 MySQL mysql -u root -p < /tmp/restore.sql

如果备份文件里带--databases参数,导出内容自带CREATE DATABASE和USE语句,直接执行这条就能完整恢复。如果没有带这个参数,你需要先手动建库:

mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS your_database DEFAULT CHARSET utf8mb4;" mysql -u root -p your_database < /tmp/restore.sql

6.2 恢复演练的正确流程

  1. 起一台临时 MySQL 实例(Docker 一台或云上开个测试机)。
  2. 把最近一份备份文件恢复进去。
  3. 随机抽查几张核心表的数据量、最新记录时间,跟生产库对比。
  4. 确认无异常后,销毁临时实例。

这个流程每月至少做一次。我见过太多团队,备份文件存在服务器上吃灰,直到真出事才发现 mysqldump 的版本不兼容、表结构不全、数据是半截的——定期演练是验证备份有效性的唯一手段。

6.3 时间点恢复(PITR):误删数据的终极防线

只有每日全量备份还不够,因为你只能恢复到昨天凌晨的状态,今天白天的数据更新会丢失。

要真正做到“精确到某一秒”的恢复,就需要 Binlog。MySQL 开启 Binlog 后,所有变更操作都会记录在二进制日志里。

# my.cnf [mysqld] server-id=1 log-bin=/var/lib/mysql/mysql-bin binlog_format=ROW expire_logs_days=7 max_binlog_size=256M

恢复流程是:

  1. 恢复到最近一次全量备份。
  2. 使用mysqlbinlog重放全量备份之后的 Binlog:
    mysqlbinlog --start-datetime="2024-06-01 02:30:00" --stop-datetime="2024-06-01 14:30:00" \ /var/lib/mysql/mysql-bin.000012 | mysql -u root -p

这样就做到了完整的时间点恢复。Binlog 是 MySQL 高可用和数据恢复的最后一道防线,日常备份必须和 Binlog 配合使用。

7. 常见问题与排查技巧实录

备份脚本跑了一阵子之后,你大概率会遇到下面这些问题。我把我踩过的坑按出现频率排个序,直接给你排查思路。

7.1 crontab 到点没执行

排查步骤:

# 1. 检查 cron 服务 systemctl status crond # 2. 检查 crontab 列表 crontab -l # 3. 检查 cron 日志(CentOS/RHEL) grep CRON /var/log/cron | tail -20 # 4. 检查脚本是否有执行权限 ls -l /usr/local/bin/mysql_backup.sh

最常见的三个原因:脚本没有可执行权限、crontab 里写的路径不对、cron 服务的 PATH 环境变量没包含 mysqldump 路径。

7.2 备份文件是 0 字节

这是最让人崩溃的情况,你以为是备份成功了,结果文件是空的。排查方向:

  1. 手动执行脚本,看有没有报错输出。
  2. 检查磁盘空间:df -h,空间满了会导致管道写入失败。
  3. 检查.my.cnf权限是不是 600,权限过宽 MySQL 会不认。
  4. 检查--single-transaction和LOCK TABLES是否冲突,特别是 MyISAM 表。

7.3 备份文件出现乱码

这个基本是字符集问题。备份时指定--default-character-set=utf8mb4,恢复时如果目标库默认字符集不是 utf8mb4,也容易出现乱码。

建议全链路统一:库表字符集设置成 utf8mb4,备份导出用--default-character-set=utf8mb4,恢复时 MySQL 客户端加--default-character-set=utf8mb4。

7.4 mysqldump 卡死或者太慢

大库用 mysqldump 确实慢,尤其是几千万行的表,导出可能要几十分钟。这时候考虑几个方案:

  1. 拆库分表备份:每天只备份变更频繁的表,全量备份放到周末做。
  2. 改用 XtraBackup:物理备份,速度快一个数量级。
  3. 从从库备份:在只读副本上执行 mysqldump,彻底不影响主库。

7.5 备份文件被磁盘撑爆

备份文件占空间是必然的,所以保留策略必须合理。我给的脚本是保留 7 天全量,大约 7 份备份。

如果你数据量大,考虑差异备份 + 全量备份组合:周日全量备份,周一到周六只备份当天变化的数据(通过 Binlog 或只导出关键表)。这样磁盘占用能从 7 份全量大幅下降到 1 份全量 + 6 份增量。

7.6 备份文件传不到异地

备份文件只在本地磁盘上是远远不够的。本地磁盘可能和数据库一起挂掉。我的建议是备份脚本里加一步同步操作,常见的手段:

  • rsync 到另一台服务器:
    rsync -avz /data/backup/mysql/daily/ user@remote-server:/data/backup/mysql/
  • 上传到对象存储(阿里云 OSS、腾讯云 COS、AWS S3),用官方 CLI 工具即可。
  • 如果只是个人用,tar 包发送到另一块硬盘或 NAS 也行。

我在脚本里通常这样加:

# 同步今日备份到异地服务器 rsync -avz --timeout=60 "${TODAY_DIR}" backup@192.168.1.100:/data/backup/mysql/daily/ >> "${LOG_FILE}" 2>&1 if [ $? -eq 0 ]; then log_info "异地同步成功" else log_error "异地同步失败,请检查网络或 rsync 配置" fi

这一步加上去,你的备份方案才算真正完整。

8. 补充两个实操中反复出现的细节

备份方案跑久了,还有两个细节值得单独拎出来讲,因为它们直接影响你备份出来的东西能不能用。

8.1 存储过程、触发器到底备份没备份

网上很多教程给的 mysqldump 命令只有简单的-u -p,不带--routines、--triggers、--events。如果你库里有定时任务、存储过程,这一漏,恢复出来的库就像被抽走了灵魂。

怎么验证备份文件里到底有没有?直接搜:

grep -i "PROCEDURE" your_database_20240601.sql.gz | head

或者解压后搜索:

gunzip -c your_database_20240601.sql.gz | grep -c "CREATE PROCEDURE"

如果返回 0,说明你的备份命令里没带--routines参数,赶紧加上。

8.2 二进制日志(Binlog)占空间怎么办

开启 Binlog 之后,磁盘占用会明显上升。但不要因为占空间就把它关掉——Binlog 是你最后的时间点恢复手段。

合理的做法是:

  • 设置expire_logs_days或 MySQL 8.0 的binlog_expire_logs_seconds,让 Binlog 自动清理。
  • 每天备份任务执行后,用PURGE BINARY LOGS BEFORE命令清理已经备份过的 Binlog。
  • 给/var/lib/mysql所在磁盘多留一些空间,Binlog 增长量大约是数据变更量的几倍是正常的。

9. 最后的实操心得

整套方案你自己动手走一遍下来,其实比我写的还要简单,因为核心就三步:写好 mysqldump 命令、放进脚本、配置 crontab。难的不是工具本身,而是你能不能长期坚持验证备份的有效性。

我个人的习惯是:手机日历上设一个每月的提醒,内容是“恢复演练日”。到了那天就花二十分钟,把最新的备份拉到一台测试机上恢复一次,查几条关键数据,然后清掉。坚持了两年,从来没有因为备份问题在事故中出过丑。

还有一个细节是,每次修改了备份脚本,都要立刻手动执行一遍,确认没有语法错误,再让它进 crontab。不要改完脚本就直接等凌晨自动执行,那样万一有错误,最快也要第二天才能发现,白白损失一天的备份。

最后再补一句:如果你刚接手一套老的备份方案,第一步不是优化它,而是先手动跑一次并把备份文件完整恢复一遍,确认现有方案是真实的、可用的,再来谈改进。连现有方案都没验证过就瞎优化,等于在沙滩上盖楼。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询