Skip to content
清晨的一缕阳光
返回

MySQL 读写分离实战

核心概念

读写分离是将数据库的读操作和写操作分离到不同数据库实例的技术,用于提升数据库并发能力。

读写分离架构

典型架构:
         应用层

    读写分离中间件
       ↓     ↓
    主库    从库 1
           从库 2
           从库 3

写操作 → 主库
读操作 → 从库

读写分离优势

优势说明
提升并发读操作分散到多个从库
性能提升主库专注于写操作
高可用从库可作为备用
扩展性可动态添加从库

读写分离挑战

挑战说明解决方案
数据一致性从库延迟导致读到旧数据强制读主库
事务处理事务内读写分离复杂事务内固定路由
故障切换主库故障处理复杂自动故障转移

读写分离实现方案

方案 1:应用层路由

原理: 应用层根据 SQL 类型路由到不同数据源。

// 数据源路由
public class DataSourceRouter {
    
    private static final ThreadLocal<DataSourceType> CONTEXT 
        = new ThreadLocal<>();
    
    public enum DataSourceType {
        MASTER,  // 主库
        SLAVE_1, // 从库 1
        SLAVE_2  // 从库 2
    }
    
    // 设置数据源
    public static void useMaster() {
        CONTEXT.set(DataSourceType.MASTER);
    }
    
    public static void useSlave() {
        CONTEXT.set(DataSourceType.SLAVE_1);  // 简单轮询
    }
    
    public static DataSourceType get() {
        return CONTEXT.get();
    }
}

// AOP 拦截
@Aspect
@Component
public class DataSourceAspect {
    
    @Before("@annotation(Master)")
    public void useMaster() {
        DataSourceRouter.useMaster();
    }
    
    @Before("@annotation(Slave)")
    public void useSlave() {
        DataSourceRouter.useSlave();
    }
}

// 使用
@Service
public class OrderService {
    
    @Master  // 写操作
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
    
    @Slave   // 读操作
    public Order getOrder(Long id) {
        return orderMapper.selectById(id);
    }
}

优点:

缺点:

方案 2:ShardingSphere-JDBC

原理: JDBC 层中间件,透明读写分离。

<!-- Maven 依赖 -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>5.3.0</version>
</dependency>
# application.yml 配置
spring:
  shardingsphere:
    datasource:
      names: ds-master,ds-slave-0,ds-slave-1
      ds-master:
        jdbc-url: jdbc:mysql://master:3306/order_db
        username: root
        password: password
      ds-slave-0:
        jdbc-url: jdbc:mysql://slave1:3306/order_db
        username: root
        password: password
      ds-slave-1:
        jdbc-url: jdbc:mysql://slave2:3306/order_db
        username: root
        password: password
    
    rules:
      readwrite-splitting:
        data-groups:
          rw-group:
            write-data-source-name: ds-master
            read-data-source-names: ds-slave-0,ds-slave-1
            load-balancer-name: round_robin
        load-balancers:
          round_robin:
            type: ROUND_ROBIN

优点:

缺点:

方案 3:ProxySQL

原理: MySQL 代理层,自动读写分离。

-- ProxySQL 配置

-- 1. 配置主从服务器
INSERT INTO mysql_servers (hostgroup_id, hostname, port) 
VALUES (1, 'master', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port) 
VALUES (2, 'slave1', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port) 
VALUES (2, 'slave2', 3306);

-- 2. 配置读写分离规则
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES 
(1, 1, '^SELECT.*FOR UPDATE$', 1, 1),  -- 写查询到主库
(2, 1, '^SELECT', 2, 1);                -- 读查询到从库

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

优点:

缺点:

方案 4:MyCat

原理: 数据库中间件,支持读写分离和分库分表。

<!-- schema.xml 配置 -->
<schema name="order_db" checkSQLschema="false" sqlMaxLimit="100">
    <table name="orders" dataNode="dn_master" type="global"/>
</schema>

<dataNode name="dn_master" dataHost="master" database="order_db"/>
<dataNode name="dn_slave1" dataHost="slave1" database="order_db"/>
<dataNode name="dn_slave2" dataHost="slave2" database="order_db"/>

<dataHost name="master" maxCon="1000" minCon="10" balance="1" writeType="0">
    <heartbeat>select user()</heartbeat>
    <writeHost host="master" url="master:3306" user="root" password="password"/>
</dataHost>

<dataHost name="slave1" maxCon="1000" minCon="10" balance="1">
    <heartbeat>select user()</heartbeat>
    <readHost host="slave1" url="slave1:3306" user="root" password="password"/>
</dataHost>

优点:

缺点:

数据一致性问题

问题 1:主从延迟

场景:
1. 用户下单(写主库)
2. 立即查询订单(读从库)
3. 从库延迟,查询失败

解决方案 1:强制读主库

// 关键业务强制读主库
@Master
public Order getOrderAfterCreate(Long orderId) {
    return orderMapper.selectById(orderId);
}

// 或标记关键数据
public Order getOrder(Long orderId, boolean forceMaster) {
    if (forceMaster) {
        DataSourceRouter.useMaster();
    }
    return orderMapper.selectById(orderId);
}

解决方案 2:等待从库追上

public Order getOrder(Long orderId) {
    // 检查从库延迟
    long delay = replicationMonitor.getDelay();
    if (delay > 1000) {  // 延迟超过 1 秒
        DataSourceRouter.useMaster();
    }
    return orderMapper.selectById(orderId);
}

解决方案 3:缓存最新数据

public Order getOrder(Long orderId) {
    // 先查缓存
    Order order = cache.get(orderId);
    if (order != null) {
        return order;
    }
    
    // 缓存未命中,查从库
    order = orderMapper.selectById(orderId);
    if (order != null) {
        cache.set(orderId, order, 60);  // 缓存 60 秒
    }
    return order;
}

问题 2:事务内读写分离

// 错误:事务内切换数据源
@Transactional
public void processOrder(Long orderId) {
    Order order = orderMapper.selectById(orderId);  // 从库
    order.setStatus(1);
    orderMapper.update(order);  // 主库(事务失效)
}

// 正确:事务内固定数据源
@Transactional
@Master  // 事务内都使用主库
public void processOrder(Long orderId) {
    Order order = orderMapper.selectById(orderId);  // 主库
    order.setStatus(1);
    orderMapper.update(order);  // 主库
}

问题 3:全局表一致性

场景:配置表、字典表等全局表需要实时一致

解决方案:

# ShardingSphere 配置全局表
spring:
  shardingsphere:
    rules:
      readwrite-splitting:
        tables:
          config_table:  # 全局表
            actual-data-nodes: ds-master

或应用层特殊处理:

// 全局表强制读主库
@Master
public Config getConfig(String key) {
    return configMapper.selectByKey(key);
}

负载均衡策略

轮询策略

// Round Robin
public class RoundRobinLoadBalancer {
    private AtomicInteger index = new AtomicInteger(0);
    
    public DataSource getDataSource(List<DataSource> dataSources) {
        int idx = Math.abs(index.getAndIncrement() % dataSources.size());
        return dataSources.get(idx);
    }
}

权重策略

// 根据从库性能分配权重
public class WeightLoadBalancer {
    
    static class SlaveWeight {
        DataSource dataSource;
        int weight;  // 权重
        int currentWeight;  // 当前权重
    }
    
    public DataSource getDataSource(List<SlaveWeight> slaves) {
        int totalWeight = slaves.stream().mapToInt(s -> s.weight).sum();
        int maxWeight = slaves.stream().mapToInt(s -> s.weight).max().getAsInt();
        
        int offset = new Random().nextInt(totalWeight);
        int current = 0;
        
        for (SlaveWeight slave : slaves) {
            current += slave.weight;
            if (offset < current) {
                return slave.dataSource;
            }
        }
        return slaves.get(0).dataSource;
    }
}

延迟优先策略

// 选择延迟最低的从库
public class LatencyLoadBalancer {
    
    public DataSource getDataSource(List<SlaveInfo> slaves) {
        return slaves.stream()
            .min(Comparator.comparingLong(SlaveInfo::getDelay))
            .map(SlaveInfo::getDataSource)
            .orElse(slaves.get(0).getDataSource());
    }
}

最佳实践

配置建议

# ShardingSphere 生产配置
spring:
  shardingsphere:
    props:
      sql-show: false  # 生产关闭 SQL 日志
      executor-size: 32  # 执行器线程数
    
    rules:
      readwrite-splitting:
        data-groups:
          rw-group:
            write-data-source-name: ds-master
            read-data-source-names: ds-slave-0,ds-slave-1,ds-slave-2
            load-balancer-name: weight
        load-balancers:
          weight:
            type: WEIGHT
            props:
              ds-slave-0: 3
              ds-slave-1: 2
              ds-slave-2: 1

监控指标

// 读写分离监控
@Component
public class ReadWriteSplittingMonitor {
    
    @Autowired
    private DataSource dataSource;
    
    // 监控主从延迟
    @Scheduled(fixedRate = 5000)
    public void monitorDelay() {
        long delay = getReplicationDelay();
        metrics.record("replication_delay", delay);
        
        if (delay > 60) {
            alert.send("主从延迟超过 60 秒");
        }
    }
    
    // 监控读写比例
    public void recordQuery(String type) {
        metrics.increment("db.query." + type);
    }
}

参考资料


分享这篇文章到:

上一篇文章
InnoDB 存储引擎详解
下一篇文章
Spring Cloud Gateway 核心