MySQL 备份及恢复脚本
作为开发,我们都知道数据备份的重要性,而数据备份最重要的就是数据库备份,前一段时间由于操作失误,误删过一次数据库,所以特把备份和恢复脚本分享出来,作为笔记。
1. MySQL 备份脚本
#!/bin/bash # =================================================================== # MySQL 分库全量备份脚本(生产级 | 自适应 --source-data / --master-data) # 功能: # - 自动发现用户数据库 # - 每库独立压缩备份 # - 自动选择 --source-data (8.0+) 或 --master-data (5.7) # - 智能处理 GTID(仅在启用时设置) # - 提取 binlog 位置生成 .info 文件 # - 清理 N 天前旧备份 # 作者:BridgeLi # 版本:1.0 # =================================================================== # ------------------------------- # 配置区 # ------------------------------- BACKUP_DIR="/project/backup/mysql/dbs" CNF_FILE="/project/backup/mysql/my.cnf" RETENTION_DAYS=7 MIN_FREE_SPACE_GB=5 HOSTNAME=$(hostname -s) DT=$(date +%Y-%m-%d_%H%M%S) # 排除系统库 EXCLUDED_DBS="^(mysql|sys|information_schema|performance_schema)$" # ------------------------------- # 初始化 & 依赖检查 # ------------------------------- for cmd in mysql mysqldump gzip gunzip df date awk sed; do command -v "$cmd" >/dev/null || { echo "错误:缺少命令 '$cmd'"; exit 1; } done mkdir -p "$BACKUP_DIR" || { echo "错误:无法创建目录 $BACKUP_DIR"; exit 1; } LOG_FILE="$BACKUP_DIR/backup.log" LOCK_FILE="$BACKUP_DIR/.backup.lock" if [ -f "$LOCK_FILE" ]; then echo "[$(date +'%Y-%m-%d %H:%M:%S')] 错误:锁文件存在,可能已有备份在运行。" exit 1 fi trap "rm -f '$LOCK_FILE'" EXIT touch "$LOCK_FILE" AVAILABLE_GB=$(df -P "$BACKUP_DIR" | tail -1 | awk '{print int($4/1024/1024)}') if [ "$AVAILABLE_GB" -lt "$MIN_FREE_SPACE_GB" ]; then echo "[$(date +'%Y-%m-%d %H:%M:%S')] 错误:磁盘空间不足 (${AVAILABLE_GB}GB < ${MIN_FREE_SPACE_GB}GB)" exit 1 fi exec > >(tee -a "$LOG_FILE") 2>&1 echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$HOSTNAME] 开始全量分库备份..." # ------------------------------- # 检测 MySQL 版本和 GTID 状态 # ------------------------------- MYSQL_CMD="mysql --defaults-extra-file=$CNF_FILE -sN" # 获取 MySQL 主版本(57, 80) MYSQL_VERSION=$($MYSQL_CMD -e "SELECT REPLACE(LEFT(VERSION(), 4), '.', '');") if ! [[ "$MYSQL_VERSION" =~ ^[0-9]+$ ]]; then echo "[$(date +'%Y-%m-%d %H:%M:%S')] 错误:无法获取 MySQL 版本" exit 1 fi # 选择 source-data / master-data if [ "$MYSQL_VERSION" -ge 80 ]; then REPLICATION_OPT="--source-data=2" else REPLICATION_OPT="--master-data=2" fi # 检查 GTID 是否启用 GTID_MODE=$($MYSQL_CMD -e "SELECT @@GLOBAL.gtid_mode;" 2>/dev/null || echo "OFF") if [[ "$GTID_MODE" =~ ^(ON|ON_PERMISSIVE|OFF_PERMISSIVE)$ ]]; then GTID_PURGED_OPT="--set-gtid-purged=ON" GTID_ENABLED=true else GTID_PURGED_OPT="--set-gtid-purged=OFF" GTID_ENABLED=false fi echo "[$(date +'%Y-%m-%d %H:%M:%S')] MySQL 版本: $MYSQL_VERSION, 使用: $REPLICATION_OPT, GTID: ${GTID_MODE:-OFF}" # ------------------------------- # 获取所有非系统数据库 # ------------------------------- dbs=() while IFS= read -r db; do [[ -z "$db" ]] && continue if [[ ! "$db" =~ $EXCLUDED_DBS ]]; then dbs+=("$db") fi done < <($MYSQL_CMD -e "SHOW DATABASES;" 2>>"$LOG_FILE") if [ ${#dbs[@]} -eq 0 ]; then echo "[$(date +'%Y-%m-%d %H:%M:%S')] 警告:未找到可备份的数据库。" exit 0 fi echo "[$(date +'%Y-%m-%d %H:%M:%S')] 发现 ${#dbs[@]} 个数据库: ${dbs[*]}" # ------------------------------- # 执行备份 # ------------------------------- SUCCESS_COUNT=0 FAILURE_COUNT=0 START_TIME=$(date +%s) for db in "${dbs[@]}"; do DUMP_FILE="${db}-${DT}.sql.gz" DUMP_PATH="$BACKUP_DIR/$DUMP_FILE" INFO_PATH="${DUMP_PATH%.gz}.info" echo "[$(date +'%Y-%m-%d %H:%M:%S')] 正在备份: $db -> $DUMP_PATH" mysqldump --defaults-extra-file="$CNF_FILE" \ --single-transaction \ --routines \ --triggers \ $REPLICATION_OPT \ $GTID_PURGED_OPT \ --databases "$db" 2>>"$LOG_FILE" | gzip -c > "$DUMP_PATH" if [ $? -eq 0 ] && [ -s "$DUMP_PATH" ] && gunzip -t "$DUMP_PATH" >/dev/null 2>&1; then # 提取 binlog 位置(source/master 兼容) read master_file master_pos < <(gzip -dc "$DUMP_PATH" | sed -n "/^-- CHANGE MASTER TO / s/.*LOG_FILE='\\([^']*\\)',.*LOG_POS=\\([0-9]*\\).*/\\1 \\2/p" | head -1) # 提取 GTID(仅当启用) if [ "$GTID_ENABLED" = true ]; then gtid_purged=$(gzip -dc "$DUMP_PATH" | sed -n "s/^SET @@GLOBAL.GTID_PURGED='\\([^']*\)';\$/\\1/p" | head -1) [ -z "$gtid_purged" ] && gtid_purged="NONE" else gtid_purged="DISABLED" fi # 写入 .info { [ -n "$master_file" ] && echo "File: $master_file" [ -n "$master_pos" ] && echo "Position: $master_pos" echo "GTID: $gtid_purged" } > "$INFO_PATH" chmod 600 "$DUMP_PATH" "$INFO_PATH" 2>/dev/null || true echo "[$(date +'%Y-%m-%d %H:%M:%S')] 成功: $db (binlog: $master_file, pos: $master_pos, gtid: $gtid_purged)" ((SUCCESS_COUNT++)) else echo "[$(date +'%Y-%m-%d %H:%M:%S')] 失败: $db" rm -f "$DUMP_PATH" "$INFO_PATH" ((FAILURE_COUNT++)) fi done # ------------------------------- # 清理 N 天前的旧备份 # ------------------------------- if [ $FAILURE_COUNT -eq 0 ]; then cleanup_old() { local pattern="$1" local now_ts=$(date +%s) local files=() mapfile -t files < <(find "$BACKUP_DIR" -name "$pattern" -type f 2>/dev/null) for file in "${files[@]}"; do if [[ "$file" =~ -([0-9]{4})-([0-9]{2})-([0-9]{2})_([0-9]{2})([0-9]{2})([0-9]{2})\. ]]; then # 提取各部分:年、月、日、时、分、秒 local y=${BASH_REMATCH[1]} local m=${BASH_REMATCH[2]} local d=${BASH_REMATCH[3]} local H=${BASH_REMATCH[4]} local M=${BASH_REMATCH[5]} local S=${BASH_REMATCH[6]} # 构造合法日期字符串:2025-09-25 02:00:01 local datetime="$y-$m-$d $H:$M:$S" # 转换为时间戳 local file_ts=$(date -d "$datetime" +%s 2>/dev/null) || continue local age_days=$(( (now_ts - file_ts) / 86400 )) if [ $age_days -ge $RETENTION_DAYS ]; then echo "[$(date +'%Y-%m-%d %H:%M:%S')] 清理过期文件: $file (已存在 $age_days 天)" rm -f "$file" fi fi done } cleanup_old "*.sql.gz" cleanup_old "*.info" END_TIME=$(date +%s) echo "[$(date +'%Y-%m-%d %H:%M:%S')] 备份完成: 成功 $SUCCESS_COUNT,失败 $FAILURE_COUNT,耗时 $((END_TIME - START_TIME)) 秒" else echo "[$(date +'%Y-%m-%d %H:%M:%S')] 警告:有 $FAILURE_COUNT 个数据库备份失败,跳过清理。" exit 1 fi exit 0
2. MySQL binlog 备份脚本:
#!/bin/bash # =================================================================== # MySQL Binlog 增量备份脚本(优化版) # 配合全量备份实现 PITR # 作者:BridgeLi # 版本:1.0 # =================================================================== # 配置参数 BINLOG_DIR="/var/lib/mysql" BACKUP_DIR="/project/backup/mysql/binlogs" CNF_FILE="/project/backup/mysql/my.cnf" LOG_FILE="$BACKUP_DIR/binlog_backup.log" LOCK_FILE="$BACKUP_DIR/.backup.lock" LAST_COPIED_FILE="$BACKUP_DIR/.last_binlog" # 创建备份目录 mkdir -p "$BACKUP_DIR" || { echo "[$(date)] 错误:无法创建备份目录 $BACKUP_DIR" >&2; exit 1; } # 使用 flock 防止并发执行 exec 200>"$LOCK_FILE" if ! flock -n 200; then echo "[$(date)] 错误:备份脚本已在运行,退出。" | tee -a "$LOG_FILE" exit 1 fi # 重定向所有输出到日志 exec >> "$LOG_FILE" 2>&1 echo "==================================" echo "[$(date +'%Y-%m-%d %H:%M:%S')] 开始 binlog 增量备份..." # 获取当前活跃 binlog CURRENT_LOG=$(mysql --defaults-extra-file="$CNF_FILE" -sN -e "SHOW MASTER STATUS;" 2>/dev/null | awk '{print $1}') if [ -z "$CURRENT_LOG" ]; then echo "[$(date +'%Y-%m-%d %H:%M:%S')] 错误:无法获取当前 binlog 名称,请检查 MySQL 连接或权限。" exit 1 fi echo "当前活跃 binlog: $CURRENT_LOG" # 读取上次备份的 binlog if [[ -f "$LAST_COPIED_FILE" ]]; then LAST_LOG=$(cat "$LAST_COPIED_FILE") echo "上次已备份至: $LAST_LOG" else LAST_LOG="" echo "首次运行,将备份所有历史 binlog(除当前外)。" fi COPIED=0 cd "$BINLOG_DIR" || { echo "无法进入 binlog 目录: $BINLOG_DIR"; exit 1; } # 获取所有 binlog 文件并按版本排序 mapfile -t LOGS < <(find . -maxdepth 1 -name 'mysql-bin.*' -type f -printf '%f\n' | sort -V) for log in "${LOGS[@]}"; do [[ ! -f "$log" ]] && continue # 跳过当前活跃的 binlog [[ "$log" == "$CURRENT_LOG" ]] && continue # 判断是否需要备份:log > LAST_LOG(版本排序) if [[ -n "$LAST_LOG" ]]; then # 使用 sort -V 判断顺序 greater=$(printf '%s\n%s' "$LAST_LOG" "$log" | sort -V | tail -1) if [[ "$greater" != "$log" || "$log" == "$LAST_LOG" ]]; then continue fi fi # 执行压缩备份 if gzip -c "$log" > "$BACKUP_DIR/${log}.gz"; then echo "[$(date +'%Y-%m-%d %H:%M:%S')] 已压缩备份: $log" ((COPIED++)) else echo "[$(date +'%Y-%m-%d %H:%M:%S')] 错误:备份失败 $log" fi done # 原子更新最后备份的 binlog echo "$CURRENT_LOG" > "${LAST_COPIED_FILE}.tmp" && mv "${LAST_COPIED_FILE}.tmp" "$LAST_COPIED_FILE" echo "[$(date +'%Y-%m-%d %H:%M:%S')] binlog 增量备份完成,共复制 $COPIED 个文件。" echo "=================================="
3. MySQL 健康检查脚本(非必需,只需要备份就行)
#!/bin/bash # =================================================================== # MySQL 备份健康检查脚本(优化生产版 - 已修复) # 功能:检查最近备份时效、磁盘使用率,记录日志,发送告警,输出 Prometheus 指标 # 作者:BridgeLi # 版本:1.0 # =================================================================== # ------------------------------- # 配置参数 # ------------------------------- BACKUP_DIR="/project/backup/mysql/dbs" LOG_DIR="/var/log/mysql" LOG_FILE="$LOG_DIR/health_check.log" ALERT_EMAIL="admin@example.com" HOSTNAME=$(hostname -s) # Prometheus 指标输出路径 PROM_FILE="/tmp/backup_health.prom" PROM_TMP_FILE="/tmp/backup_health.prom.tmp" # 告警阈值(小时) MAX_BACKUP_AGE_HOURS=26 DISK_WARN_THRESHOLD=80 DISK_CRIT_THRESHOLD=90 # ------------------------------- # 创建日志目录(避免 tee 报错) # ------------------------------- if [ ! -d "$LOG_DIR" ]; then mkdir -p "$LOG_DIR" && chmod 755 "$LOG_DIR" [ $? -ne 0 ] && echo "ERROR: Cannot create log directory $LOG_DIR" && exit 1 fi # ------------------------------- # 重定向输出:同时输出到日志和终端 # ------------------------------- exec > >(tee -a "$LOG_FILE") 2>&1 # ------------------------------- # 检查最近一次备份是否在合理时间内 # 输出:状态信息 # 返回值: # 0 = OK # 1 = CRITICAL(超时) # 2 = ERROR(无备份) # ------------------------------- check_last_backup() { local latest_entry=$(find "$BACKUP_DIR" -name "*.sql.gz" -type f -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1) if [ -z "$latest_entry" ]; then echo "ERROR no_recent_backup" return 2 fi # 提取时间戳(取整数部分) local mtime_epoch=$(echo "$latest_entry" | awk '{split($1,a,"."); print a[1]}') local now_epoch=$(date +%s) local age_seconds=$((now_epoch - mtime_epoch)) local age_hours=$((age_seconds / 3600)) if [ $age_hours -gt $MAX_BACKUP_AGE_HOURS ]; then echo "CRITICAL backup_too_old $age_hours hours" return 1 else echo "OK last_backup $age_hours hours ago" return 0 fi } # ------------------------------- # 检查备份目录所在磁盘使用率 # 返回值: # 0 = OK (<80%) # 1 = CRITICAL (>90%) # 2 = WARN (80%~90%) # 输出:状态信息 # ------------------------------- check_disk_usage() { if [ ! -d "$BACKUP_DIR" ]; then echo "ERROR backup_dir_not_found: $BACKUP_DIR" return 1 fi local df_out=$(df -P "$BACKUP_DIR" 2>/dev/null) if [ -z "$df_out" ]; then echo "ERROR disk_check_failed" return 1 fi local used_percent=$(echo "$df_out" | tail -1 | awk '{print $5}' | tr -d '%') if ! [[ "$used_percent" =~ ^[0-9]+$ ]]; then echo "ERROR disk_usage_invalid: $used_percent" return 1 fi if [ $used_percent -gt $DISK_CRIT_THRESHOLD ]; then echo "CRITICAL disk_usage ${used_percent}%" return 1 elif [ $used_percent -gt $DISK_WARN_THRESHOLD ]; then echo "WARN disk_usage ${used_percent}%" return 2 else echo "OK disk_usage ${used_percent}%" return 0 fi } # ------------------------------- # 发送告警邮件 # ------------------------------- send_alert() { local subject="$1" local body="$2" if command -v mail >/dev/null 2>&1; then echo -e "$body" | mail -s "$subject" "$ALERT_EMAIL" echo "Alert sent to $ALERT_EMAIL" else echo "WARNING: 'mail' command not available. Skipping alert." logger "MySQL Backup Alert: $subject | $body" fi } # ------------------------------- # 主逻辑开始 # ------------------------------- echo "=== Backup Health Check ($(date)) on $HOSTNAME ===" # 执行检查,捕获输出和返回值(只执行一次!) output1=$(check_last_backup) res1=$? echo "$output1" output2=$(check_disk_usage) res2=$? echo "$output2" # 判断是否需要告警 alert_needed=false if [ $res1 -eq 1 ] || [ $res1 -eq 2 ] || [ $res2 -eq 1 ]; then alert_needed=true fi # 发送告警 if [ "$alert_needed" = true ]; then subject="⚠️ MySQL 备份异常 - $HOSTNAME" body="【备份状态】$output1\n【磁盘状态】$output2\n\n请立即检查备份目录:$BACKUP_DIR" send_alert "$subject" "$body" else echo "All checks OK." fi # ------------------------------- # 输出 Prometheus 指标(原子写入) # ------------------------------- ( echo "# HELP mysql_backup_last_success_age_hours Age of last successful backup in hours, -1 if none" echo "# TYPE mysql_backup_last_success_age_hours gauge" local latest_entry=$(find "$BACKUP_DIR" -name "*.sql.gz" -type f -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1) if [ -z "$latest_entry" ]; then echo "mysql_backup_last_success_age_hours -1" else local mtime_epoch=$(echo "$latest_entry" | awk '{split($1,a,"."); print a[1]}') local now=$(date +%s) local age_hours=$(( (now - mtime_epoch) / 3600 )) echo "mysql_backup_last_success_age_hours $age_hours" fi echo "" echo "# HELP mysql_backup_disk_usage_percent Disk usage of the backup partition (%)" echo "# TYPE mysql_backup_disk_usage_percent gauge" df -P "$BACKUP_DIR" 2>/dev/null | tail -1 | awk '{gsub(/%/,"",$5); print "mysql_backup_disk_usage_percent", $5}' ) > "$PROM_TMP_FILE" && mv "$PROM_TMP_FILE" "$PROM_FILE" if [ $? -eq 0 ]; then echo "Prometheus metrics written to $PROM_FILE" else echo "ERROR: Failed to write Prometheus metrics" fi echo "=== Check completed ==="
4. MySQL 恢复脚本
#!/bin/bash # =================================================================== # MySQL Point-in-Time Recovery (PITR) 脚本 # 功能:基于全量备份 + binlog 恢复到指定时间点 # 作者:BridgeLi # 版本:1.0 # # 用法: # ./mysql-pitr-restore.sh "2025-09-24 10:00:00" [数据库名] # ./mysql-pitr-restore.sh --dry-run "2025-09-24 10:00:00" [db_name] # ./mysql-pitr-restore.sh --help # # 依赖: # mysql, mysqlbinlog, gzip, find, sort # =================================================================== set -euo pipefail # 严格模式:出错即退出 # ---------------------------- # 默认配置 # ---------------------------- BACKUP_DIR="/project/backup/mysql/dbs" BINLOG_DIR="/project/backup/mysql/binlogs" RESTORE_DIR="/tmp/mysql_restore_$$" # 使用 PID 避免冲突 CNF_FILE="/project/backup/mysql/my.cnf" LOG_FILE="$BACKUP_DIR/restore.log" DRY_RUN=0 DEBUG=0 TARGET_TIME="" TARGET_DB="" # ---------------------------- # 函数定义 # ---------------------------- usage() { cat << 'EOF' 用法: ./mysql-pitr-restore.sh [选项] <目标时间 'YYYY-MM-DD HH:MM:SS'> [数据库名] 选项: --dry-run 模拟执行,不真正恢复数据 --debug 启用调试输出 --help 显示此帮助信息 示例: ./mysql-pitr-restore.sh "2025-09-24 10:00:00" ./mysql-pitr-restore.sh --dry-run "2025-09-24 10:00:00" mydb ./mysql-pitr-restore.sh --debug "2025-09-24 12:30:00" 注意: - 全量备份文件需包含 CHANGE MASTER TO 语句以提取 binlog 位置 - binlog 文件需为 .gz 压缩格式,命名如 mysql-bin.000001.gz - 恢复前请确保数据库无写入操作! EOF } log() { local level="${1}" shift echo "[$(date '+%F %T')] [$level] $*" | tee -a "$LOG_FILE" } debug() { [[ $DEBUG -eq 1 ]] && log "DEBUG" "$@" } cleanup() { if [[ -d "$RESTORE_DIR" ]]; then debug "正在清理临时目录: $RESTORE_DIR" rm -rf "$RESTORE_DIR" fi } trap cleanup EXIT confirm_proceed() { log "WARN" "即将开始恢复至时间点: $TARGET_TIME" if [[ -n "$TARGET_DB" ]]; then log "INFO" "仅恢复数据库: $TARGET_DB" fi log "WARN" "请确保 MySQL 当前无写入操作,否则可能导致数据不一致!" read -p "确定继续?[y/N]: " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log "INFO" "用户取消操作" exit 1 fi } find_latest_full_backup() { local latest_file="" local latest_time="" local file time_str backup_time for file in "$BACKUP_DIR"/*.sql{,.gz}; do [[ -f "$file" ]] || continue # 提取时间戳:匹配 -YYYY-MM-DD_HHMMSS.sql 或 .sql.gz if [[ "$file" =~ -([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6})\.sql(\.gz)?$ ]]; then time_str="${BASH_REMATCH[1]}" backup_time="${time_str:0:10} ${time_str:11:2}:${time_str:13:2}:${time_str:15:2}" debug "发现备份: $file -> 时间: $backup_time" if [[ "$backup_time" < "$TARGET_TIME" ]]; then if [[ -z "$latest_time" || "$backup_time" > "$latest_time" ]]; then latest_file="$file" latest_time="$backup_time" fi fi else debug "跳过不匹配的文件: $file" fi done if [[ -z "$latest_file" ]]; then log "ERROR" "未找到早于 $TARGET_TIME 的全量备份" return 1 fi echo "$latest_file|$latest_time" } extract_binlog_position() { local backup_file="$1" local content_cmd="gzip -dc" # 默认是 .gz [[ "$backup_file" == *.sql ]] && content_cmd="cat" local line line=$(eval "$content_cmd" "$backup_file" | sed -n "s/.*CHANGE MASTER TO MASTER_LOG_FILE='\([^']*\)',.*, MASTER_LOG_POS=\([0-9]*\).*/\1 \2/p" | head -1) if [[ -z "$line" ]]; then log "ERROR" "无法从备份中提取 binlog 位置信息,请检查是否启用 --master-data=2" return 1 fi echo "$line" } # ---------------------------- # 参数解析 # ---------------------------- while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=1 shift ;; --debug) DEBUG=1 shift ;; --help) usage exit 0 ;; -*) log "ERROR" "未知选项: $1" usage exit 1 ;; *) break ;; esac done if [[ $# -lt 1 ]]; then log "ERROR" "缺少目标时间参数" usage exit 1 fi TARGET_TIME="$1" if [[ ! "$TARGET_TIME" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}$ ]]; then log "ERROR" "时间格式无效,应为 'YYYY-MM-DD HH:MM:SS'" exit 1 fi TARGET_DB="${2:-}" # 设置日志输出 exec > >(tee -a "$LOG_FILE") 2>&1 # ---------------------------- # 主流程 # ---------------------------- log "INFO" "开始 PITR 恢复流程,目标时间: $TARGET_TIME" if (( DRY_RUN )); then log "DRYRUN" "运行在模拟模式 (--dry-run),不会执行实际恢复" fi # 1. 查找最接近的全量备份 log "INFO" "正在查找最接近且早于 $TARGET_TIME 的全量备份..." result=$(find_latest_full_backup) if [[ $? -ne 0 ]]; then exit 1 fi FULL_BACKUP=$(echo "$result" | cut -d'|' -f1) FULL_TIME=$(echo "$result" | cut -d'|' -f2) log "INFO" "使用全量备份: $FULL_BACKUP (时间: $FULL_TIME)" # 2. 用户确认 if (( !DRY_RUN )); then confirm_proceed fi # 3. 恢复全量数据 log "INFO" "开始导入全量数据..." if (( DRY_RUN )); then log "DRYRUN" "将导入: $FULL_BACKUP" else mkdir -p "$RESTORE_DIR" if [[ "$FULL_BACKUP" == *.gz ]]; then if gzip -dc "$FULL_BACKUP" | mysql --defaults-extra-file="$CNF_FILE"; then log "INFO" "全量恢复成功" else log "ERROR" "全量导入失败" exit 1 fi else if mysql --defaults-extra-file="$CNF_FILE" < "$FULL_BACKUP"; then log "INFO" "全量恢复成功" else log "ERROR" "全量导入失败" exit 1 fi fi fi # 4. 提取 binlog 起始位置 log "INFO" "提取 binlog 起始位置..." position_line=$(extract_binlog_position "$FULL_BACKUP") if [[ $? -ne 0 ]]; then exit 1 fi read -r START_FILE START_POS <<< "$position_line" log "INFO" "从 binlog 开始应用: $START_FILE, 位置: $START_POS" # 5. 应用 binlog 到目标时间 log "INFO" "开始应用 binlog 增量日志..." applied=0 for binlog_gz in $(find "$BINLOG_DIR" -name "*.gz" | sort); do local binlog_base binlog_base=$(basename "$binlog_gz" .gz) # 跳过早于起始文件的日志 if [[ "$binlog_base" < "$START_FILE" ]]; then continue fi log "INFO" "处理 binlog: $binlog_base" # 解压到临时目录 mkdir -p "$RESTORE_DIR/binlogs" local tmp_binlog="$RESTORE_DIR/binlogs/$binlog_base" if (( DRY_RUN )); then log "DRYRUN" "将解压并应用: $binlog_gz -> $tmp_binlog" applied=1 continue fi gzip -dc "$binlog_gz" > "$tmp_binlog" # 构建 mysqlbinlog 命令 local mysqlbinlog_cmd=( mysqlbinlog --start-position="$START_POS" --stop-datetime="$TARGET_TIME" "${TARGET_DB:+--database=$TARGET_DB}" "$tmp_binlog" ) local mysql_cmd=(mysql --defaults-extra-file="$CNF_FILE") debug "执行命令: ${mysqlbinlog_cmd[*]} | ${mysql_cmd[*]}" if "${mysqlbinlog_cmd[@]}" | "${mysql_cmd[@]}"; then log "INFO" "成功应用 binlog: $binlog_base" applied=1 else local ret=$? log "INFO" "完成 binlog 应用(可能已到达目标时间或中断),返回码: $ret" applied=1 break # 关键:不再处理后续 binlog fi rm -f "$tmp_binlog" START_POS=4 # 下一个文件从事件头后开始 done # 6. 结果汇报 if (( applied == 0 )); then log "WARN" "未应用任何 binlog,请检查 binlog 是否存在、时间范围是否合理" fi if (( DRY_RUN )); then log "DRYRUN" "模拟执行结束。真实恢复请移除 --dry-run 参数。" else log "INFO" "恢复完成:已恢复至 $TARGET_TIME" log "INFO" "请立即验证数据一致性,并检查关键业务逻辑。" fi exit 0
5. my.cnf 文件(600 权限)
[client] user=用户名 password=密码 host=localhost port=3306
6. crontab 表达式
# 每天 2:00 全量备份 0 2 * * * /project/backup/mysql/backup_per_db.sh # 每小时 0 分 增量 binlog 0 * * * * /project/backup/mysql/backup_binlog.sh # 每天 3:10 健康检查 10 3 * * * /project/backup/mysql/check_backup_health.sh
7. 如果报:bash: ./backup_per_db.sh: /bin/bash^M: 坏的解释器: 没有那个文件或目录,解决方案:
使用 vim 编辑器 vim backup_per_db.sh 在命令模式下执行: :set fileformat=unix :wq
全文完,如果本文对您有所帮助,请花 1 秒钟帮忙点击一下广告,谢谢。
作 者: BridgeLi,https://www.bridgeli.cn
原文链接:http://www.bridgeli.cn/archives/800
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
作 者: BridgeLi,https://www.bridgeli.cn
原文链接:http://www.bridgeli.cn/archives/800
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
近期评论