作者:没有四次元口袋的蓝胖
日期:2026-09-17
标签:MySQL, Redis, 高可用集群, 主从复制, 哨兵模式
写在前面
这是一篇 MySQL 主从复制 + Redis 哨兵高可用集群的实战记录。基于 Docker 容器技术,分别搭建了 MySQL 一主两从(GTID 模式)和 Redis 主从+哨兵集群,并验证了自动故障转移机制。
一、项目背景
单机数据库存在三个致命问题:
- 单点故障:数据库挂了 = 整个系统瘫痪
- 读写瓶颈:所有读写请求压在一台机器上
- 数据风险:硬盘坏了数据就没了
解决方案:
| 组件 | 方案 | 解决的问题 |
|---|---|---|
| MySQL | 主从复制(一主两从,GTID 模式) | 读写分离 + 数据冗余 |
| Redis | 主从 + 哨兵(Sentinel) | 高可用 + 自动故障转移 |
二、MySQL 主从复制原理
2.1 replication 工作流程
写操作
│
▼
┌──────────────┐
│ Master │ 写入 binlog(二进制日志)
│ (主库) │
└──────┬───────┘
│
dump thread 发送 binlog 事件
│
┌──────────┼──────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Slave 1 │ │ Slave 2 │
│ (从库1) │ │ (从库2) │
└──────────────┘ └──────────────┘
每个从库有两个线程:
- IO 线程:从主库拉取 binlog → 写入 relay log
- SQL 线程:读取 relay log → 重放 SQL
2.2 两种复制模式对比
| 对比项 | 传统模式(基于文件名+偏移量) | GTID 模式(推荐) |
|---|---|---|
| 定位方式 | MASTER_LOG_FILE + MASTER_LOG_POS |
GTID(全局唯一事务ID) |
| 配置复杂度 | 高(需要精确计算偏移量) | 低(自动定位) |
| 故障恢复 | 手动找偏移量,容易出错 | 自动找到断点继续同步 |
| 数据一致性 | 依赖偏移量正确性 | 每个事务有唯一 ID,更可靠 |
💡 结论:生产环境直接用 GTID 模式,传统模式了解一下原理就行。
2.3 GTID 是什么?
GTID = Global Transaction Identifier,格式为 UUID:NUMBER:
UUID:MySQL 实例的唯一标识(server_uuid)NUMBER:该实例上执行的事务序号(从 1 开始递增)
例如:a1b2c3d4-1234-5678-9012-e5f6g7h8i9j0:42 表示某个 MySQL 实例上第 42 个事务。
三、MySQL 一主两从集群搭建
3.1 环境规划
| 角色 | 容器名 | 端口映射 | server-id |
|---|---|---|---|
| 主库 | mysql-master | 3307:3306 | 1 |
| 从库1 | mysql-slave1 | 3308:3306 | 2 |
| 从库2 | mysql-slave2 | 3309:3306 | 3 |
3.2 主库配置(my.cnf)
ini
[mysqld]
server-id=1
log-bin=mysql-bin
binlog-format=ROW
gtid-mode=ON
enforce-gtid-consistency=TRUE
# 允许从库连接
# 注意:生产环境要限定具体 IP
3.3 从库配置(my.cnf)
ini
[mysqld]
server-id=2 # 从库1用2,从库2用3(每个实例必须唯一)
log-bin=mysql-bin
relay-log=relay-bin
gtid-mode=ON
enforce-gtid-consistency=TRUE
# 从库设置为只读(防止误写)
read-only=1
3.4 Docker Compose 编排
yaml
version: '3.8'
services:
mysql-master:
image: mysql:5.7
container_name: mysql-master
environment:
MYSQL_ROOT_PASSWORD: root123
ports:
- "3307:3306"
volumes:
- ./master/my.cnf:/etc/mysql/conf.d/my.cnf
- master-data:/var/lib/mysql
networks:
- mysql-cluster
mysql-slave1:
image: mysql:5.7
container_name: mysql-slave1
environment:
MYSQL_ROOT_PASSWORD: root123
ports:
- "3308:3306"
volumes:
- ./slave1/my.cnf:/etc/mysql/conf.d/my.cnf
- slave1-data:/var/lib/mysql
networks:
- mysql-cluster
depends_on:
- mysql-master
mysql-slave2:
image: mysql:5.7
container_name: mysql-slave2
environment:
MYSQL_ROOT_PASSWORD: root123
ports:
- "3309:3306"
volumes:
- ./slave2/my.cnf:/etc/mysql/conf.d/my.cnf
- slave2-data:/var/lib/mysql
networks:
- mysql-cluster
depends_on:
- mysql-master
volumes:
master-data:
slave1-data:
slave2-data:
networks:
mysql-cluster:
driver: bridge
3.5 配置主从关系
步骤1:在主库创建复制专用账户
sql
-- 登录主库
mysql -h127.0.0.1 -P3307 -uroot -proot123
-- 创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl123';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
步骤2:在从库配置主库信息(GTID 模式)
sql
-- 登录从库1
mysql -h127.0.0.1 -P3308 -uroot -proot123
-- 配置主库信息
CHANGE MASTER TO
MASTER_HOST='mysql-master',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl123',
MASTER_AUTO_POSITION=1; -- GTID 模式:自动定位
-- 启动复制
START SLAVE;
-- 查看复制状态
SHOW SLAVE STATUS\G
关键状态检查:
Slave_IO_Running: Yes→ IO 线程正常(能从主库拉 binlog)Slave_SQL_Running: Yes→ SQL 线程正常(能重放 SQL)Seconds_Behind_Master: 0→ 没有延迟
从库2 的操作完全相同,只是 server-id=3,端口用 3309。
3.6 验证主从同步
sql
-- 在主库创建测试数据
mysql -h127.0.0.1 -P3307 -uroot -proot123
CREATE DATABASE test_db;
USE test_db;
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(50));
INSERT INTO users VALUES (1, '张三'), (2, '李四');
-- 在从库验证
mysql -h127.0.0.1 -P3308 -uroot -proot123
USE test_db;
SELECT * FROM users;
-- 应该能看到:1 张三, 2 李四
3.7 🎯 面试高频问题
Q:MySQL 主从复制的三个线程分别是什么?
| 线程 | 运行位置 | 作用 |
|---|---|---|
| Dump Thread | 主库 | 将 binlog 事件发送给从库 |
| IO Thread | 从库 | 从主库接收 binlog → 写入 relay log |
| SQL Thread | 从库 | 读取 relay log → 重放 SQL 语句 |
Q:主从延迟怎么解决?
- 业务层面:写操作后立即读走主库(强制读主)
- 半同步复制:主库等待至少一个从库确认收到 binlog 后才返回(折中方案)
- 并行复制 :MySQL 5.7+ 支持
slave_parallel_workers,多个 SQL 线程并行重放 - MGR(Group Replication):MySQL 5.7.17+ 原生支持的多主复制方案
Q:binlog 的三种格式有什么区别?
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| STATEMENT | 原始 SQL 语句 | 日志量小 | 不安全(NOW()、UUID() 等函数导致数据不一致) |
| ROW | 数据行的变更 | 最安全,精确记录行变化 | 日志量大 |
| MIXED | 默认 STATEMENT,不安全时自动切 ROW | 折中 | 仍有风险 |
💡 生产环境推荐用 ROW 格式。
四、Shell 脚本自动化配置主从
手动在每个从库执行 SQL 太麻烦,写个脚本一键搞定:
bash
#!/bin/bash
# setup_replication.sh - 自动化配置 MySQL 主从关系
MASTER_HOST="mysql-master"
MASTER_PORT=3306
REPL_USER="repl"
REPL_PASS="repl123"
ROOT_PASS="root123"
echo "===== Step 1: 在主库创建复制账户 ====="
docker exec mysql-master mysql -uroot -p${ROOT_PASS} -e "
CREATE USER IF NOT EXISTS '${REPL_USER}'@'%' IDENTIFIED BY '${REPL_PASS}';
GRANT REPLICATION SLAVE ON *.* TO '${REPL_USER}'@'%';
FLUSH PRIVILEGES;
"
echo "===== Step 2: 配置从库1 ====="
docker exec mysql-slave1 mysql -uroot -p${ROOT_PASS} -e "
STOP SLAVE;
CHANGE MASTER TO
MASTER_HOST='${MASTER_HOST}',
MASTER_PORT=${MASTER_PORT},
MASTER_USER='${REPL_USER}',
MASTER_PASSWORD='${REPL_PASS}',
MASTER_AUTO_POSITION=1;
START SLAVE;
"
echo "===== Step 3: 配置从库2 ====="
docker exec mysql-slave2 mysql -uroot -p${ROOT_PASS} -e "
STOP SLAVE;
CHANGE MASTER TO
MASTER_HOST='${MASTER_HOST}',
MASTER_PORT=${MASTER_PORT},
MASTER_USER='${REPL_USER}',
MASTER_PASSWORD='${REPL_PASS}',
MASTER_AUTO_POSITION=1;
START SLAVE;
"
echo "===== Step 4: 验证复制状态 ====="
for SLAVE in mysql-slave1 mysql-slave2; do
echo "--- ${SLAVE} ---"
docker exec ${SLAVE} mysql -uroot -p${ROOT_PASS} -e "SHOW SLAVE STATUS\G" | grep -E "Slave_IO_Running|Slave_SQL_Running|Seconds_Behind_Master"
done
echo "===== 主从配置完成 ====="
💡 这就是运维自动化的价值 ------ 把重复操作封装成脚本,减少人为错误。
五、Redis 主从 + 哨兵(Sentinel)集群
5.1 为什么需要哨兵?
Redis 主从复制的问题:主节点挂了,从节点不会自动升级为主节点,需要人工介入。
哨兵模式解决的问题:自动故障检测 + 自动故障转移(Failover)。
5.2 哨兵的工作原理
┌─────────────┐
│ Sentinel 1 │
└──────┬──────┘
│ 监控 + 投票
┌──────┼──────┐
▼ ▼
┌──────────┐ ┌──────────┐
│Sentinel 2│ │Sentinel 3│
└──────────┘ └──────────┘
│
│ 监控
┌──────┼──────┐
▼ ▼ ▼
┌────────┐┌────────┐┌────────┐
│Master ││Slave 1 ││Slave 2 │
│(主节点) ││(从节点) ││(从节点) │
└────────┘└────────┘└────────┘
故障转移流程:
1. 哨兵发现主节点不可达 → 标记为"主观下线"(SDOWN)
2. 超过 quorum 个哨兵同意 → 标记为"客观下线"(ODOWN)
3. 哨兵投票选出一个 Leader 哨兵
4. Leader 哨兵从从节点中选一个升级为主节点
5. 通知其他从节点跟随新的主节点
6. 通知客户端新的主节点地址
5.3 环境规划
| 角色 | 容器名 | 端口映射 |
|---|---|---|
| Redis 主节点 | redis-master | 6380:6379 |
| Redis 从节点1 | redis-slave1 | 6381:6379 |
| Redis 从节点2 | redis-slave2 | 6382:6379 |
| 哨兵1 | sentinel1 | 26379:26379 |
| 哨兵2 | sentinel2 | 26380:26379 |
| 哨兵3 | sentinel3 | 26381:26379 |
5.4 Redis 主从配置
主节点 redis.conf:
conf
# 绑定地址
bind 0.0.0.0
# 开启 AOF 持久化
appendonly yes
# 主节点密码
requirepass redis123
从节点 redis.conf:
conf
bind 0.0.0.0
appendonly yes
requirepass redis123
# 指定主节点(从节点配置)
replicaof redis-master 6379
masterauth redis123
5.5 哨兵配置(sentinel.conf)
conf
# 监控主节点,quorum=2(至少2个哨兵同意才判定下线)
sentinel monitor mymaster redis-master 6379 2
# 主节点下线判定时间(毫秒)
sentinel down-after-milliseconds mymaster 5000
# 故障转移超时时间
sentinel failover-timeout mymaster 60000
# 故障转移时,最多同时同步的从节点数
sentinel parallel-syncs mymaster 1
# 哨兵密码(如果 Redis 开启了密码)
sentinel auth-pass mymaster redis123
关键参数解析:
| 参数 | 含义 | 建议值 |
|---|---|---|
quorum |
判定主节点下线需要的哨兵数 | 哨兵总数的过半数(如 3 个哨兵设 2) |
down-after-milliseconds |
多久没响应判定为下线 | 5000(5秒) |
failover-timeout |
故障转移超时 | 60000(60秒) |
parallel-syncs |
故障转移后同时同步的从节点数 | 1(避免所有从节点同时同步导致服务不可用) |
5.6 Docker Compose 编排
yaml
version: '3.8'
services:
redis-master:
image: redis:6-alpine
container_name: redis-master
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./redis/master/redis.conf:/usr/local/etc/redis/redis.conf
- master-data:/data
ports:
- "6380:6379"
networks:
- redis-cluster
redis-slave1:
image: redis:6-alpine
container_name: redis-slave1
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./redis/slave1/redis.conf:/usr/local/etc/redis/redis.conf
- slave1-data:/data
ports:
- "6381:6379"
networks:
- redis-cluster
depends_on:
- redis-master
redis-slave2:
image: redis:6-alpine
container_name: redis-slave2
command: redis-server /usr/local/etc/redis/redis.conf
volumes:
- ./redis/slave2/redis.conf:/usr/local/etc/redis/redis.conf
- slave2-data:/data
ports:
- "6382:6379"
networks:
- redis-cluster
depends_on:
- redis-master
sentinel1:
image: redis:6-alpine
container_name: sentinel1
command: redis-sentinel /usr/local/etc/redis/sentinel.conf
volumes:
- ./redis/sentinel/sentinel.conf:/usr/local/etc/redis/sentinel.conf
ports:
- "26379:26379"
networks:
- redis-cluster
depends_on:
- redis-master
sentinel2:
image: redis:6-alpine
container_name: sentinel2
command: redis-sentinel /usr/local/etc/redis/sentinel.conf
volumes:
- ./redis/sentinel/sentinel2.conf:/usr/local/etc/redis/sentinel.conf
ports:
- "26380:26379"
networks:
- redis-cluster
depends_on:
- redis-master
sentinel3:
image: redis:6-alpine
container_name: sentinel3
command: redis-sentinel /usr/local/etc/redis/sentinel.conf
volumes:
- ./redis/sentinel/sentinel3.conf:/usr/local/etc/redis/sentinel.conf
ports:
- "26381:26379"
networks:
- redis-cluster
depends_on:
- redis-master
volumes:
master-data:
slave1-data:
slave2-data:
networks:
redis-cluster:
driver: bridge
5.7 验证故障转移
步骤1:确认主从关系正常
bash
# 查看主节点信息
docker exec redis-master redis-cli -a redis123 INFO replication
# 应该看到:
# role:master
# connected_slaves:2
步骤2:查看哨兵状态
bash
docker exec sentinel1 redis-cli -p 26379 SENTINEL master mymaster
# 应该看到:
# name:mymaster
# status:ok
# num-slaves:2
# num-other-sentinels:2
步骤3:模拟主节点故障
bash
# 停止主节点
docker stop redis-master
步骤4:等待 5~10 秒后检查故障转移结果
bash
# 查看哨兵的判断
docker exec sentinel1 redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# 返回的 IP 已经变了 → 说明新的主节点已经选出!
# 查看哪个从节点被提升为主节点
docker exec redis-slave1 redis-cli -a redis123 INFO replication
# 如果 role:master → 说明这个从节点已被提升为新主节点
步骤5:恢复旧主节点
bash
# 重新启动旧主节点
docker start redis-master
# 旧主节点会自动变成从节点,跟随新的主节点
docker exec redis-master redis-cli -a redis123 INFO replication
# role:slave → 自动降级为从节点
5.8 🎯 常见坑点
坑1:哨兵无法判定主节点下线
原因:哨兵数量不够或 quorum 设置过大。3 个哨兵时 quorum 设为 2,如果只有 1 个哨兵能连接到主节点则无法触发故障转移。
坑2:故障转移后应用还连接旧主节点
原因:应用直连 Redis IP。
解决:应用通过哨兵获取主节点地址:
java
// Spring Boot 配置
spring.redis.sentinel.master=mymaster
spring.redis.sentinel.nodes=sentinel1:26379,sentinel2:26379,sentinel3:26379
坑3:sentinel.conf 被自动修改后重启失败
哨兵在故障转移过程中会修改 sentinel.conf 文件。如果文件权限不对或容器内无写权限,会导致哨兵启动失败。确保 sentinel.conf 文件权限为 666。
六、整体架构图
┌─────────────────── MySQL 集群 ───────────────────┐
│ │
│ ┌──────────┐ binlog ┌──────────┐ │
│ │ Master │ ─────────────→ │ Slave 1 │ │
│ │ (写操作) │ │ (读操作) │ │
│ │ :3307 │ ─────────────→ │ Slave 2 │ │
│ └──────────┘ binlog │ (读操作) │ │
│ └──────────┘ │
└────────────────────────────────────────────────────┘
┌─────────────────── Redis 集群 ────────────────────┐
│ │
│ ┌──────────┐ 复制 ┌──────────┐ │
│ │ Master │ ───────→ │ Slave 1 │ │
│ │ (主节点) │ ───────→ │ Slave 2 │ │
│ │ :6380 │ 复制 └──────────┘ │
│ └──────────┘ │
│ ↑ 监控 ┌──────────────┐ │
│ ┌────┴────┐ │ Sentinel ×3 │ │
│ │ 哨兵集群 │ ←────── │ (自动故障转移) │ │
│ └─────────┘ └──────────────┘ │
└────────────────────────────────────────────────────┘
七、写在最后
学习建议
- 主从复制原理必须理解透,不只是会配,面试会问 binlog 格式、复制线程、延迟处理等细节
- GTID 模式是主流,传统基于偏移量的模式了解原理即可,实操中优先用 GTID
- 哨兵的 quorum 参数 很关键,设置不当会导致误判或无法故障转移,建议记住公式:
quorum = 哨兵数 / 2 + 1 - Shell 脚本是加分项,把重复的 SQL 操作封装成脚本,面试时展示这个能体现自动化运维意识
- 实操建议:在自己电脑上用 Docker 跑一遍,亲手做一次故障转移实验,面试时才能讲得生动
🎯 面试高频三连问
Q1:MySQL 主从复制的原理?
主库将数据变更写入 binlog,Dump Thread 将 binlog 事件发送给从库。从库的 IO Thread 接收后写入 relay log,SQL Thread 读取 relay log 并重放 SQL,实现数据同步。
Q2:Redis 哨兵模式的工作流程?
① 哨兵持续监控主从节点状态;② 主节点超时未响应 → 标记为 SDOWN(主观下线);③ 超过 quorum 个哨兵同意 → 标记为 ODOWN(客观下线);④ 哨兵投票选出 Leader;⑤ Leader 从从节点中选择一个升级为主节点;⑥ 通知其他从节点跟随新主节点。
Q3:Redis 主从复制是全量还是增量?
首次连接是全量同步(RDB 快照 + 增量命令),之后是增量同步(基于 replication offset 和 repl_backlog)。全量同步的触发条件:① 首次连接;② offset 已不在 repl_backlog 中。