一、为什么需要读写分离

在大多数互联网应用中,读操作远多于写操作:

读请求:70-80%
写请求:20-30%

单机数据库的问题:

主库:处理所有写请求 + 部分读请求
      ↓
连接池耗尽 → 响应变慢 → 用户投诉

解决方案:读写分离

写请求 ──► 主库
          │
读请求 ──┼──► 从库1
          │
          └──► 从库2

二、ShardingSphere实战

1. ShardingSphere-JDBC

架构:

应用 ──► ShardingSphere-JDBC(嵌入应用)
         ├── 改写SQL
         ├── 路由到正确的数据源
         └── 结果合并
              │
         ┌────┼────┐
         ▼    ▼    ▼
       主库  从库1 从库2

依赖配置:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>5.3.0</version>
</dependency>

数据源配置:

# 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/order_db
        username: root
        password: password
        
      slave0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.101:3306/order_db
        username: root
        password: password
        
      slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.102:3306/order_db
        username: root
        password: password
    
    rules:
      readwrite_splitting:
        data-sources:
          ds_master_slave:
            type: Static
            props:
              write-data-source-name: master
              read-data-source-names: slave0,slave1
            load-balancer:
              type: ROUND_ROBIN
              props:
                alpha: 5

# 强制路由到主库(写操作后立即读取)
    props:
      query-with-connection-preference: true

Java配置方式:

@Configuration
public class ShardingSphereConfig {
    
    @Bean
    public DataSource dataSource() {
        Map<String, DataSource> dataSourceMap = new HashMap<>();
        
        // 主库
        DataSource master = createDataSource(
            "jdbc:mysql://192.168.1.100:3306/order_db"
        );
        dataSourceMap.put("master", master);
        
        // 从库1
        DataSource slave0 = createDataSource(
            "jdbc:mysql://192.168.1.101:3306/order_db"
        );
        dataSourceMap.put("slave0", slave0);
        
        // 从库2
        DataSource slave1 = createDataSource(
            "jdbc:mysql://192.168.1.102:3306/order_db"
        );
        dataSourceMap.put("slave1", slave1);
        
        // 读写分离配置
        ReadwriteSplittingDataSourceRuleConfiguration dataSourceConfig = 
            new ReadwriteSplittingDataSourceRuleConfiguration(
                "ds_master_slave",     // 数据源名称
                "master",               // 主库
                Arrays.asList("slave0", "slave1"),  // 从库
                "round_robin"           // 负载均衡策略
            );
        
        // 负载均衡配置
        Properties props = new Properties();
        props.setProperty("alpha", "5");  // 权重
        
        Map<String, DataSource> dataSources = new HashMap<>();
        dataSources.put("ds_master_slave", 
            ReadwriteSplittingDataSourceFactory.create(
                dataSourceMap, 
                dataSourceConfig, 
                props
            ));
        
        return dataSources.get("ds_master_slave");
    }
}

强制路由到主库:

// 使用Hint强制路由到主库
HintManager hintManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();

// 写完数据后立即读取,走主库
try (HintManager hintManager = HintManager.getInstance()) {
    hintManager.setMasterRouteOnly();
    Order order = orderService.createOrder(request);
    // 这里读取走主库
    return orderService.getOrder(order.getId());
}

2. ShardingSphere-Proxy

架构:

应用(不需要修改)──► MySQL Client
                         │
                    ShardingSphere-Proxy
                    (独立的MySQL服务进程)
                         │
              ┌──────────┼──────────┐
              ▼          ▼          ▼
            主库       从库1       从库2

Docker部署:

# docker-compose.yml
version: '3'
services:
  shardingsphere-proxy:
    image: apache/shardingsphere-proxy:5.3.0
    container_name: shardingsphere-proxy
    ports:
      - "3307:3307"
    environment:
      MODE_TYPE: Standalone
      JVM_OPTS: "-Xmx512m -Xms512m"
    volumes:
      - ./conf:/opt/shardingsphere-proxy/conf
    networks:
      - shardingsphere

networks:
  shardingsphere:
    driver: bridge

server.yaml配置:

# conf/server.yaml
schemaName: readwrite_splitting

dataSources:
  ds_master:
    url: jdbc:mysql://192.168.1.100:3306/order_db?serverTimezone=UTC
    username: root
    password: password
    connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
    maxPoolSize: 50
    minPoolSize: 10
  
  ds_slave_0:
    url: jdbc:mysql://192.168.1.101:3306/order_db?serverTimezone=UTC
    username: root
    password: password
    connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
    maxPoolSize: 50
    minPoolSize: 10
    
  ds_slave_1:
    url: jdbc:mysql://192.168.1.102:3306/order_db?serverTimezone=UTC
    username: root
    password: password
    connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
    maxPoolSize: 50
    minPoolSize: 10

rules:
  - !readwrite_splitting
    dataSources:
      prds:
        type: Static
        props:
          write-data-source-name: ds_master
          read-data-source-names: ds_slave_0,ds_slave_1
        loadBalancerName: round_robin
    
    loadBalancers:
      round_robin:
        type: ROUND_ROBIN

应用连接:

// 应用连接ShardingSphere-Proxy(像连接普通MySQL一样)
String url = "jdbc:mysql://192.168.1.100:3307/readwrite_splitting";

三、MyCat实战

1. MyCat安装

# 下载
wget http://dl.mycat.org.cn/2.0/Mycat-server-2.0.0-release/
tar -zxf Mycat-server-2.0.0-release.tar.gz -C /usr/local/

# 配置环境变量
export MYCAT_HOME=/usr/local/mycat
export PATH=$PATH:$MYCAT_HOME/bin

# 启动
mycat start

# 查看状态
mycat status

2. server.xml配置

<!-- conf/server.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:server SYSTEM "server.dtd">
<mycat:server xmlns:mycat="http://io.mycat/">
    
    <system>
        <property name="defaultSqlParser">druidparser</property>
        <property name="serverPort">8066</property>
        <property name="managerPort">9066</property>
    </system>
    
    <user name="root">
        <property name="password">password</property>
        <property name="schemas">order_db</property>
    </user>
    
</mycat:server>

3. schema.xml配置

<!-- conf/schema.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
    
    <schema name="order_db" checkSQLschema="false" sqlMaxLimit="100">
        <!-- 定义逻辑表(可选) -->
        <table name="order" primaryKey="id" dataNode="dn1,dn2"/>
    </schema>
    
    <!-- 数据节点 -->
    <dataNode name="dn1" dataHost="dh1" database="order_db"/>
    <dataNode name="dn2" dataHost="dh2" database="order_db"/>
    
    <!-- 数据主机(读写分离) -->
    <dataHost name="dh1" maxCon="1000" minCon="10" balance="1"
              writeType="0" dbType="mysql" dbDriver="native">
        
        <heartbeat>select user()</heartbeat>
        
        <!-- 写库 -->
        <writeHost host="hostM1" url="192.168.1.100:3306" 
                   user="root" password="password">
            <!-- 读库 -->
            <readHost host="hostS1" url="192.168.1.101:3306" 
                      user="root" password="password"/>
            <readHost host="hostS2" url="192.168.1.102:3306" 
                      user="root" password="password"/>
        </writeHost>
    </dataHost>
    
</mycat:schema>

4. balance负载均衡策略

策略 说明 适用场景
0 所有读操作发送到writeHost 不推荐,可能读到旧数据
1 读操作分发到所有readHost 常用,读写分离
2 随机分发到readHost 不推荐
3 只分发到readHost,无可用则writeHost 读写分离+高可用

推荐配置:

<dataHost name="dh1" balance="1" writeType="0">
    <!-- balance=1:读操作分发到所有readHost -->
</dataHost>

四、ShardingSphere vs MyCat对比

维度 ShardingSphere MyCat
架构 JDBC层(轻量) 独立服务(重量)
部署 嵌入应用/独立Proxy 独立部署
性能 较高 略低
维护 社区活跃 社区相对沉寂
分库分表 支持 支持
读写分离 支持 支持
学习成本 较高
配置复杂度

五、读写分离最佳实践

1. 什么时候用读写分离

适合场景:

  • 读多写少(读:写 >= 7:3)
  • 单机数据库成为瓶颈
  • 数据量适中(单表<1000万)

不适合场景:

  • 写多读少
  • 强一致性要求(读写分离有延迟)
  • 单表数据量过亿(需要分库分表)

2. 读写延迟处理

方案1:强制路由到主库

// ShardingSphere
try (HintManager hintManager = HintManager.getInstance()) {
    hintManager.setMasterRouteOnly();
    return orderMapper.selectById(id);
}

方案2:应用层判断

public Order getOrderAfterCreate(Long orderId) {
    // 创建订单后,等待一小段时间让主从同步完成
    // 然后可以走从库
    Order order = orderMapper.selectById(orderId);
    
    // 如果是刚创建的,从主库读
    if (order.getCreateTime().isAfter(
            LocalDateTime.now().minusSeconds(5))) {
        return getOrderFromMaster(orderId);
    }
    
    return order;
}

3. 监控配置

spring:
  shardingsphere:
    rules:
      readwrite-splitting:
        data-sources:
          ds_0:
            type: Static
            load-balancer:
              type: ROUND_ROBIN
    
    props:
      # 开启SQL日志
      sql-show: true
-- MyCat监控
mysql -h 192.168.1.100 -P 9066 -u root -ppassword -e "show @@datanode;"
mysql -h 192.168.1.100 -P 9066 -u root -ppassword -e "show @@heartbeat;"

六、总结

读写分离是提升数据库读性能的有效手段:

  • ShardingSphere:轻量级,嵌入应用,易于使用
  • MyCat:独立服务,功能强大但配置复杂
  • 强制主库路由:解决读写延迟问题
  • 监控告警:确保复制健康

选型建议:

  1. 新项目推荐ShardingSphere
  2. 需要分库分表优先ShardingSphere
  3. 遗留系统改造可选MyCat

思考题:你们用的什么读写分离方案?有没有遇到过主从延迟的问题?


个人观点,仅供参考

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐