MySQL 2 主 4 从 主从复制 + ShardingSphere 读写分离 完整生产级部署
一、前置清理(避免旧容器 / 网络冲突)
# 停止并删除所有容器
docker rm -f mysql-master1 mysql-master2 mysql-slave1 mysql-slave2 mysql-slave3 mysql-slave4
# 删除旧网络
docker network rm mysql-network
# 清空所有数据目录
sudo rm -rf /opt/mysql/mysql-*/*
# 创建新网络
docker network create --subnet=192.168.100.0/24 mysql-network
二、启动双主库
2.1 启动 master1
# 创建目录结构
sudo mkdir -p /opt/mysql/mysql-master1/{conf,data,logs}
# 生成配置文件
sudo tee /opt/mysql/mysql-master1/conf/my.cnf <<'EOF'
[mysqld]
server-id=1
log-bin=mysql-bin
binlog_format=ROW
log_slave_updates=ON # 双主必须开启:让主库也能作为从库同步数据
auto_increment_offset=1 # 双主自增偏移量:master1=1, master2=2
auto_increment_increment=2 # 双主自增步长:统一为2,避免主键冲突
# GTID核心配置(所有节点必须一致)
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_gtid_simple_recovery=ON
character-set-server=utf8mb4
default_time_zone='+8:00'
EOF
# 启动容器
docker run -d \
--name mysql-master1 \
--network mysql-network \
--ip 192.168.100.10 \
-p 3310:3306 \
-v /opt/mysql/mysql-master1/conf/my.cnf:/etc/my.cnf \
-v /opt/mysql/mysql-master1/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
--restart=always \
mysql:8.0
2.2 启动 master2(主库 2,端口 3311)
# 创建目录结构
sudo mkdir -p /opt/mysql/mysql-master2/{conf,data,logs}
# 生成配置文件
sudo tee /opt/mysql/mysql-master2/conf/my.cnf <<'EOF'
[mysqld]
server-id=2
log-bin=mysql-bin
binlog_format=ROW
log_slave_updates=ON
auto_increment_offset=2 # 双主自增偏移量:master2=2
auto_increment_increment=2 # 双主自增步长:统一为2
# GTID核心配置(所有节点必须一致)
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_gtid_simple_recovery=ON
character-set-server=utf8mb4
default_time_zone='+8:00'
EOF
# 启动容器
docker run -d \
--name mysql-master2 \
--network mysql-network \
--ip 192.168.100.11 \
-p 3311:3306 \
-v /opt/mysql/mysql-master2/conf/my.cnf:/etc/my.cnf \
-v /opt/mysql/mysql-master2/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
--restart=always \
mysql:8.0
2.3 只在 master1 创建复制账号,会自动同步到 master2
docker exec -it mysql-master1 mysql -uroot -proot -e "
CREATE USER 'repl'@'192.168.100.%' IDENTIFIED WITH mysql_native_password BY 'repl';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.100.%';
FLUSH PRIVILEGES;
"
2.4 配置双主双向同步
第一步:让 master2 同步 master1
docker exec -it mysql-master2 mysql -uroot -proot -e "
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO
MASTER_HOST='192.168.100.10',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl',
MASTER_AUTO_POSITION=1;
START SLAVE;
"
第二步:验证 master2 同步成功
# 等待10秒让同步完成
sleep 10
# 验证master2状态
echo "=== master2 同步状态 ==="
docker exec -it mysql-master2 mysql -uroot -proot -e "SHOW SLAVE STATUS\G" | grep Running
# 验证账号已经同步到master2
echo "=== 验证账号同步 ==="
docker exec -it mysql-master2 mysql -uroot -proot -e "SELECT user, host FROM mysql.user WHERE user='repl';"
✅ 成功标志:
Slave_IO_Running: Yes和Slave_SQL_Running: Yes- 能看到
repl@192.168.100.%账号
第三步:让 master1 同步 master2
docker exec -it mysql-master1 mysql -uroot -proot -e "
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO
MASTER_HOST='192.168.100.11',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl',
MASTER_AUTO_POSITION=1;
START SLAVE;
"
# 验证master1状态
echo "=== master1 同步状态 ==="
docker exec -it mysql-master1 mysql -uroot -proot -e "SHOW SLAVE STATUS\G" | grep Running
验证双向数据同步
# 在master1插入数据
docker exec -it mysql-master1 mysql -uroot -proot -e "
CREATE DATABASE IF NOT EXISTS test_db;
USE test_db;
CREATE TABLE IF NOT EXISTS test_sync (
id INT PRIMARY KEY AUTO_INCREMENT,
content VARCHAR(100),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO test_sync (content) VALUES ('从master1插入的数据');
"
# 在master2查看是否同步成功
echo "=== master2 查看同步数据 ==="
docker exec -it mysql-master2 mysql -uroot -proot -e "SELECT * FROM test_db.test_sync;"
# 在master2插入数据
docker exec -it mysql-master2 mysql -uroot -proot -e "
USE test_db;
INSERT INTO test_sync (content) VALUES ('从master2插入的数据');
"
# 在master1查看是否同步成功
echo "=== master1 查看同步数据 ==="
docker exec -it mysql-master1 mysql -uroot -proot -e "SELECT * FROM test_db.test_sync;"
✅ 成功标志:两个主库都能看到两条数据。
三、启动 4 个从库(slave1-slave4,负载均衡同步两个主库)
最佳实践:让 2 个从库同步 master1,2 个从库同步 master2,均衡两个主库的读压力
- slave1、slave3 → 同步 master1(3306)
- slave2、slave4 → 同步 master2(3306)
3.1 启动 slave1(从库 1,端口 3320,同步 master1)
# 创建目录结构
sudo mkdir -p /opt/mysql/mysql-slave1/{conf,data,logs}
# 生成配置文件
sudo tee /opt/mysql/mysql-slave1/conf/my.cnf <<'EOF'
[mysqld]
server-id=3
relay-log=mysql-relay-bin
read_only=1
super_read_only=1
# GTID核心配置(所有节点必须一致)
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_gtid_simple_recovery=ON
character-set-server=utf8mb4
default_time_zone='+8:00'
EOF
# 启动容器
docker run -d \
--name mysql-slave1 \
--network mysql-network \
--ip 192.168.100.20 \
-p 3320:3306 \
-v /opt/mysql/mysql-slave1/conf/my.cnf:/etc/my.cnf \
-v /opt/mysql/mysql-slave1/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
--restart=always \
mysql:8.0
3.2 启动 slave2(从库 2,端口 3321,同步 master2)
# 创建目录结构
sudo mkdir -p /opt/mysql/mysql-slave2/{conf,data,logs}
# 生成配置文件
sudo tee /opt/mysql/mysql-slave2/conf/my.cnf <<'EOF'
[mysqld]
server-id=4
relay-log=mysql-relay-bin
read_only=1
super_read_only=1
# GTID核心配置(所有节点必须一致)
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_gtid_simple_recovery=ON
character-set-server=utf8mb4
default_time_zone='+8:00'
EOF
# 启动容器
docker run -d \
--name mysql-slave2 \
--network mysql-network \
--ip 192.168.100.21 \
-p 3321:3306 \
-v /opt/mysql/mysql-slave2/conf/my.cnf:/etc/my.cnf \
-v /opt/mysql/mysql-slave2/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
--restart=always \
mysql:8.0
3.3 启动 slave3(从库 3,端口 3322,同步 master1)
# 创建目录结构
sudo mkdir -p /opt/mysql/mysql-slave3/{conf,data,logs}
# 生成配置文件
sudo tee /opt/mysql/mysql-slave3/conf/my.cnf <<'EOF'
[mysqld]
server-id=5
relay-log=mysql-relay-bin
read_only=1
super_read_only=1
# GTID核心配置(所有节点必须一致)
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_gtid_simple_recovery=ON
character-set-server=utf8mb4
default_time_zone='+8:00'
EOF
# 启动容器
docker run -d \
--name mysql-slave3 \
--network mysql-network \
--ip 192.168.100.22 \
-p 3322:3306 \
-v /opt/mysql/mysql-slave3/conf/my.cnf:/etc/my.cnf \
-v /opt/mysql/mysql-slave3/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
--restart=always \
mysql:8.0
3.4 启动 slave4(从库 4,端口 3323,同步 master2)
# 创建目录结构
sudo mkdir -p /opt/mysql/mysql-slave4/{conf,data,logs}
# 生成配置文件
sudo tee /opt/mysql/mysql-slave4/conf/my.cnf <<'EOF'
[mysqld]
server-id=6
relay-log=mysql-relay-bin
read_only=1
super_read_only=1
# GTID核心配置(所有节点必须一致)
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_gtid_simple_recovery=ON
character-set-server=utf8mb4
default_time_zone='+8:00'
EOF
# 启动容器
docker run -d \
--name mysql-slave4 \
--network mysql-network \
--ip 192.168.100.23 \
-p 3323:3306 \
-v /opt/mysql/mysql-slave4/conf/my.cnf:/etc/my.cnf \
-v /opt/mysql/mysql-slave4/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
--restart=always \
mysql:8.0
3.5 配置所有从库同步
配置 slave1 同步 master1:
docker exec -it mysql-slave1 mysql -uroot -proot -e "
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO
MASTER_HOST='192.168.100.10',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl',
MASTER_AUTO_POSITION=1;
START SLAVE;
"
配置 slave2 同步 master2:
docker exec -it mysql-slave2 mysql -uroot -proot -e "
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO
MASTER_HOST='192.168.100.11',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl',
MASTER_AUTO_POSITION=1;
START SLAVE;
"
配置 slave3 同步 master1:
docker exec -it mysql-slave3 mysql -uroot -proot -e "
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO
MASTER_HOST='192.168.100.10',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl',
MASTER_AUTO_POSITION=1;
START SLAVE;
"
配置 slave4 同步 master2:
docker exec -it mysql-slave4 mysql -uroot -proot -e "
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO
MASTER_HOST='192.168.100.11',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl',
MASTER_AUTO_POSITION=1;
START SLAVE;
"
验证所有从库同步状态
# 批量查看所有从库状态
for i in 1 2 3 4; do
echo "=== mysql-slave$i 状态 ==="
docker exec -it mysql-slave$i mysql -uroot -proot -e "SHOW SLAVE STATUS\G" | grep Running
done
✅ 成功标志:4 个从库都显示 Slave_IO_Running: Yes 和 Slave_SQL_Running: Yes
四、2 主 4 从同步验证
4.1 双主写验证
# 在master1插入数据
docker exec -it mysql-master1 mysql -uroot -proot -e "
CREATE DATABASE IF NOT EXISTS test_db;
USE test_db;
CREATE TABLE user (
id BIGINT PRIMARY KEY, # 双主必须用雪花算法主键,不能用自增
name VARCHAR(50) NOT NULL,
age INT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO user (id, name, age) VALUES (1, '张三', 20);
"
# 在master2插入数据
docker exec -it mysql-master2 mysql -uroot -proot -e "
USE test_db;
INSERT INTO user (id, name, age) VALUES (2, '李四', 25);
"
4.2 全节点数据同步验证
# 批量查看所有节点数据
for node in master1 master2 slave1 slave2 slave3 slave4; do
echo "=== mysql-$node 数据 ==="
docker exec -it mysql-$node mysql -uroot -proot -e "SELECT * FROM test_db.user;"
done
✅ 成功标志:6 个节点都能看到两条数据
4.3 从库只读验证
# 任意从库执行写操作,预期失败
docker exec -it mysql-slave3 mysql -uroot -proot -e "INSERT INTO test_db.user (id, name, age) VALUES (3, '王五', 30);"
五、Spring Boot 2 主 4 从读写分离配置(ShardingSphere 5.4.1)
5.1 pom.xml 依赖(完全不变)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- ShardingSphere 5.4.1 完美兼容Spring Boot 3.x -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
5.2 application.yml 2 主 4 从读写分离配置(核心修改)
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
shardingsphere:
enabled: true
datasource:
names: master1,master2,slave1,slave2,slave3,slave4
# 主库1
master1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.47.101:3306/test_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
maximum-pool-size: 20
# 主库2
master2:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.47.101:3307/test_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
maximum-pool-size: 20
# 从库1(高配:8核16G,权重40)
slave1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.47.101:3308/test_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
maximum-pool-size: 30 # 高配机器连接池更大
# 从库2(高配:8核16G,权重30)
slave2:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.47.101:3309/test_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
maximum-pool-size: 30
# 从库3(低配:4核8G,权重20)
slave3:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.47.101:3310/test_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
maximum-pool-size: 15
# 从库4(低配:4核8G,权重10)
slave4:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.47.101:3311/test_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
maximum-pool-size: 15
# 2主4从读写分离规则(核心修改:权重负载均衡)
rules:
readwrite-splitting:
data-sources:
readwrite-db:
type: Static
props:
write-data-source-names: master1,master2
read-data-source-names: slave1,slave2,slave3,slave4
write-load-balancer-name: write_round_robin # 写库继续用轮询
load-balancer-name: read_weight # 读库改用权重负载均衡
# 负载均衡策略定义
load-balancers:
# 写库:轮询(双主性能一致,不需要权重)
write_round_robin:
type: ROUND_ROBIN
# 读库:权重负载均衡(核心配置)
read_weight:
type: WEIGHT
props:
# 格式:数据源名称=权重值
slave1: 40
slave2: 30
slave3: 20
slave4: 10
props:
sql-show: true
database-type: MySQL
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: assign_id
5.3 核心代码(完全不变)
实体类
@Data
@TableName("user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
private LocalDateTime createTime;
}
Mapper 接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
Service 层
public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
Controller 层(含强制读主库)
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// 新增:轮询写master1/master2
@PostMapping
public User createUser(@RequestBody User user) {
userService.save(user);
return user;
}
// 根据ID查询:轮询读slave1/slave2
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getById(id);
}
// 查询所有:轮询读slave1/slave2
@GetMapping
public List<User> getAllUsers() {
return userService.list();
}
// 强制读主库(解决主从延迟场景)
@GetMapping("/force-master/{id}")
public User getUserByIdForceMaster(@PathVariable Long id) {
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.setWriteRouteOnly();
return userService.getById(id);
}
}
}
5.4 启动类(纯净版)
@SpringBootApplication
@MapperScan("com.mars.mapper")
public class ShardingSphereApplication {
public static void main(String[] args) {
SpringApplication.run(ShardingSphereApplication.class, args);
}
}
六、接口测试与路由验证
启动项目后,观察控制台日志,验证路由规则:
- 写操作(POST /user):交替路由到
master1和master2 - 读操作(GET /user):依次轮询路由到
slave1→slave2→slave3→slave4→slave1... - 强制读主库(GET /user/force-master/1):路由到任意一个主库
七、故障模拟与高可用验证
7.1 模拟单个从库宕机
docker stop mysql-slave2
- 现象:读请求自动跳过 slave2,轮询
slave1→slave3→slave4 - 写请求完全不受影响
- 恢复:启动 slave2 后,自动重新加入集群,读请求恢复 4 个从库轮询
7.2 模拟多个从库宕机
docker stop mysql-slave1 mysql-slave3
- 现象:读请求自动全部路由到
slave2和slave4 - 写请求完全不受影响
7.3 模拟单个主库宕机
docker stop mysql-master1
- 现象:写请求自动全部路由到
master2 - 读请求自动全部路由到
slave2和slave4(因为 slave1、slave3 同步的 master1 宕机了) - 恢复:启动 master1 后,自动重新加入集群,写请求恢复轮询,读请求恢复 4 个从库轮询
7.4 恢复主从同步
# 启动所有故障节点
docker start mysql-master1 mysql-slave1 mysql-slave2 mysql-slave3
# 批量验证所有节点同步状态
for node in master1 master2 slave1 slave2 slave3 slave4; do
echo "=== mysql-$node 状态 ==="
docker exec -it mysql-$node mysql -uroot -proot -e "SHOW SLAVE STATUS\G" | grep Running
done
✅ 2 主 4 从架构核心优势
- 超高可用:
- 写:任意 1 个主库宕机,另 1 个主库继续提供写服务
- 读:最多可宕机 3 个从库,只要还有 1 个从库正常,读服务就不中断
- 极致性能:
- 写性能:比单主提升 1 倍
- 读性能:比单从提升 4 倍,理论上可线性扩展
- 负载均衡:
- 写请求均匀分发到 2 个主库
- 读请求均匀分发到 4 个从库
- 自动故障转移,无需人工干预
- 无代码侵入:业务代码完全不需要修改,升级成本为 0
浙公网安备 33010602011771号