写在前面
最近上线一个项目,基于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.ymlversion: "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_Running和Slave_SQL_Running都是Yes才是对的。如果发现不对,请看图中的Last_Error,这里会给出错误说明。我这里是正确配置,所以这项是空的。

四、验证
- 在主库上插入或者修改一条数据,提交。
- 马上去备份库检查这条数据,会发现数据同步过来了( 我就不截图了)
我找了一张网图,很好的阐述了mysql主从原理


评论区