说实话,以前我有个客户,老张,是个数据库管理员。他每天早上一到公司,第一件事就是登录服务器,手动敲命令备份数据。有一次,他因为堵车迟到了半小时,结果那半小时里正好有人误删了一张表,虽然恢复了,但差点让他把头发都愁白。从那天起,他就发誓:再也不搞人工备份了。
后来,他花了一个星期写了几个Shell脚本,配上了定时任务,还加了完善的异常处理。现在?他每天多睡半小时,周末还能去钓鱼。今天我就把老张这套“保命秘籍”拆解开来,手把手教你怎么做,保证你看完也能写出同样靠谱的备份系统。
一、为什么非要自动化?别嫌我啰嗦
你可能会想:“手动备份也就几分钟,折腾脚本麻烦不?” 咱们算笔账。
假设你每天备份需要10分钟,加上检查备份是否成功、清理旧文件,总共15分钟。一年365天,那就是91.25小时,差不多11个工作日!而且,人是会犯错的。你记得检查过吗?磁盘空间够吗?密码对不对?半夜三点闹钟响了吗?这些细节一旦漏掉,数据丢了就是大事。
自动化脚本的优势就体现在这里:
- 省心:设定好后,全自动运行。
- 可靠:脚本不会忘记检查,不会手抖。
- 可追溯:日志清清楚楚,出了什么问题一眼就能看出来。
- 灵活:想改时间、改路径,改配置文件就行,不用动核心代码。
老张现在每天省下的2小时,其实是“隐性成本”——他不用提心吊胆地担心数据丢失,不用半夜爬起来恢复数据。这种安心,是钱买不来的。
二、环境准备:先看看你家里有什么
在写代码之前,咱们得先摸清家底。不同公司的环境不一样,咱们以最常见的 Linux (CentOS/Ubuntu) + MySQL/MariaDB 或 PostgreSQL 为例。如果你的数据库是Oracle或SQL Server,思路差不多,但命令会有区别。
1. 检查备份工具
首先,确保你的系统里有 mysqldump(MySQL/MariaDB)或 pg_dump(PostgreSQL)。打开终端,敲一下:
which mysqldump
which pg_dump
如果返回路径,比如 /usr/bin/mysqldump,那就没问题。如果没有,得先装:
# CentOS/RHEL
sudo yum install -y mysql-server mariadb-server
# 或者
sudo yum install -y postgresql-server postgresql-contrib
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y mysql-server mariadb-server
# 或者
sudo apt-get install -y postgresql postgresql-contrib
2. 规划备份目录
别把备份文件随便扔在 /tmp 里,那玩意儿重启就没了。咱们建一个专门的目录:
sudo mkdir -p /backup/mysql
sudo mkdir -p /backup/logs
sudo chown -R $(whoami):$(whoami) /backup
这里我用了当前用户,你也可以根据权限需要调整。
3. 创建备份专用账户(安全第一)
别用 root 账户直接备份!这是大忌。咱们创建一个权限最小的备份用户。
对于MySQL:
CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'StrongPassword123!';
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES ON *.* TO 'backup_user'@'localhost';
FLUSH PRIVILEGES;
对于PostgreSQL:
CREATE USER backup_user WITH PASSWORD 'StrongPassword123!';
GRANT CONNECT ON DATABASE your_db_name TO backup_user;
-- PostgreSQL的权限比较复杂,按需授予SELECT等权限
注意:密码一定要强,而且别硬编码在脚本里!咱们后面会讲怎么用配置文件存密码。
三、核心脚本:写出能跑的备份代码
现在,咱们进入重头戏。我会分步骤写脚本,每一步都有详细解释,保证你看得懂、跑得通。
第一步:基础备份脚本(单库备份)
先写一个最简单的,能把一个库备份出来。新建文件 backup_db.sh:
#!/bin/bash
# ==================== 配置区域 ====================
# 数据库类型:mysql 或 postgresql
DB_TYPE="mysql"
# 数据库地址、端口、用户名
DB_HOST="localhost"
DB_PORT="3306"
DB_USER="backup_user"
DB_PASS="StrongPassword123!"
# 要备份的数据库名(多个库用空格分隔,如 "db1 db2 db3")
DB_NAMES="my_website_db"
# 备份存储目录
BACKUP_DIR="/backup/mysql"
LOG_DIR="/backup/logs"
DATE_FORMAT="%Y%m%d_%H%M%S"
# 备份文件保留天数(超过这个天数的备份会被自动删除)
RETENTION_DAYS=30
# ==================== 函数定义 ====================
# 记录日志
log() {
local message="$1"
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$timestamp] $message" | tee -a "${LOG_DIR}/backup_$(date +%Y%m%d).log"
}
# 发送告警通知(可选,比如发邮件或钉钉)
send_alert() {
local subject="$1"
local body="$2"
log "ALERT: $subject - $body"
# 这里可以加邮件发送命令,比如:
# echo "$body" | mail -s "$subject" admin@example.com
# 或者调用钉钉/企微机器人接口
}
# 清理过期备份
cleanup_old_backups() {
log "开始清理 ${RETENTION_DAYS} 天前的备份..."
find "${BACKUP_DIR}" -type f -name "*.sql.gz" -mtime +${RETENTION_DAYS} -delete
log "清理完成。"
}
# ==================== 主逻辑 ====================
main() {
log "========== 开始数据库备份 =========="
# 确保目录存在
mkdir -p "${BACKUP_DIR}" "${LOG_DIR}"
# 遍历每个数据库
for DB_NAME in ${DB_NAMES}; do
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE_FORMAT}.sql.gz"
log "正在备份数据库: ${DB_NAME}"
if [ "${DB_TYPE}" = "mysql" ]; then
# MySQL备份命令
mysqldump -h "${DB_HOST}" \
-P "${DB_PORT}" \
-u "${DB_USER}" \
-p"${DB_PASS}" \
--single-transaction \
--routines \
--triggers \
--events \
"${DB_NAME}" | gzip > "${BACKUP_FILE}"
elif [ "${DB_TYPE}" = "postgresql" ]; then
# PostgreSQL备份命令
export PGPASSWORD="${DB_PASS}"
pg_dump -h "${DB_HOST}" \
-p "${DB_PORT}" \
-U "${DB_USER}" \
-Fc \
-f "${BACKUP_FILE%.gz}" \
"${DB_NAME}"
gzip "${BACKUP_FILE%.gz}"
unset PGPASSWORD
else
log "错误:不支持的数据库类型 ${DB_TYPE}"
send_alert "备份失败" "不支持的数据库类型: ${DB_TYPE}"
exit 1
fi
# 检查备份是否成功
if [ $? -eq 0 ]; then
log "数据库 ${DB_NAME} 备份成功: ${BACKUP_FILE}"
# 可以加一个校验,比如计算文件大小,防止空文件
if [ ! -s "${BACKUP_FILE}" ]; then
log "警告:备份文件为空!"
send_alert "备份异常" "数据库 ${DB_NAME} 备份文件为空"
fi
else
log "错误:数据库 ${DB_NAME} 备份失败!"
send_alert "备份失败" "数据库 ${DB_NAME} 备份出错,请检查日志"
exit 1
fi
done
# 清理旧备份
cleanup_old_backups
log "========== 数据库备份完成 =========="
}
# 执行主函数
main
代码解读:
--single-transaction是MySQL备份的关键参数,它保证备份期间数据一致性,而不会锁表(除非用--lock-all-tables)。gzip压缩备份文件,节省空间。tee -a把日志同时输出到屏幕和文件。find ... -mtime +${RETENTION_DAYS} -delete自动清理过期文件,不用你管。
第二步:异常处理——让脚本“聪明”起来
上面的脚本已经能跑了,但还不够“结实”。咱们加上更完善的异常处理。
1. 捕获更细粒度的错误
刚才的脚本用 $? 判断,但其实可以改进。比如,mysqldump 可能部分成功,但密码错误会直接退出。咱们可以加一个“检查退出码”的逻辑:
# 改进后的备份部分
if [ "${DB_TYPE}" = "mysql" ]; then
# 使用更严格的错误检查
set -o pipefail # 让管道命令如果任何一步失败,整个管道返回非零
mysqldump -h "${DB_HOST}" \
-P "${DB_PORT}" \
-u "${DB_USER}" \
-p"${DB_PASS}" \
--single-transaction \
--routines \
--triggers \
--events \
"${DB_NAME}" | gzip > "${BACKUP_FILE}"
DUMP_STATUS=$?
if [ ${DUMP_STATUS} -ne 0 ]; then
log "错误:mysqldump 返回码 ${DUMP_STATUS},备份失败"
send_alert "备份失败" "mysqldump 错误码: ${DUMP_STATUS}, 数据库: ${DB_NAME}"
exit 1
fi
else
# PostgreSQL 类似处理
fi
2. 监控磁盘空间
备份到一半,磁盘满了怎么办?脚本会失败,但你可能不知道。咱们加一个磁盘检查:
# 在 main 函数开头加
check_disk_space() {
local dir="$1"
local min_space_mb=1024 # 至少需要1GB空间
local used_percent=$(df -h "${dir}" | awk 'NR==2 {print $5}' | sed 's/%//')
local available_mb=$(df -m "${dir}" | awk 'NR==2 {print $4}')
if [ "${available_mb}" -lt "${min_space_mb}" ]; then
log "警告:磁盘空间不足!剩余 ${available_mb}MB,需要至少 ${min_space_mb}MB"
send_alert "磁盘空间警告" "备份目录 ${dir} 剩余空间不足: ${available_mb}MB"
# 可以选择退出,或者只备份最近的几个库
exit 1
fi
}
# 在 main 里调用
check_disk_space "${BACKUP_DIR}"
3. 数据库连接检查
备份前先看看数据库连不连得上:
check_db_connection() {
if [ "${DB_TYPE}" = "mysql" ]; then
if ! mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" -e "SELECT 1" > /dev/null 2>&1; then
log "错误:无法连接到 MySQL 数据库"
send_alert "连接失败" "无法连接到 MySQL 数据库 ${DB_HOST}:${DB_PORT}"
exit 1
fi
elif [ "${DB_TYPE}" = "postgresql" ]; then
if ! psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAMES}" -c "SELECT 1" > /dev/null 2>&1; then
log "错误:无法连接到 PostgreSQL 数据库"
send_alert "连接失败" "无法连接到 PostgreSQL 数据库 ${DB_HOST}:${DB_PORT}"
exit 1
fi
fi
}
第三步:隐藏密码——别硬编码在脚本里
把密码写在脚本里,谁都能看见,太危险了!咱们用配置文件来存密码。
1. 创建配置文件
新建 /etc/backup/db_backup.conf(或者你的用户目录下):
# MySQL 配置
DB_TYPE=mysql
DB_HOST=localhost
DB_PORT=3306
DB_USER=backup_user
DB_PASS=StrongPassword123!
DB_NAMES=my_website_db db2 db3
BACKUP_DIR=/backup/mysql
LOG_DIR=/backup/logs
RETENTION_DAYS=30
2. 修改脚本读取配置
# 在脚本开头加载配置
CONFIG_FILE="/etc/backup/db_backup.conf"
if [ ! -f "${CONFIG_FILE}" ]; then
echo "错误:配置文件 ${CONFIG_FILE} 不存在"
exit 1
fi
# 安全地读取配置(避免eval执行危险命令)
while IFS='=' read -r key value; do
# 跳过注释和空行
[[ "${key}" =~ ^#.*$ ]] && continue
[[ -z "${key}" ]] && continue
# 去除首尾空格
key=$(echo "${key}" | xargs)
value=$(echo "${value}" | xargs)
# 导出为环境变量或局部变量
declare "${key}=${value}"
done < "${CONFIG_FILE}"
# 检查必要配置
if [ -z "${DB_PASS}" ]; then
echo "错误:配置文件中缺少 DB_PASS"
exit 1
fi
3. 限制配置文件权限
sudo chmod 600 /etc/backup/db_backup.conf
sudo chown root:root /etc/backup/db_backup.conf
这样只有root能读,脚本用root权限运行时才能读取。如果你用普通用户运行,可以把配置文件放到用户目录下,权限设为600。
四、定时任务:让备份“自动”跑起来
脚本写好了,怎么让它每天自动跑?答案就是 crontab。
1. 编辑 crontab
crontab -e
2. 添加定时任务
假设你想每天晚上2点执行备份:
# 每天凌晨2点执行备份
0 2 * * * /path/to/backup_db.sh >> /backup/logs/cron.log 2>&1
注意:
>> /backup/logs/cron.log 2>&1把crontab的输出也记到日志里,方便排查问题。- 最好用绝对路径,比如
/usr/bin/bash /path/to/backup_db.sh,避免环境变量问题。
3. 测试定时任务
别直接等第二天,先手动运行一次,确保一切正常:
# 手动运行脚本
bash /path/to/backup_db.sh
# 检查日志
tail -f /backup/logs/backup_$(date +%Y%m%d).log
如果没有问题,再交给crontab。
4. 设置多个备份策略(可选)
老张还有更精细的策略:
- 每天:全量备份,保留30天。
- 每周日:一次特别完整的全量备份,保留90天。
- 每小时:增量备份(如果数据库支持,比如MySQL Binlog)。
你可以写多个脚本,或者在一个脚本里根据日期判断执行不同策略。
五、监控与告警——别等出了问题才知道
备份了不等于成功,得有人知道备份是否真的OK。
1. 邮件告警
修改 send_alert 函数,加上邮件发送:
send_alert() {
local subject="$1"
local body="$2"
log "ALERT: $subject - $body"
# 发送电子邮件
echo "$body" | mail -s "$subject" admin@example.com
}
确保服务器能发邮件(配置SMTP,或者用 sendmail)。
2. 钉钉/企微机器人(更现代)
send_alert() {
local subject="$1"
local body="$2"
log "ALERT: $subject - $body"
# 钉钉机器人Webhook
curl -s 'https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text", "text": {"content": "数据库备份告警: ${subject}\n${body}"}}'
}
把 YOUR_TOKEN 换成你钉钉机器人的真实token。
3. 备份健康检查
除了告警,还可以做一个简单的健康检查页面或API,定期探测备份文件是否存在、大小是否正常。
六、实战案例:老张的完整流程
老张最后是怎么用的?
- 脚本位置:
/home/zhangsan/scripts/backup_db.sh - 配置文件:
/home/zhangsan/config/db_backup.conf - crontab:
0 2 * * * /home/zhangsan/scripts/backup_db.sh >> /backup/logs/cron.log 2>&1 - 监控:备份成功后,钉钉机器人发一条“✅ 数据库备份成功”;失败则发“❌ 数据库备份失败,请立即检查”。
现在,老张每天早上收到一条钉钉消息,看看有没有异常。如果没有,他就安心开始一天的工作。
七、常见坑点与避坑指南
坑1:时区问题
服务器上时间和北京时间不一致,导致备份文件名时间错乱。 解决:在脚本开头统一时区。
export TZ='Asia/Shanghai'
坑2:中文路径/文件名
如果数据库
