diff --git a/restore_full.sh b/restore_full.sh new file mode 100644 index 0000000..ffe5a1f --- /dev/null +++ b/restore_full.sh @@ -0,0 +1,451 @@ +#!/bin/bash +# ============================================================================ +# MySQL 8.0.24 全量备份恢复脚本 +# ============================================================================ +# 作者: AI Assistant +# 版本: 1.0.0 +# 说明: 从全量备份恢复 MySQL 数据库 +# +# 功能特点: +# - 支持恢复全部数据库或指定数据库 +# - 自动解压压缩备份 +# - 恢复前可选择性备份当前数据 +# - 支持验证恢复结果 +# +# 使用方法: +# ./restore_full.sh [选项] <备份目录> +# +# 选项: +# -d, --database 仅恢复指定数据库 +# -y, --yes 跳过确认提示 +# --no-backup 恢复前不备份当前数据 +# -h, --help 显示帮助信息 +# +# 警告: +# 恢复操作会覆盖现有数据,请确保已做好备份! +# ============================================================================ + +set -o pipefail + +# ---------------------------------------------------------------------------- +# 脚本路径和配置加载 +# ---------------------------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 加载配置文件 +if [[ ! -f "${SCRIPT_DIR}/config.sh" ]]; then + echo "[ERROR] 配置文件不存在: ${SCRIPT_DIR}/config.sh" + exit 1 +fi +source "${SCRIPT_DIR}/config.sh" + +# 加载公共函数库 +if [[ ! -f "${SCRIPT_DIR}/lib/common.sh" ]]; then + echo "[ERROR] 公共函数库不存在: ${SCRIPT_DIR}/lib/common.sh" + exit 1 +fi +source "${SCRIPT_DIR}/lib/common.sh" + +# ---------------------------------------------------------------------------- +# 全局变量 +# ---------------------------------------------------------------------------- +RESTORE_TIMESTAMP=$(date +"${TIMESTAMP_FORMAT}") +LOG_FILE="${LOG_DIR}/restore_full_${RESTORE_TIMESTAMP}.log" +LOCK_FILE="${BACKUP_ROOT_DIR}/.restore.lock" +BACKUP_PATH="" +SPECIFIED_DATABASE="" +SKIP_CONFIRM=false +NO_BACKUP_BEFORE_RESTORE=false + +# ---------------------------------------------------------------------------- +# 显示帮助信息 +# ---------------------------------------------------------------------------- +show_help() { + cat << EOF +MySQL 8.0.24 全量备份恢复脚本 + +使用方法: + $(basename "$0") [选项] <备份目录> + +选项: + -d, --database 仅恢复指定数据库 + -y, --yes 跳过确认提示 + --no-backup 恢复前不备份当前数据 + -h, --help 显示此帮助信息 + +示例: + $(basename "$0") /data/mysql_backup/full/mysql_backup_full_20231220_120000 + $(basename "$0") -d mydb /path/to/backup + $(basename "$0") -y --no-backup /path/to/backup + +警告: + 恢复操作会覆盖现有数据,请确保已做好备份! +EOF +} + +# ---------------------------------------------------------------------------- +# 解析命令行参数 +# ---------------------------------------------------------------------------- +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + -d|--database) + shift + if [[ -z "$1" ]]; then + die "选项 -d/--database 需要一个参数" + fi + SPECIFIED_DATABASE="$1" + shift + ;; + -y|--yes) + SKIP_CONFIRM=true + shift + ;; + --no-backup) + NO_BACKUP_BEFORE_RESTORE=true + shift + ;; + -h|--help) + show_help + exit 0 + ;; + -*) + die "未知选项: $1\n使用 --help 查看帮助" + ;; + *) + if [[ -z "$BACKUP_PATH" ]]; then + BACKUP_PATH="$1" + else + die "多余的参数: $1" + fi + shift + ;; + esac + done + + if [[ -z "$BACKUP_PATH" ]]; then + die "请指定备份目录\n使用 --help 查看帮助" + fi +} + +# ---------------------------------------------------------------------------- +# 验证备份目录 +# ---------------------------------------------------------------------------- +validate_backup_dir() { + log_info "验证备份目录: $BACKUP_PATH" + + if [[ ! -d "$BACKUP_PATH" ]]; then + die "备份目录不存在: $BACKUP_PATH" + fi + + # 检查是否有备份文件 + local sql_files=$(find "$BACKUP_PATH" -name "*.sql*" -type f 2>/dev/null) + + if [[ -z "$sql_files" ]]; then + die "备份目录中没有找到 SQL 备份文件" + fi + + # 检查元数据文件 + if [[ -f "${BACKUP_PATH}/metadata.txt" ]]; then + log_info "备份元数据:" + cat "${BACKUP_PATH}/metadata.txt" | grep -E "^(备份类型|备份时间|备份大小):" | while read line; do + log_info " $line" + done + fi + + log_info "备份目录验证通过" +} + +# ---------------------------------------------------------------------------- +# 显示备份内容并确认 +# ---------------------------------------------------------------------------- +confirm_restore() { + echo "" + echo "============================================================" + echo "警告: 即将执行数据库恢复操作" + echo "============================================================" + echo "" + echo "备份目录: $BACKUP_PATH" + echo "目标服务器: ${MYSQL_HOST}:${MYSQL_PORT}" + echo "MySQL 用户: $MYSQL_USER" + + if [[ -n "$SPECIFIED_DATABASE" ]]; then + echo "恢复数据库: $SPECIFIED_DATABASE" + else + echo "恢复范围: 所有数据库" + fi + + echo "" + echo "备份文件列表:" + find "$BACKUP_PATH" -name "*.sql*" -type f | while read f; do + echo " - $(basename "$f") ($(get_file_size "$f"))" + done + + echo "" + echo "警告: 此操作将覆盖目标数据库中的现有数据!" + echo "" + + if [[ "$SKIP_CONFIRM" != "true" ]]; then + read -p "确认要继续吗? (输入 'yes' 确认): " confirm + if [[ "$confirm" != "yes" ]]; then + log_info "操作已取消" + exit 0 + fi + else + log_warn "跳过确认 (-y 参数)" + fi +} + +# ---------------------------------------------------------------------------- +# 恢复前备份当前数据 +# ---------------------------------------------------------------------------- +backup_before_restore() { + if [[ "$NO_BACKUP_BEFORE_RESTORE" == "true" ]]; then + log_warn "跳过恢复前备份 (--no-backup 参数)" + return + fi + + log_info "恢复前备份当前数据..." + + local pre_restore_dir="${BACKUP_ROOT_DIR}/pre_restore_backup_${RESTORE_TIMESTAMP}" + ensure_dir "$pre_restore_dir" + + local dump_args="-h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER}" + if [[ -n "$MYSQL_PASSWORD" ]]; then + dump_args="$dump_args -p${MYSQL_PASSWORD}" + fi + + if [[ -n "$SPECIFIED_DATABASE" ]]; then + # 仅备份指定数据库 + log_info "备份数据库: $SPECIFIED_DATABASE" + $MYSQLDUMP_PATH $dump_args "$SPECIFIED_DATABASE" > "${pre_restore_dir}/${SPECIFIED_DATABASE}.sql" 2>> "$LOG_FILE" + else + # 备份所有数据库 + log_info "备份所有数据库" + $MYSQLDUMP_PATH $dump_args --all-databases > "${pre_restore_dir}/all_databases.sql" 2>> "$LOG_FILE" + fi + + log_info "恢复前备份完成: $pre_restore_dir" +} + +# ---------------------------------------------------------------------------- +# 恢复单个数据库 +# ---------------------------------------------------------------------------- +restore_database() { + local db_name="$1" + local sql_file="" + + # 查找对应的备份文件 + sql_file=$(find "$BACKUP_PATH" -name "${db_name}.sql*" -type f 2>/dev/null | head -n1) + + if [[ -z "$sql_file" ]]; then + die "找不到数据库 $db_name 的备份文件" + fi + + log_info "恢复数据库: $db_name" + log_info "备份文件: $sql_file" + + local mysql_args="-h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER}" + if [[ -n "$MYSQL_PASSWORD" ]]; then + mysql_args="$mysql_args -p${MYSQL_PASSWORD}" + fi + + # 判断是否需要解压 + if [[ "$sql_file" == *.gz ]]; then + log_info "解压并恢复..." + if ! gunzip -c "$sql_file" | $MYSQL_PATH $mysql_args "$db_name" 2>> "$LOG_FILE"; then + die "数据库 $db_name 恢复失败" + fi + elif [[ "$sql_file" == *.lz4 ]]; then + log_info "解压并恢复..." + if ! lz4 -d -c "$sql_file" | $MYSQL_PATH $mysql_args "$db_name" 2>> "$LOG_FILE"; then + die "数据库 $db_name 恢复失败" + fi + else + log_info "直接恢复..." + if ! $MYSQL_PATH $mysql_args "$db_name" < "$sql_file" 2>> "$LOG_FILE"; then + die "数据库 $db_name 恢复失败" + fi + fi + + log_info "数据库 $db_name 恢复成功" +} + +# ---------------------------------------------------------------------------- +# 恢复所有数据库 +# ---------------------------------------------------------------------------- +restore_all_databases() { + # 查找全库备份文件 + local all_db_file=$(find "$BACKUP_PATH" -name "all_databases.sql*" -type f 2>/dev/null | head -n1) + + if [[ -n "$all_db_file" ]]; then + # 使用全库备份文件 + log_info "使用全库备份文件恢复" + log_info "备份文件: $all_db_file" + + local mysql_args="-h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER}" + if [[ -n "$MYSQL_PASSWORD" ]]; then + mysql_args="$mysql_args -p${MYSQL_PASSWORD}" + fi + + if [[ "$all_db_file" == *.gz ]]; then + log_info "解压并恢复..." + if ! gunzip -c "$all_db_file" | $MYSQL_PATH $mysql_args 2>> "$LOG_FILE"; then + die "全库恢复失败" + fi + elif [[ "$all_db_file" == *.lz4 ]]; then + log_info "解压并恢复..." + if ! lz4 -d -c "$all_db_file" | $MYSQL_PATH $mysql_args 2>> "$LOG_FILE"; then + die "全库恢复失败" + fi + else + log_info "直接恢复..." + if ! $MYSQL_PATH $mysql_args < "$all_db_file" 2>> "$LOG_FILE"; then + die "全库恢复失败" + fi + fi + + log_info "全库恢复成功" + else + # 逐个恢复数据库 + log_info "逐个恢复数据库" + + local sql_files=$(find "$BACKUP_PATH" -name "*.sql*" -type f | grep -v "all_databases") + + if [[ -z "$sql_files" ]]; then + die "没有找到可恢复的数据库备份文件" + fi + + while IFS= read -r sql_file; do + local db_name=$(basename "$sql_file" | sed -E 's/\.sql(\.gz|\.lz4)?$//') + + # 先创建数据库 (如果不存在) + log_info "确保数据库存在: $db_name" + execute_mysql "CREATE DATABASE IF NOT EXISTS \`${db_name}\`;" 2>> "$LOG_FILE" || true + + restore_database "$db_name" + done <<< "$sql_files" + fi +} + +# ---------------------------------------------------------------------------- +# 验证恢复结果 +# ---------------------------------------------------------------------------- +verify_restore() { + log_info "验证恢复结果..." + + if [[ -n "$SPECIFIED_DATABASE" ]]; then + # 验证指定数据库 + local table_count=$(execute_mysql "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${SPECIFIED_DATABASE}';") + log_info "数据库 $SPECIFIED_DATABASE 包含 $table_count 个表" + else + # 验证所有数据库 + local db_count=$(execute_mysql "SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'performance_schema', 'sys', 'mysql');") + log_info "共有 $db_count 个用户数据库" + fi + + log_info "恢复验证完成" +} + +# ---------------------------------------------------------------------------- +# 初始化恢复环境 +# ---------------------------------------------------------------------------- +init_restore() { + log_info "============================================================" + log_info "MySQL 全量备份恢复开始" + log_info "============================================================" + log_info "恢复时间: $RESTORE_TIMESTAMP" + log_info "备份目录: $BACKUP_PATH" + log_info "日志文件: $LOG_FILE" + log_info "============================================================" + + # 设置错误处理 + setup_error_trap + + # 检查必要命令 + check_commands "$MYSQL_PATH" "gunzip" + + # 检查 MySQL 连接 + check_mysql_connection + + # 创建必要目录 + ensure_dir "$LOG_DIR" + ensure_dir "$RESTORE_TMP_DIR" + + # 获取锁 + acquire_lock "$LOCK_FILE" "恢复" + + # 设置清理 trap + trap cleanup_restore EXIT +} + +# ---------------------------------------------------------------------------- +# 清理函数 +# ---------------------------------------------------------------------------- +cleanup_restore() { + local exit_code=$? + + # 释放锁 + release_lock "$LOCK_FILE" + + if [[ $exit_code -ne 0 ]]; then + log_error "恢复过程中发生错误,退出码: $exit_code" + send_notification "[失败] MySQL 全量恢复" "恢复失败,请检查日志: $LOG_FILE" + fi +} + +# ---------------------------------------------------------------------------- +# 主函数 +# ---------------------------------------------------------------------------- +main() { + # 记录开始时间 + START_TIME=$(date +%s) + + # 解析参数 + parse_args "$@" + + # 初始化 + init_restore + + # 验证备份目录 + validate_backup_dir + + # 确认恢复操作 + confirm_restore + + # 恢复前备份 + backup_before_restore + + if [[ -n "$SPECIFIED_DATABASE" ]]; then + # 恢复指定数据库 + # 先创建数据库 (如果不存在) + log_info "确保数据库存在: $SPECIFIED_DATABASE" + execute_mysql "CREATE DATABASE IF NOT EXISTS \`${SPECIFIED_DATABASE}\`;" 2>> "$LOG_FILE" || true + + restore_database "$SPECIFIED_DATABASE" + else + # 恢复所有数据库 + restore_all_databases + fi + + # 验证恢复结果 + verify_restore + + # 计算总耗时 + local end_time=$(date +%s) + local duration=$(calculate_duration "$START_TIME" "$end_time") + + log_info "============================================================" + log_info "MySQL 全量备份恢复完成" + log_info "============================================================" + log_info "执行耗时: $duration" + log_info "============================================================" + + # 发送成功通知 + send_notification "[成功] MySQL 全量恢复" "恢复完成\n耗时: $duration" + + return 0 +} + +# 执行主函数 +main "$@"