侧边栏壁纸
博主头像
术业有道之编程博主等级

亦是三月纷飞雨,亦是人间惊鸿客。亦是秋霜去叶多,亦是风华正当时。

  • 累计撰写 101 篇文章
  • 累计创建 54 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

mysql主从同步一键脚本

Administrator
2025-12-17 / 0 评论 / 0 点赞 / 3 阅读 / 15786 字

写在前面

最近上线一个项目,基于aws,但是量还没起来,为了节约成本没有使用aws rds,而是在ec2上安装了一个mysql(非 docker 模式,因为 docker 模式性能有限内存却多了接近一倍)
新的问题就诞生了,生产数据的安全如何保证。
于是我写了一个无脑脚本,将生产库的数据按毫秒级实时同步到备份库。用术语来说就是mysql主从复制

一、环境说明

  • 生产环境:aws ec2,使用 mysql8,裸装在liunx系统上。
  • 备份环境:私有服务器,使用mysql8,使用docker虚拟化
  • 无脑脚本mysql-slave.sh,在生产和备份环境都需要它,记得给执行权限 chmod +x mysql-slave.sh,一定要去改脚本中的全局必填变量,改成自己的。
#!/bin/bash
set -e

# ============================================================
# MySQL Master-Slave Replication Manager (Interactive, Idempotent)
# chmod +x mysql-slave.sh
# ============================================================
# =========
# 提前准备
# 1、MASTER 上的 mysql 的 root 密码
# 2、MASTER 的 ssh 密码
# 3、在 MASTER 上执行 1、2、3、4步
# 4、将生成的 ${DB_NAME}_init.sql 文件放到  Slave 的当前脚本目录中
# ------------------------
# 全局必填变量(请根据环境修改)
# ------------------------
DB_NAME="flow_db"  # 库名必须一致
# Master(宿主机)
MASTER_HOST_IP=""  # 主库的ip -- 为了脱敏 我删除了ip地址
MASTER_PORT=3306 # 主库的端口
REPL_USER="repl"  # 复制数据专属的用户-- 脚本会自动创建
REPL_PASS="123456789" # 复制数据专属的密码-- 脚本会自动创建
MASTER_MYSQL_CNF="/etc/mysql/mysql.conf.d/mysqld.cnf" # 主库的mysql配置文件

# Slave(Docker)
SLAVE_CONTAINER="slave_flow_db" # 备份库的mysql docker 名称
SLAVE_ROOT_PASSWORD="123456" # 备份库的mysql root账户的密码
SLAVE_MYSQL_CNF="/etc/mysql/my.cnf" # 这个文件可能不存在--脚本会自动创建
SLAVE_TEMP_DUMP_PATH="/tmp/${DB_NAME}_init.sql"  # 将文件放到 docker 容器的 /tmp 目录下

# ------------------------
# 通用函数
# ------------------------

error_exit() {
    echo "❌ 错误: $1" >&2
    exit 1
}

confirm() {
    read -p "⚠️ 确认执行?(y/n): " c
    [[ "$c" == "y" ]] || error_exit "用户取消"
}

pause() {
    read -p "按 Enter 继续..."
}

# ============================================================
# Master 相关
# ============================================================
master_restart(){
   echo "🔥 高危操作:将重启数据库"
      confirm
      confirm
      systemctl restart mysql && systemctl status mysql
}
master_configure_file() {
    echo "--- Master:配置 my.cnf (幂等) ---"

    [ -f "$MASTER_MYSQL_CNF" ] || error_exit "未找到 ${MASTER_MYSQL_CNF}"

    grep -q '^server-id=1' "$MASTER_MYSQL_CNF" || sudo sed -i '/^\[mysqld\]/a server-id=1' "$MASTER_MYSQL_CNF"
    grep -q '^log_bin=mysql-bin' "$MASTER_MYSQL_CNF" || sudo sed -i '/^\[mysqld\]/a log_bin=mysql-bin' "$MASTER_MYSQL_CNF"
    grep -q '^binlog_format=ROW' "$MASTER_MYSQL_CNF" || sudo sed -i '/^\[mysqld\]/a binlog_format=ROW' "$MASTER_MYSQL_CNF"
    grep -q '^gtid_mode=ON' "$MASTER_MYSQL_CNF" || sudo sed -i '/^\[mysqld\]/a gtid_mode=ON' "$MASTER_MYSQL_CNF"
    grep -q '^enforce_gtid_consistency=ON' "$MASTER_MYSQL_CNF" || sudo sed -i '/^\[mysqld\]/a enforce_gtid_consistency=ON' "$MASTER_MYSQL_CNF"

    echo "✅ 配置已写入"
    echo "⚠️ 请重启 MySQL "
}


master_create_repl_user() {
    echo "--- Master:创建复制用户 ---"
    read -s -p "输入 Master root 密码: " MYSQL_PASS
    echo

    mysql -u root -p${MYSQL_PASS} -e "
        CREATE USER IF NOT EXISTS '${REPL_USER}'@'%' IDENTIFIED BY '${REPL_PASS}';
        GRANT REPLICATION SLAVE ON *.* TO '${REPL_USER}'@'%';
        FLUSH PRIVILEGES;
    " || error_exit "授权失败"

    echo "✅ 复制用户已就绪"
}

master_export_data() {
    echo "--- Master:导出数据与坐标 ---"

    echo "⚠️ 检查 Master GTID 状态"
    check_master_gtid

    read -s -p "输入 Master root 密码: " MYSQL_PASS
    echo

    STATUS=$(mysql -u root -p${MYSQL_PASS} -e "SHOW MASTER STATUS\G")

    mysqldump -u root -p${MYSQL_PASS} \
        --single-transaction --routines --triggers --events \
        ${DB_NAME} > ${DB_NAME}_init.sql

    echo "✅ 导出完成"
    echo "请复制以下文件到 Slave 主机:"
    echo " - ${DB_NAME}_init.sql"
}

check_master_gtid() {
    MASTER_GTID_ON=$(mysql -h ${MASTER_HOST_IP} -u${REPL_USER} -p${REPL_PASS} -e "SHOW VARIABLES LIKE 'gtid_mode';" -s -N | awk '{print $2}')
    if [[ "$MASTER_GTID_ON" != "ON" ]]; then
        error_exit "Master 未开启 GTID,无法使用 MASTER_AUTO_POSITION=1,请先开启 GTID 并重启 Master"
    fi
}

# ============================================================
# Slave 相关(Docker)
# ============================================================

# --- 幂等检测 ---
slave_has_master() {
    docker exec ${SLAVE_CONTAINER} \
      mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e \
      "SHOW SLAVE STATUS\G" 2>/dev/null \
      | grep -q "Master_Host"
}

slave_running() {
    docker exec ${SLAVE_CONTAINER} \
      mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e \
      "SHOW SLAVE STATUS\G" 2>/dev/null \
      | grep -q "Slave_IO_Running: Yes"
}

slave_db_has_tables() {
    docker exec ${SLAVE_CONTAINER} mysql \
      -uroot -p${SLAVE_ROOT_PASSWORD} \
      -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}';" \
      | tail -n1 | grep -vq "^0$"
}

# --- 幂等操作 ---
slave_import_data_safe() {
    echo "--- Slave:幂等导入数据 ---"
    docker ps | grep -q ${SLAVE_CONTAINER} || error_exit "Slave 容器未运行"

    if slave_db_has_tables; then
        echo "ℹ️ Slave 数据库已有表,跳过数据导入(幂等保护)"
        return
    fi

    docker cp ${DB_NAME}_init.sql ${SLAVE_CONTAINER}:${SLAVE_TEMP_DUMP_PATH}

    docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "
        CREATE DATABASE IF NOT EXISTS ${DB_NAME};
    "

    docker exec -i ${SLAVE_CONTAINER} \
        mysql -uroot -p${SLAVE_ROOT_PASSWORD} ${DB_NAME} \
        < ${DB_NAME}_init.sql

    echo "✅ 数据导入完成"
}

slave_configure_mycnf() {
    echo "--- 配置 Slave my.cnf(幂等) ---"

    # 核心变量
    local CONF_PATH="/etc/mysql/my.cnf"
    local SERVER_ID=2

    # 关键配置项
    declare -A CONFIGS
    CONFIGS=(
        ["server-id"]="$SERVER_ID"
        ["log_bin"]="mysql-bin"
        ["binlog_format"]="ROW"
        ["gtid_mode"]="ON"
        ["enforce_gtid_consistency"]="ON"
        ["relay_log"]="relay-bin"
        ["relay_log_index"]="relay-bin.index"
        ["read_only"]="ON"
    )

    # 1️⃣ 检查文件是否存在
    docker exec ${SLAVE_CONTAINER} bash -c "mkdir -p /etc/mysql"
    if ! docker exec ${SLAVE_CONTAINER} test -f ${CONF_PATH}; then
        echo "ℹ️ my.cnf 不存在,创建并写入默认配置..."
cat > /tmp/my.cnf <<EOF
[mysqld]
server-id=${SERVER_ID}
log_bin=mysql-bin
binlog_format=ROW
gtid_mode=ON
enforce_gtid_consistency=ON
relay_log=relay-bin
relay_log_index=relay-bin.index
read_only=ON
EOF

        docker cp /tmp/my.cnf ${SLAVE_CONTAINER}:${CONF_PATH}
        echo "✅ my.cnf 已创建"
    else
        echo "ℹ️ my.cnf 已存在,检查关键配置..."
        for key in "${!CONFIGS[@]}"; do
            value="${CONFIGS[$key]}"
            # 检查是否已存在 key=value 行
            if ! docker exec ${SLAVE_CONTAINER} grep -q "^[[:space:]]*$key[[:space:]]*=" ${CONF_PATH}; then
                echo "ℹ️ 配置 $key 不存在,追加写入..."
                docker exec ${SLAVE_CONTAINER} bash -c "echo '$key=$value' >> ${CONF_PATH}"
            else
                echo "ℹ️ 配置 $key 已存在,跳过"
            fi
        done
        echo "✅ my.cnf 检查完成"
    fi

    echo "⚠️ 请重启 MySQL 容器以生效配置"
}

slave_start_replication_safe() {
    echo "--- Slave:幂等启动主从 ---"

    if slave_running; then
        echo "ℹ️ Slave 已在同步中,跳过 START SLAVE"
        return
    fi

    if slave_has_master; then
        echo "ℹ️ Slave 已配置 Master,仅启动复制线程"
        docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "START SLAVE;"
        return
    fi

    echo "--- 1. ⚠️ Slave 设置 Slave server-id---"
    local CURRENT_ID=$(docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "SELECT @@server_id;" -s -N)
    if [[ "$CURRENT_ID" != "2" ]]; then
        echo "⚠️ 当前 server_id=${CURRENT_ID},设置为 2"
        docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "SET GLOBAL server_id = 2;" || true
    else
        echo "ℹ️ server_id 已是 2,无需修改"
    fi

    echo "⚠️ Slave 尚未配置 Master,开始 CHANGE MASTER"
    confirm

    docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "
        CHANGE MASTER TO
            MASTER_HOST='${MASTER_HOST_IP}',
            MASTER_USER='${REPL_USER}',
            MASTER_PASSWORD='${REPL_PASS}',
            MASTER_PORT=${MASTER_PORT},
            MASTER_AUTO_POSITION=1;
        CHANGE REPLICATION FILTER REPLICATE_DO_DB=(${DB_NAME});
        START SLAVE;
    "

    echo "✅ 主从复制已配置并启动"
}

slave_status() {
    docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "SHOW SLAVE STATUS\G"
    echo "检查:\n Slave_IO_Running: Yes \n Slave_SQL_Running: Yes"
}

slave_pause_io() {
    docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "STOP SLAVE IO_THREAD;"
}

slave_pause_sql() {
    docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "STOP SLAVE SQL_THREAD;"
}

slave_pause_all() {
    docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "STOP SLAVE;"
}

slave_resume_safe() {
    if slave_running; then
        echo "ℹ️ Slave 已在运行"
        return
    fi
    docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "START SLAVE;"
    echo "✅ Slave 已恢复"
}

slave_reset_danger() {
    echo "🔥 高危操作:将彻底解除主从复制"
    confirm
    confirm
    docker exec ${SLAVE_CONTAINER} mysql -uroot -p${SLAVE_ROOT_PASSWORD} -e "
        STOP SLAVE;
        RESET SLAVE ALL;
    "
}
slave_restart_docker() {
    echo "🔥 高危操作:将重启数据库"
    confirm
    confirm
    docker restart ${SLAVE_CONTAINER}
}
# ============================================================
# 菜单
# ============================================================

while true; do
    clear
    echo "========== MySQL 主从复制管理 =========="
    echo "1) Master:配置 my.cnf"
    echo "2) Master:创建复制用户"
    echo "3) Master:重启数据库"
    echo "4) Master:导出数据 + 坐标"
    echo "---------------------------------------"
    echo "5) Slave:导入初始数据"
    echo "6) Slave:配置 my.cnf"
    echo "7) Slave:重启数据库"
    echo "8) Slave:启动主从同步"
    echo "9) Slave:查看状态(请检查 Slave_IO_Running 和 Slave_SQL_Running 是否为 Yes)"
    echo "10) Slave:暂停 IO"
    echo "11) Slave:暂停 SQL"
    echo "12) Slave:暂停全部"
    echo "13) Slave:恢复同步"
    echo "14) Slave:解除主从"
    echo "0) 退出"
    echo "======================================="
    read -p "请选择: " choice

    case $choice in
        1) master_configure_file ;;
        2) master_create_repl_user ;;
        3) master_restart ;;
        4) master_export_data ;;
        5) slave_import_data_safe ;;
        6) slave_configure_mycnf ;;
        7) slave_restart_docker ;;
        8) slave_start_replication_safe ;;
        9) slave_status ;;
        10) slave_pause_io ;;
        11) slave_pause_sql ;;
        12) slave_pause_all ;;
        13) slave_resume_safe ;;
        14) slave_reset_danger ;;
        0) exit 0 ;;
        *) echo "无效选择" ;;
    esac

    pause
done

  • 脚本运行菜单
    脚本运行菜单

注意事项:

  • 备份环境的数据库名要与生产环境的库名相同
  • 备份环境的数据库要能访问到生产环境数据库(用户名可以暂时不考虑,ip和端口能通就可以)
  • 一个备份库只能对应一个主库,但是一个主库可以对应多个备份库
  • 脚本中数据同步使用的是mysql GTID机制,这种机制可以确保数据库重启也能保持同步,比使用master_coord.txt可靠性高太多。
  • 主从默认是同步整个mysql实例,而不是某一个库,但是为了避免无效的同步,请在脚本slave_start_replication_safe方法中找到CHANGE REPLICATION FILTER REPLICATE_DO_DB=(${DB_NAME}); 行,加入你想同步的库名,例如:CHANGE REPLICATION FILTER REPLICATE_DO_DB=('a_db','b_db');。嫌麻烦直接删掉这行,直接同步整个mysql实例。

二、主库环境操作

  • 按顺序执行脚本菜单1) Master:配置 my.cnf、2) Master:创建复制用户、3) Master:重启数据库、4) Master:导出数据 + 坐标(可以反复执行,脚本已处理了幂等)
  • 执行完菜单4) Master:导出数据 + 坐标之后,脚本所在的文件夹中会生成一个${DB_NAME}_init.sql,将它丢到备份库的文件夹中(我的备份库上的文件夹是 /root/mysql_slave_flow
  • 如果你的主库本来就是空的,可以不执行菜单4) Master:导出数据 + 坐标,也就不需要${DB_NAME}_init.sql文件,对应的备份环境中也就不需要执行菜单5) Slave:导入初始数据

三、备份环境操作

  • 创建一个文件夹,用来存放要备份的库相关的文件 mkdir /root/mysql_slave_flow,文件夹内需要有mysql-slave.sh、${DB_NAME}_init.sql
  • 创建一个mysql容器,用来作为备份数据库 docker-compose-slave-flow.yml
    version: "3"
    services:
      slave_flow_mysql:
        container_name: slave_flow_db
        image: mysql:8.0.28
        restart: always
        ports:
          - "3316:3306"
        environment:
          TZ: "Asia/Shanghai"
          MYSQL_ROOT_PASSWORD: 123456
          MYSQL_DATABASE: flow_db
        volumes:
          - '/root/docker/volumes/slave_flow_db/conf:/etc/mysql' # 配置挂载
          - '/root/docker/volumes/slave_flow_db/data:/var/lib/mysql' # 数据挂载
          - '/root/docker/volumes/slave_flow_db/log:/var/log/mysql' # 日志挂载
          - '/root/docker/volumes/slave_flow_db/mysql-files:/var/lib/mysql-files'
        command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --default-authentication-plugin=mysql_native_password
        logging:
          driver: "json-file"
          options:
            max-size: "10m"
            max-file: "1"
    
  • 按顺序执行脚本菜单5) Slave:导入初始数据、6) Slave:配置 my.cnf、7) Slave:重启数据库、8) Slave:启动主从同步、9) Slave:查看状态(请检查 Slave_IO_Running 和 Slave_SQL_Running 是否为 Yes)(可以反复执行,脚本已处理了幂等)
  • 重点看菜单9) Slave:查看状态(请检查 Slave_IO_Running 和 Slave_SQL_Running 是否为 Yes)的结果,如图所示,只有 Slave_IO_RunningSlave_SQL_Running 都是 Yes 才是对的。如果发现不对,请看图中的Last_Error,这里会给出错误说明。我这里是正确配置,所以这项是空的。
    同步状态

四、验证

  • 在主库上插入或者修改一条数据,提交。
  • 马上去备份库检查这条数据,会发现数据同步过来了( 我就不截图了)

我找了一张网图,很好的阐述了mysql主从原理
mysql主从原理

个人公众号

0

评论区