聊聊怎么用 Bash 脚本搞定历史数据迁移

Admin 31 次阅读 本文阅读量 加载中… 编程语言

最近在搞一个历史数据迁移的任务,涉及到一堆 MySQL 表的数据备份和导入。为了不让手动操作搞得太累,我们写了个 Bash 脚本来搞定这事儿。今天就来分享一下这个脚本的思路和一些细节。

背景

我们有几个 MySQL 数据库,里面存了很多历史数据。这些数据是按月分区的,时间跨度还挺大。现在需要把这些数据迁移到另一个数据库里,顺便把原来的分区删掉,腾出点空间。

脚本思路

  1. 配置部分:脚本一开始就是一堆配置,比如源数据库和目标数据库的连接信息,还有要处理的表名、数据库名啥的。这些配置可以根据环境不同进行调整。
  2. 日期处理:我们用了 date 命令来处理日期,计算备份的时间范围。比如,我们要从某个时间点开始,按月备份数据,直到当前时间的前几个月。
  3. 备份和导入:mysqldump 来备份数据,然后通过 mysql 命令把数据导入到目标数据库。备份的时候,我们还按时间片来分片备份,避免一次性操作太多数据。
  4. 分区删除:备份完数据后,脚本还会自动删除原数据库中的对应分区。这部分是通过查询 INFORMATION_SCHEMA.PARTITIONS 来找到要删除的分区,然后执行 ALTER TABLE ... DROP PARTITION 来删除。
  5. 日志记录:整个过程都有详细的日志记录,方便排查问题。每次备份的月份也会记录在一个日志文件里,下次运行脚本时会接着上次的进度继续。

遇到的问题

  • 时间处理:日期计算这块儿有点绕,特别是涉及到跨年、跨月的时候。我们用了 date -d 来处理这些日期计算,但还是得小心边界情况。
  • 分区删除:删除分区的时候,得确保备份的数据已经成功导入到目标数据库,不然就丢数据了。所以脚本里加了很多检查,确保每一步都成功了才继续。
  • 性能问题:一开始我们没分片备份,结果一次性操作太多数据,数据库扛不住。后来改成按时间片分片备份,问题就解决了。

脚本实例

#!/bin/bash

# 工作目录
cd /home/appuser/scripts/migrate_historical_data/

# 源 MySQL 配置(生产环境)
DB_HOST="192.168.30.2"
DB_PORT="13306"
DB_USER="root"
DB_PASS="YOUR_SOURCE_DB_PASSWORD"

# 目标 MySQL 配置(归档库)
TARGET_DB_HOST="192.168.30.11"
TARGET_DB_PORT="33306"
TARGET_DB_USER="root"
TARGET_DB_PASS="YOUR_TARGET_DB_PASSWORD"

# 数据库与表配置
DB_NAME_ARRAY=("wf_cmbp" "wf_cmbp" "sg-jfgl")
TABLE_NAME_ARRAY=("stat_point_energy" "data_power_cumulant" "data_pile_monitor")
TABLE_INTERVAL_ARRAY=(4 6 240)

# 日期格式化函数
format_date() {
    date -d "@$1" "+%Y-%m-%d %H:%M:%S"
}

# 备份的最大月份数
max_backup_months=1

# 定义文件路径
backup_dir="./backup"
log_dir="./logs"

# 创建备份目录
mkdir -p "$backup_dir"
mkdir -p "$log_dir"

console_file="${log_dir}/exportAndCopy_$(date +%Y%m%d_%H%M%S).log"

# 获取当前时间的3年前的上个月
current_last_month=$(date -d "$(date +%Y-%m-15) -37 month" +%Y-%m)
echo "迁移数据的最后月是: $current_last_month" >> "$console_file"

len=${#TABLE_NAME_ARRAY[@]}

for (( i=0; i<$len; i++ )); do
  DB_NAME=${DB_NAME_ARRAY[$i]}
  TABLE_NAME=${TABLE_NAME_ARRAY[$i]}
  interval=$((${TABLE_INTERVAL_ARRAY[$i]} * 60 * 60))

  echo "db: $DB_NAME table: $TABLE_NAME" >> "$console_file"

  log_file="${log_dir}/${TABLE_NAME}_log.txt"

  # 获取最后备份的月份
  if test -f "$log_file"; then
    last_backup_month=$(tail -n 1 "$log_file")
  else
    last_backup_month="2018-11"
  fi

  echo "$(date +%Y%m%d_%H%M%S): 最后的备份月份是: $last_backup_month" >> "$console_file"

  # 获取下一个备份月份
  next_backup_month=$(date -d "$last_backup_month-15 +1 month" +%Y-%m)

  # 开始备份循环
  for ((j=1; j<=$max_backup_months; j++)); do

    # 如果下一个备份月份超过当前的上个月,停止备份
    if [[ "$next_backup_month" > "$current_last_month" ]]; then
      echo "$(date +%Y%m%d_%H%M%S): 已经达到最大备份月份 $current_last_month, 停止备份。" >> "$console_file"
      break
    fi

    # 计算当前备份月份的开始和结束日期
    start_date="${next_backup_month}-01"
    end_date=$(date -d "$start_date +1 month" +%Y-%m-01)

    echo "$(date +%Y%m%d_%H%M%S): 正在备份 ${TABLE_NAME} ${next_backup_month} ..." >> "$console_file"
    echo "$(date +%Y%m%d_%H%M%S): start: $start_date -> end: $end_date" >> "$console_file"

    # 按月备份,分片备份
    current=$(date -d "$start_date" +%s)
    end=$(date -d "$end_date" +%s)
    count=1

    while [ $current -lt $end ]; do
      next=$((current + interval))
      if [ $next -gt $end ]; then
        next=$end
      fi

      start_datetime=$(format_date $current)
      end_datetime=$(format_date $next)

      echo "$(date +%Y%m%d_%H%M%S): start: $start_datetime -> end: $end_datetime" >> "$console_file"

      backup_file_path="${backup_dir}/${TABLE_NAME}_${next_backup_month}_$(date +%H%M%S)_${count}.sql"

      # 条件判断:j = 1 且 count = 1 时无起始时间限制
      if [[ $j -eq 1 && $count -eq 1 ]]; then
        echo "backup data's condition: 2019-01-01 <= data_time < $end_datetime" >> "$console_file"
        mysqldump -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" "$TABLE_NAME" \
          --no-create-info \
          --where="data_time >= '2019-01-01' and data_time < '$end_datetime'" \
          > "${backup_file_path}"
      else
        echo "backup data's condition: $start_datetime <= data_time < $end_datetime" >> "$console_file"
        mysqldump -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" "$TABLE_NAME" \
          --no-create-info \
          --where="data_time >= '$start_datetime' AND data_time < '$end_datetime'" \
          > "${backup_file_path}"
      fi

      # 删除 mysqldump 中可能包含的 GTID 信息
      sed -i '/^SET @@GLOBAL.GTID_PURGED=/,/;/d' ${backup_file_path}

      # 检查备份是否成功
      if [ $? -eq 0 ]; then
        echo "$(date +%Y%m%d_%H%M%S): $DB_NAME $TABLE_NAME ${next_backup_month}_${count} 的数据备份成功" >> "$console_file"
      else
        echo "$(date +%Y%m%d_%H%M%S): $DB_NAME $TABLE_NAME ${next_backup_month}_${count} 的数据备份失败" >> "$console_file"
        exit 1
      fi

      # 导入数据
      mysql -h"$TARGET_DB_HOST" -P"$TARGET_DB_PORT" -u"$TARGET_DB_USER" -p"$TARGET_DB_PASS" "$DB_NAME" < "${backup_file_path}"

      # 检查导入是否成功
      if [ $? -eq 0 ]; then
        echo "$(date +%Y%m%d_%H%M%S): $DB_NAME $TABLE_NAME ${next_backup_month}_${count} 的数据导入成功" >> "$console_file"
      else
        echo "$(date +%Y%m%d_%H%M%S): $DB_NAME $TABLE_NAME ${next_backup_month}_${count} 的数据导入失败" >> "$console_file"
        exit 1
      fi

      current=$next
      ((count++))
    done

    echo "正在查找需要删除的分区,日期范围: $start_date 到 $end_date" >> "$console_file"

    # 查询要删除的分区名称
    partition_name=$(mysql -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" -D"$DB_NAME" -sse "
      SELECT PARTITION_NAME
      FROM INFORMATION_SCHEMA.PARTITIONS
      WHERE TABLE_SCHEMA = '$DB_NAME'
        AND TABLE_NAME = '$TABLE_NAME'
        AND PARTITION_DESCRIPTION LIKE '%$end_date%'
      LIMIT 1;
    ")

    # 检查是否找到分区
    if [ -z "$partition_name" ]; then
      echo "$(date +%Y%m%d_%H%M%S): 未找到符合日期范围的分区" >> "$console_file"
    else
      echo "$(date +%Y%m%d_%H%M%S): 正在删除分区: $DB_NAME $TABLE_NAME $partition_name" >> "$console_file"

      # 删除分区
      delete_partition_sql="ALTER TABLE $TABLE_NAME DROP PARTITION $partition_name;"
      mysql -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" -e "$delete_partition_sql" "$DB_NAME"

      # 检查删除是否成功
      if [ $? -eq 0 ]; then
        echo "$(date +%Y%m%d_%H%M%S): $DB_NAME $TABLE_NAME $partition_name 分区删除成功" >> "$console_file"
      else
        echo "$(date +%Y%m%d_%H%M%S): $DB_NAME $TABLE_NAME $partition_name 分区删除失败" >> "$console_file"
        exit 1
      fi
    fi

    # 将最新备份月份写入日志
    echo "$next_backup_month" >> "$log_file"

    # 更新下一个备份月份
    next_backup_month=$(date -d "$next_backup_month-15 +1 month" +%Y-%m)
  done
done

echo "$(date +%Y%m%d_%H%M%S): 所有备份操作完成。" >> "$console_file"

总结

这个脚本虽然看起来有点复杂,但其实思路挺清晰的。通过自动化脚本,我们省去了很多手动操作的麻烦,也减少了出错的可能性。如果你也有类似的数据迁移需求,可以参考一下这个脚本的思路,说不定能帮上忙。

当然,脚本还有很多可以优化的地方,比如加入更多的错误处理,或者支持更多的数据库类型。不过目前来说,它已经很好地完成了我们的任务。如果你有更好的想法,欢迎一起讨论!