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

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

背景

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

脚本思路
  1. 配置部分:脚本一开始就是一堆配置,比如源数据库和目标数据库的连接信息,还有要处理的表名、数据库名啥的。这些配置可以根据环境不同进行调整。

  2. 日期处理:我们用了 date 命令来处理日期,计算备份的时间范围。比如,我们要从某个时间点开始,按月备份数据,直到当前时间的前几个月。

  3. 备份和导入:用 mysqldump 来备份数据,然后通过 mysql 命令把数据导入到目标数据库。备份的时候,我们还按时间片来分片备份,避免一次性操作太多数据。

  4. 分区删除:备份完数据后,脚本还会自动删除原数据库中的对应分区。这部分是通过查询 INFORMATION_SCHEMA.PARTITIONS 来找到要删除的分区,然后执行 ALTER TABLE ... DROP PARTITION 来删除。

  5. 日志记录:整个过程都有详细的日志记录,方便排查问题。每次备份的月份也会记录在一个日志文件里,下次运行脚本时会接着上次的进度继续。

遇到的问题
  • 时间处理:日期计算这块儿有点绕,特别是涉及到跨年、跨月的时候。我们用了 date -d 来处理这些日期计算,但还是得小心边界情况。

  • 分区删除:删除分区的时候,得确保备份的数据已经成功导入到目标数据库,不然就丢数据了。所以脚本里加了很多检查,确保每一步都成功了才继续。

  • 性能问题:一开始我们没分片备份,结果一次性操作太多数据,数据库扛不住。后来改成按时间片分片备份,问题就解决了。

脚本实例
#!/bin/bash

#生产 MySQL 配置
cd /home/appuser/scripts/migrate_historical_data/
DB_HOST="192.168.30.2"
DB_PORT="13306"
DB_USER="root"
DB_PASS="zfh250126"
DB_NAME_ARRAY=("wf_cmbp" "wf_cmbp" "wf_cmbp" "wf_cmbp" "wf_cmbp" "wf_cmbp" "wf_cmbp" "wf_cmbp" "wf_cmbp" "wf_cmbp" "sg-jfgl")
TABLE_NAME_ARRAY=("stat_point_energy" "data_power_cumulant" "data_cumulant" "stat_point_used" "data_power_instant" "data_power_harmonic" "stat_max_min_iu" "stat_max_min_p" "data_water_extend" "data_pmo_device_json" "data_pile_monitor")
TABLE_INTERVAL_ARRAY=(4 6 240 240 744 744 240 240 744 744 240)

TARGET_DB_HOST="192.168.30.11"
TARGET_DB_PORT="33306"
TARGET_DB_USER="root"
TARGET_DB_PASS="j^To!S2h9B#KhhZL"
# 设置时间间隔(秒)
#interval=$((6 * 60 * 60))  # 小时

#测试 MySQL 配置
#cd /home/tmp
#DB_HOST="172.18.100.7"
#DB_PORT="3306"
#DB_USER="root"
#DB_PASS="j^To!S2h9B#KhhZL"
#DB_NAME_ARRAY=("wf_cmbp" "wf_cmbp" "sg-jfgl")
#TABLE_NAME_ARRAY=("data_power_cumulant" "data_cumulant" "data_pile_monitor")

# 备份到此数据库
#TARGET_DB_HOST="172.18.108.6"
#TARGET_DB_PORT="3307"
#TARGET_DB_USER="root"
#TARGET_DB_PASS="b3B0ls1ooLu5*1HV"
#TARGET_DB_PASS="sunrise"
#interval=$((372 * 60 * 60))  # 小时

# 日期格式化函数
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

    # 备份 MySQL 指定表的数据到文件
    echo "$(date +%Y%m%d_%H%M%S): 正在备份${TABLE_NAME}  ${next_backup_month} ... " >> "$console_file"
    
    # 计算当前备份月份的开始和结束日期
    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): 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"
        # 使用 mysqldump 备份,指定过滤条件
        backup_file_path="${backup_dir}/${TABLE_NAME}_${next_backup_month}_$(date +%H%M%S)_${count}.sql"
        # 条件判断:j = 1 没有时间段, j > 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

        # 使用 sed 删除 SET @@GLOBAL.GTID_PURGED= 开头直到分号结束的部分
        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
    # 将最新备份月份写入 backup.txt
    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"

总结

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值