读写分离

返回 Spring Boot 基础

读写分离将写操作路由到主库(Master),读操作路由到从库(Slave/Replica),以此减轻主库压力、提升查询吞吐量。属于多数据源的特定场景。


整体架构

              应用层
               │
       ┌───────┴────────┐
       │  路由逻辑       │  ← 判断当前操作是读还是写
       └───────┬────────┘
       ┌───────┴────────┐
       ▼                ▼
   主库 (Master)    从库 (Replica)
   INSERT/UPDATE    SELECT
   DELETE/DDL       负载均衡(多从库轮询)
       │                ▲
       └── 主从同步 ─────┘  (MySQL Binlog 复制,异步/半同步)

注意:主从同步存在延迟(通常毫秒级,高负载时可能更长),写后立即读应强制走主库。


方案一:dynamic-datasource 注解路由(推荐)

引入 dynamic-datasource-spring-boot-starter,详见 多数据源

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://master-host:3306/db
          username: root
          password: secret
        slave1:
          url: jdbc:mysql://slave1-host:3306/db
          username: root
          password: secret
        slave2:
          url: jdbc:mysql://slave2-host:3306/db
          username: root
          password: secret
@Service
public class UserService {
 
    @DS("slave1")
    public List<User> findAll() { ... }       // 读从库
 
    // 写操作不加 @DS,走默认主库
    @Transactional
    public User create(User user) { ... }
}

多从库负载均衡:通过 dynamic-datasource 的分组功能,将多个从库配置为同一分组,框架自动轮询:

spring:
  datasource:
    dynamic:
      datasource:
        slave_1:    # 分组名 slave,编号 _1
          url: ...
        slave_2:    # 分组名 slave,编号 _2
          url: ...
@DS("slave")        // 指定分组名,自动轮询 slave_1 / slave_2
public List<User> findAll() { ... }

方案二:AOP + AbstractRoutingDataSource

适合不引入第三方框架的场景,完全自定义路由逻辑。

1. 数据源上下文

public class DataSourceContext {
    private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();
    public static final String MASTER = "master";
    public static final String SLAVE  = "slave";
 
    public static void useMaster() { HOLDER.set(MASTER); }
    public static void useSlave()  { HOLDER.set(SLAVE); }
    public static String get()     { return HOLDER.get(); }
    public static void clear()     { HOLDER.remove(); }
}

2. 路由数据源

public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContext.get();
    }
}

3. 注册 Bean

@Configuration
public class DataSourceConfig {
 
    @Bean("masterDs")
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    @Bean("slaveDs")
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    @Bean
    @Primary
    public DataSource routingDataSource(
            @Qualifier("masterDs") DataSource master,
            @Qualifier("slaveDs")  DataSource slave) {
        Map<Object, Object> targets = Map.of(
            DataSourceContext.MASTER, master,
            DataSourceContext.SLAVE,  slave
        );
        ReadWriteRoutingDataSource ds = new ReadWriteRoutingDataSource();
        ds.setDefaultTargetDataSource(master);
        ds.setTargetDataSources(targets);
        return ds;
    }
}

4. AOP 切面自动路由

@Aspect
@Component
@Order(1)                           // 需在事务切面之前执行
public class ReadWriteAspect {
 
    // @Transactional(readOnly = true) 的方法走从库
    @Around("@annotation(transactional)")
    public Object route(ProceedingJoinPoint pjp,
                        Transactional transactional) throws Throwable {
        try {
            if (transactional.readOnly()) {
                DataSourceContext.useSlave();
            } else {
                DataSourceContext.useMaster();
            }
            return pjp.proceed();
        } finally {
            DataSourceContext.clear();         // 防止线程复用时污染
        }
    }
}

使用时只需在 Service 上标注 readOnly

@Service
public class UserService {
 
    @Transactional(readOnly = true)    // → 从库
    public List<User> findAll() { ... }
 
    @Transactional                     // → 主库
    public User create(User user) { ... }
}

主从延迟问题处理

写后立即读需强制走主库:

// 方式一:注解切换
@Transactional
public void createAndReturn(User user) {
    userRepo.save(user);
    // 同一事务内后续查询已在主库连接上,无需额外处理
}
 
// 方式二:写操作后临时强制主库(跨方法场景)
public User createUser(User user) {
    userRepo.save(user);
    DataSourceContext.useMaster();     // 强制后续读取走主库
    return userRepo.findById(user.getId()).orElseThrow();
}
 
// 方式三:业务上接受最终一致(异步场景,不强制)

ShardingSphere 方案

Apache ShardingSphere 同时支持分库分表和读写分离,配置驱动,无需 AOP:

spring:
  datasource:
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    url: jdbc:shardingsphere:classpath:shardingsphere.yml
# shardingsphere.yml
rules:
  - !READWRITE_SPLITTING
    dataSources:
      rw_ds:
        writeDataSourceName: master
        readDataSourceNames:
          - slave1
          - slave2
        loadBalancerName: round_robin
    loadBalancers:
      round_robin:
        type: ROUND_ROBIN

相关链接

  • 多数据源 — 多数据源通用配置与 dynamic-datasource 用法
  • AOP — 切面路由实现原理
  • 事务管理readOnly 事务与连接管理
  • 连接池配置 — 主从库各自的 HikariCP 参数
  • MyBatis — MyBatis 与读写分离数据源的结合
  • JPA与Hibernate — JPA 下多数据源的 EntityManagerFactory 配置