2026年mysql数据库迁移(全流程)

以下是分步骤的完整指令指南:


第一步:在生产服务器(PROD)执行导出

在生产服务器的宿主机上运行。我们直接通过 Docker 导出并压缩,不产生巨大的临时 .sql 文件。

bash 复制代码
# 1. 导出并压缩(推荐加上 --single-transaction 保证不锁表)
docker exec prod-mysql-container /usr/bin/mysqldump -u root -p'prod_password' \
--single-transaction --routines --triggers prod_db_name | gzip > /tmp/prod_data.sql.gz

# 2. (可选) 检查文件大小
ls -lh /tmp/prod_data.sql.gz

第二步:将数据传送到预发布服务器(STAGING)

在生产服务器上使用 scp 将压缩包推送到预发布服务器。

bash 复制代码
# 将压缩包传给预发布服务器的 /tmp 目录
scp /tmp/prod_data.sql.gz root@staging_server_ip:/tmp/

第三步:在预发布服务器(STAGING)执行导入

登录预发布服务器,将数据导入到一个临时库

bash 复制代码
# 1. 创建临时数据库
docker exec staging-mysql-container mysql -u root -p'stag_password' \
-e "DROP DATABASE IF EXISTS staging_db_temp; CREATE DATABASE staging_db_temp;"

# 2. 解压并导入(使用 zcat 直接流式导入,不占双倍磁盘)
zcat /tmp/prod_data.sql.gz | docker exec -i staging-mysql-container mysql -u root -p'stag_password' staging_db_temp

第四步:执行库/表切换(实现毫秒级停机)

因为 MySQL 没有 RENAME DATABASE 命令,最稳妥的方法是使用 RENAME TABLE 批量切换。

1. 自动切换 SQL 脚本

为了避免手动输入每一个表名,我们在预发布宿主机执行这一段命令,它会自动生成迁移 SQL:

bash 复制代码
#!/bin/bash

# --- 配置变量 ---
CONTAINER_NAME="462ae8bc5476"
STAG_PWD="xxxxxxx"
TARGET_DB="ai_pblxxxxxxxx"
TEMP_DB="staging_db_temp"
OLD_DB="ai_pblxxxxxx_old"

echo "🚀 开始数据库切换流程..."

# 1. 自动处理备份库:如果存在则删除,然后重新创建
# 这样可以确保 OLD_DB 是空的,不会与本次移入的表产生冲突
echo "清理并初始化备份库 $OLD_DB..."
docker exec $CONTAINER_NAME mysql -u root -p"$STAG_PWD" -e "DROP DATABASE IF EXISTS $OLD_DB; CREATE DATABASE $OLD_DB;"

# 2. 获取临时库中所有的表名
echo "正在获取表列表..."
TABLES=$(docker exec $CONTAINER_NAME mysql -u root -p"$STAG_PWD" -sN -e "SELECT table_name FROM information_schema.tables WHERE table_schema = '$TEMP_DB';")

# 检查是否获取到了表,防止空库操作
if [ -z "$TABLES" ]; then
    echo "❌ 错误:在 $TEMP_DB 中未发现任何表,请检查导入是否成功。"
    exit 1
fi

# 3. 循环切换每一个表
echo "正在执行原子切换..."
for TABLE in $TABLES; do
    echo "正在处理表: $TABLE"
    # 步骤A: 尝试把正式库的旧表移到 OLD 库
    # 如果正式库原本没有这个表(比如新加的),2>/dev/null 会忽略报错
    docker exec $CONTAINER_NAME mysql -u root -p"$STAG_PWD" -e "RENAME TABLE $TARGET_DB.$TABLE TO $OLD_DB.$TABLE;" 2>/dev/null

    # 步骤B: 把临时库的新表移到正式库
    docker exec $CONTAINER_NAME mysql -u root -p"$STAG_PWD" -e "RENAME TABLE $TEMP_DB.$TABLE TO $TARGET_DB.$TABLE;"
done

echo "---"
echo "✅ 切换完成!"
echo "当前在线库: $TARGET_DB"
echo "本次同步的旧数据已备份至: $OLD_DB (原 $OLD_DB 已被覆盖)"

第五步:清理与验证

  1. 验证:登录预发布环境检查数据是否已更新。
  2. 清理临时库
bash 复制代码
# 删除临时空库
docker exec staging-mysql-container mysql -u root -p"$STAG_PWD" -e "DROP DATABASE staging_db_temp;"
# 删除宿主机压缩包
rm /tmp/prod_data.sql.gz
  1. 旧数据保留 :建议保留 staging_db_old 24 小时。如果发现生产同步过来的数据有问题,可以按同样方法快速切回去。

总结:手动流程 vs Python 脚本

特性 手动 Shell 流程 Python 中心化脚本
操作复杂度 需在两台机器间来回登录切换 只要在一台机器运行即可
网络利用 需先产生临时文件再 scp 可以直接通过 SSH 隧道流式传输(更快)
错误处理 需要人肉盯着每一行输出 自动捕获异常并发送飞书通知
安全性 密码容易残留在 history 密码保存在配置文件中

建议 :如果你是偶尔操作一次 ,用上面的 Shell 命令最直接;如果你每周/每天都要同步一次

建议用 Python 脚本,并配合飞书通知。

实现下面是具体的脚本:

python 复制代码
import subprocess
import datetime
import requests
import json

# --- 1. 配置信息 (现在两台机器都作为远程目标) ---
FEISHU_WEBHOOK_URL = "https://open.feishu.cn/open-apis/bot/v2/hook/你的-TOKEN"

PROD_SERVER = {
    "host": "1.2.3.4",  # 生产 IP
    "user": "root",  # SSH 用户
    "container": "prod-mysql",
    "db_name": "prod_db",
    "password": "prod_password"
}

STAG_SERVER = {
    "host": "5.6.7.8",  # 预发布 IP
    "user": "root",  # SSH 用户
    "container": "staging-mysql",
    "db_name": "staging_db",
    "temp_db": "staging_db_temp",
    "old_db": "staging_db_old",
    "password": "stag_password"
}


# --- 2. 辅助函数 ---

def send_feishu(status, message):
    """发送飞书通知"""
    color = "green" if status == "success" else "red"
    title = "🟢 跨服同步成功" if status == "success" else "🔴 跨服同步失败"
    payload = {
        "msg_type": "interactive",
        "card": {
            "header": {"title": {"tag": "plain_text", "content": title}, "template": color},
            "elements": [{"tag": "div", "text": {"tag": "lark_md",
                                                 "content": f"**时间**: {datetime.datetime.now()}\n**详情**: {message}"}}]
        }
    }
    requests.post(FEISHU_WEBHOOK_URL, json=payload)


def run_remote(server, cmd, input_data=None):
    """在指定的远程服务器上执行命令"""
    ssh_cmd = f"ssh -o ConnectTimeout=10 {server['user']}@{server['host']} \"{cmd}\""
    result = subprocess.run(ssh_cmd, shell=True, capture_output=True, text=True, input=input_data)
    if result.returncode != 0:
        raise Exception(f"服务器 {server['host']} 执行失败: {result.stderr.strip()}")
    return result.stdout.strip()


# --- 3. 主逻辑 ---

def main():
    start_time = datetime.datetime.now()
    try:
        print(f"🔗 正在建立 {PROD_SERVER['host']} -> {STAG_SERVER['host']} 的同步隧道...")

        # 1. 在预发布远程创建临时库
        run_remote(STAG_SERVER,
                   f"docker exec {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' -e 'DROP DATABASE IF EXISTS {STAG_SERVER['temp_db']}; CREATE DATABASE {STAG_SERVER['temp_db']};'")

        # 2. 核心:流式同步 (跨越两个 SSH 连接)
        # 逻辑:从 A 导出 -> 管道 -> 在 B 执行导入
        sync_pipeline = (
            f"ssh {PROD_SERVER['user']}@{PROD_SERVER['host']} "
            f"\"docker exec {PROD_SERVER['container']} mysqldump -u root -p'{PROD_SERVER['password']}' --single-transaction --routines --triggers {PROD_SERVER['db_name']} | gzip -c\" "
            f"| ssh {STAG_SERVER['user']}@{STAG_SERVER['host']} "
            f"\"zcat | docker exec -i {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' {STAG_SERVER['temp_db']}\""
        )
        print("🚀 数据传输中 (这可能需要几分钟,请稍候)...")
        subprocess.run(sync_pipeline, shell=True, check=True)

        # 3. 远程执行原子切换逻辑
        print("🔄 正在远程执行原子切换...")

        # 获取表名 (在预发布服务器执行)
        fetch_tables_cmd = f"docker exec {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' -sN -e \\\"SELECT table_name FROM information_schema.tables WHERE table_schema = '{STAG_SERVER['temp_db']}';\\\""
        tables = run_remote(STAG_SERVER, fetch_tables_cmd).split('\n')

        # 准备旧备份库
        run_remote(STAG_SERVER,
                   f"docker exec {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' -e 'DROP DATABASE IF EXISTS {STAG_SERVER['old_db']}; CREATE DATABASE {STAG_SERVER['old_db']};'")

        # 批量 Rename
        for t in tables:
            t = t.strip()
            if not t: continue
            rename_sql = f"RENAME TABLE {STAG_SERVER['db_name']}.{t} TO {STAG_SERVER['old_db']}.{t}, {STAG_SERVER['temp_db']}.{t} TO {STAG_SERVER['db_name']}.{t};"
            try:
                run_remote(STAG_SERVER,
                           f"docker exec {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' -e \"{rename_sql}\"")
            except:
                # 兼容正式库不存在该表的情况
                run_remote(STAG_SERVER,
                           f"docker exec {STAG_SERVER['container']} mysql -u root -p'{STAG_SERVER['password']}' -e \"RENAME TABLE {STAG_SERVER['temp_db']}.{t} TO {STAG_SERVER['db_name']}.{t};\"")

        duration = datetime.datetime.now() - start_time
        send_feishu("success",
                    f"✅ 同步成功!\n- 源: {PROD_SERVER['host']}\n- 目标: {STAG_SERVER['host']}\n- 耗时: {duration.seconds}秒")
        print("✨ 任务完成!")

    except Exception as e:
        print(f"❌ 任务失败: {e}")
        send_feishu("error", f"❌ 同步失败!\n错误详情: {str(e)}")


if __name__ == "__main__":
    main()
相关推荐
2301_822382762 小时前
Python上下文管理器(with语句)的原理与实践
jvm·数据库·python
m0_748229992 小时前
Laravel8.X核心功能全解析
开发语言·数据库·php
液态不合群3 小时前
【面试题】MySQL 的索引下推是什么?
数据库·mysql
2301_790300963 小时前
Python深度学习入门:TensorFlow 2.0/Keras实战
jvm·数据库·python
Code blocks4 小时前
SpringBoot从0-1集成KingBase数据库
数据库
程序员敲代码吗4 小时前
用Python生成艺术:分形与算法绘图
jvm·数据库·python
未来的旋律~4 小时前
sqlilabs注入靶场搭建与sql语句
数据库·sql
一个天蝎座 白勺 程序猿5 小时前
KingbaseES查询逻辑优化深度解析:从子查询到语义优化的全链路实践
开发语言·数据库·kingbasees·金仓数据库
我真的是大笨蛋5 小时前
InnoDB行级锁解析
java·数据库·sql·mysql·性能优化·数据库开发