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: YesSlave_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: YesSlave_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);
    }
}

六、接口测试与路由验证

启动项目后,观察控制台日志,验证路由规则:

  1. 写操作(POST /user):交替路由到 master1master2
  2. 读操作(GET /user):依次轮询路由到 slave1slave2slave3slave4slave1...
  3. 强制读主库(GET /user/force-master/1):路由到任意一个主库

七、故障模拟与高可用验证

7.1 模拟单个从库宕机

docker stop mysql-slave2
  • 现象:读请求自动跳过 slave2,轮询 slave1slave3slave4
  • 写请求完全不受影响
  • 恢复:启动 slave2 后,自动重新加入集群,读请求恢复 4 个从库轮询

7.2 模拟多个从库宕机

docker stop mysql-slave1 mysql-slave3
  • 现象:读请求自动全部路由到 slave2slave4
  • 写请求完全不受影响

7.3 模拟单个主库宕机

docker stop mysql-master1
  • 现象:写请求自动全部路由到 master2
  • 读请求自动全部路由到 slave2slave4(因为 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 个主库宕机,另 1 个主库继续提供写服务
    • 读:最多可宕机 3 个从库,只要还有 1 个从库正常,读服务就不中断
  2. 极致性能:
    • 写性能:比单主提升 1 倍
    • 读性能:比单从提升 4 倍,理论上可线性扩展
  3. 负载均衡:
    • 写请求均匀分发到 2 个主库
    • 读请求均匀分发到 4 个从库
    • 自动故障转移,无需人工干预
  4. 无代码侵入:业务代码完全不需要修改,升级成本为 0
posted @ 2026-06-10 11:09  MarsCode  阅读(0)  评论(0)    收藏  举报