Skip to content

读写分离 ​

1. 整体架构 ​

读写分离把普通读请求分发到副本,把写事务和需要读取最新数据的请求发送到主库。启用前必须确认复制健康、副本延迟可接受,并明确写后读、事务内读和锁读的路由规则。

txt
                     ┌──────────────┐
                     │   应用层      │
                     └──────┬───────┘
                            │
                     ┌──────▼───────┐
                     │   代理层      │  ProxySQL / MySQL Router / ShardingSphere
                     └──────┬───────┘
                            │
              ┌─────────────┼─────────────┐
              │             │             │
       ┌──────▼──────┐ ┌───▼──────┐ ┌───▼──────┐
       │   主库(写)   │ │  副本1(读)│ │  副本2(读)│
       └─────────────┘ └──────────┘ └──────────┘

2. 配置方案 ​

2.1 ProxySQL ​

ini
# proxysql.cnf
mysql_variables:
    threads=4
    max_connections=2048

mysql_servers:
    - address: 192.168.1.100
      port: 3306
      hostgroup: 10  # 写组
    - address: 192.168.1.101
      port: 3306
      hostgroup: 20  # 读组
    - address: 192.168.1.102
      port: 3306
      hostgroup: 20  # 读组

mysql_query_rules:
    - match_pattern: "^SELECT .*FOR UPDATE"
      destination_hostgroup: 10
      apply: 1
    - match_pattern: "^SELECT .*FOR SHARE"
      destination_hostgroup: 10
      apply: 1
    - match_pattern: "^SELECT .*LOCK IN SHARE MODE"
      destination_hostgroup: 10
      apply: 1
    - match_pattern: "^SELECT"
      destination_hostgroup: 20
      apply: 1

2.2 MySQL Router ​

bash
mysqlrouter --bootstrap [email protected]:3306 --user=mysql
systemctl start mysqlrouter

2.3 Spring Boot 配置 ​

yaml
spring:
  datasource:
    write:
      url: jdbc:mysql://192.168.1.100:3306/mydb
    read:
      url: jdbc:mysql://192.168.1.101:3306/mydb

上面的 Spring 配置只声明两个数据源,不会自动完成读写路由;应用还需要使用 AbstractRoutingDataSource、注解或 ORM 框架提供的主从路由能力,并把事务内读写固定到主库。

3. 进阶方案 ​

3.1 ProxySQL 高级配置 ​

sql
-- 连接 ProxySQL 管理接口
mysql -h 127.0.0.1 -P 6032 -u admin -padmin

-- 添加后端 MySQL 服务器
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES
    (10, '192.168.1.100', 3306),  -- 写组
    (20, '192.168.1.101', 3306),  -- 读组
    (20, '192.168.1.102', 3306);  -- 读组

-- 配置查询规则;锁定读必须优先进入写组
INSERT INTO mysql_query_rules (rule_id, match_pattern, destination_hostgroup, apply) VALUES
    (1, '^SELECT .* FOR UPDATE', 10, 1),  -- SELECT FOR UPDATE 发往写组
    (2, '^SELECT .* FOR SHARE', 10, 1),   -- SELECT FOR SHARE 发往写组
    (3, '^SELECT .*LOCK IN SHARE MODE', 10, 1),
    (4, '^SELECT', 20, 1);                -- 普通 SELECT 发往读组

-- 加载配置
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL QUERY RULES TO DISK;

-- 监控后端状态
SELECT * FROM monitor.mysql_server_connect_log ORDER BY time_start_us DESC LIMIT 10;
SELECT * FROM monitor.mysql_server_replication_lag_log ORDER BY time_start_us DESC LIMIT 10;

3.2 ShardingSphere-JDBC 读写分离 ​

yaml
# application.yml
spring:
  shardingsphere:
    datasource:
      names: master,slave0,slave1
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.100:3306/mydb
        username: root
        password: secret
      slave0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.101:3306/mydb
        username: root
        password: secret
      slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.102:3306/mydb
        username: root
        password: secret
    rules:
      readwrite-splitting:
        data-sources:
          myds:
            write-data-source-name: master
            read-data-source-names: slave0,slave1
            load-balancer-name: round-robin
        load-balancers:
          round-robin:
            type: ROUND_ROBIN

4. 注意事项 ​

4.1 读写分离注意事项 ​

问题原因解决方案
写后读不一致主从复制延迟强制走主库 / 半同步复制
从库数据过期复制中断监控复制状态,自动摘除故障节点
事务内读写事务中可能读到旧数据事务内所有操作走主库
java
// Spring 中强制走主库
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MasterRoute {}

// AOP 切面实现
@Aspect
@Component
public class MasterRouteAspect {
    @Before("@annotation(masterRoute)")
    public void forceMaster(MasterRoute masterRoute) {
        DynamicDataSource.useMaster();
    }
    
    @After("@annotation(masterRoute)")
    public void clearMaster() {
        DynamicDataSource.clear();
    }
}

// 使用
@Service
public class OrderService {
    @MasterRoute  // 强制走主库
    public Order getOrderAfterCreate(Long orderId) {
        return orderRepository.findById(orderId);
    }
}

5. 最佳实践 ​

  1. 写后读走主库 — 避免主从延迟导致数据不一致
  2. 监控从库延迟 — 延迟超过阈值自动摘除
  3. 使用连接池 — 减少连接建立开销
  4. 读写分离中间件选择 — ProxySQL(运维友好)> ShardingSphere(Java 生态)> MySQL Router(官方)
  5. 从库数量合理 — 2-3 个从库即可满足大多数场景

Released under the MIT License.