You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中Postgres主库与多只读副本的数据源管理及请求路由方案咨询

Spring Boot中Postgres主库与多只读副本的数据源管理及请求路由方案咨询

嗨,我来分享几个在Spring Boot里处理这种主从分离+只读副本负载均衡的实用方案,都是实际高QPS项目里验证过的,完美匹配你的需求:

一、核心需求拆解

先明确下我们要搞定的关键点:

  • PUT请求强制走Postgres主库(AWS RDS Primary)
  • 高QPS的GET请求均匀分配到多个只读副本,且新增副本只需改配置不用改代码
  • 数据源切换要透明,业务代码不用关心底层数据源

二、方案一:基于Spring原生AbstractRoutingDataSource自定义实现

这个方案灵活性拉满,完全可控,适合需要自定义路由逻辑的场景。

1. 多数据源配置(application.yml)

把主库和所有副本的配置都写在配置文件里,用列表存副本,方便后续新增:

spring:
  datasource:
    primary:
      url: jdbc:postgresql://primary-rds-url:5432/your-db
      username: db-user
      password: db-pass
      driver-class-name: org.postgresql.Driver
      hikari:
        maximum-pool-size: 20  # 主库连接池小一点,因为PUT请求少
    replicas:
      - url: jdbc:postgresql://replica1-rds-url:5432/your-db
        username: db-user
        password: db-pass
        hikari:
          maximum-pool-size: 50  # 副本连接池调大,应对高QPS GET
      - url: jdbc:postgresql://replica2-rds-url:5432/your-db
        username: db-user
        password: db-pass
        hikari:
          maximum-pool-size: 50

2. 数据源上下文管理

用ThreadLocal存储当前请求要使用的数据源标识(MASTER/REPLICA):

public class DataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSourceKey(String key) {
        CONTEXT_HOLDER.set(key);
    }

    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }
}

3. 自定义路由数据源(实现负载均衡)

继承Spring的AbstractRoutingDataSource,重写数据源选择逻辑,这里用轮询策略保证流量均匀分配到副本:

@Component
public class ReplicaRoutingDataSource extends AbstractRoutingDataSource {

    private final List<DataSource> replicaDataSources;
    private final AtomicInteger counter = new AtomicInteger(0);

    public ReplicaRoutingDataSource(DataSource primaryDataSource, List<DataSource> replicaDataSources) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("MASTER", primaryDataSource);
        this.replicaDataSources = replicaDataSources;
        setTargetDataSources(targetDataSources);
        setDefaultTargetDataSource(primaryDataSource); // 默认走主库,避免异常
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String key = DataSourceContextHolder.getDataSourceKey();
        if ("REPLICA".equals(key)) {
            // 轮询选副本,新增副本自动纳入负载均衡
            int index = counter.getAndIncrement() % replicaDataSources.size();
            return replicaDataSources.get(index);
            // 想换随机策略的话,改成下面这句:
            // int index = new Random().nextInt(replicaDataSources.size());
        }
        return "MASTER";
    }
}

4. 配置DataSource Bean

把主库和副本的数据源实例化,注入到路由数据源中:

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public List<DataSource> replicaDataSources(@Value("#{${spring.datasource.replicas}}") List<Map<String, String>> replicaConfigs) {
        return replicaConfigs.stream()
                .map(config -> DataSourceBuilder.create()
                        .url(config.get("url"))
                        .username(config.get("username"))
                        .password(config.get("password"))
                        .driverClassName("org.postgresql.Driver")
                        .build())
                .collect(Collectors.toList());
    }

    @Bean
    public DataSource dataSource(DataSource primaryDataSource, List<DataSource> replicaDataSources) {
        return new ReplicaRoutingDataSource(primaryDataSource, replicaDataSources);
    }
}

5. AOP实现请求路由

通过AOP拦截Controller方法,根据HTTP方法自动切换数据源:

@Aspect
@Component
public class DataSourceRoutingAspect {

    @Before("execution(* com.your-project.controller..*(..))")
    public void setDataSourceBasedOnRequest(JoinPoint joinPoint) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) return;

        HttpServletRequest request = attributes.getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 事务方法强制走主库,避免副本只读报错
        if (method.isAnnotationPresent(Transactional.class)) {
            DataSourceContextHolder.setDataSourceKey("MASTER");
            return;
        }

        // GET请求走副本,其他请求(PUT等)走主库
        if ("GET".equalsIgnoreCase(request.getMethod())) {
            DataSourceContextHolder.setDataSourceKey("REPLICA");
        } else {
            DataSourceContextHolder.setDataSourceKey("MASTER");
        }
    }

    @After("execution(* com.your-project.controller..*(..))")
    public void clearDataSource() {
        DataSourceContextHolder.clearDataSourceKey();
    }
}

三、方案二:用ShardingSphere快速实现(推荐)

如果不想自己写一堆代码,用ShardingSphere的主从分离功能是更省心的选择,它自带负载均衡、路由规则,配置简单。

1. 依赖引入(pom.xml)

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.4.1</version> <!-- 用最新稳定版 -->
</dependency>

2. 主从配置(application.yml)

直接配置主库和副本,指定负载均衡策略:

spring:
  shardingsphere:
    datasource:
      names: master,replica1,replica2
      # 主库配置
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: org.postgresql.Driver
        jdbc-url: jdbc:postgresql://primary-rds-url:5432/your-db
        username: db-user
        password: db-pass
        hikari:
          maximum-pool-size: 20
      # 副本1配置
      replica1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: org.postgresql.Driver
        jdbc-url: jdbc:postgresql://replica1-rds-url:5432/your-db
        username: db-user
        password: db-pass
        hikari:
          maximum-pool-size: 50
      # 副本2配置
      replica2:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: org.postgresql.Driver
        jdbc-url: jdbc:postgresql://replica2-rds-url:5432/your-db
        username: db-user
        password: db-pass
        hikari:
          maximum-pool-size: 50
    masterslave:
      load-balance-algorithm-type: round_robin  # 轮询负载均衡,可选random
      name: ms-datasource
      master-data-source-name: master
      slave-data-source-names: replica1,replica2
    props:
      sql:
        show: true  # 可选,开启SQL日志方便调试

3. 业务代码无需修改

ShardingSphere会自动路由:

  • SELECT语句自动分发到副本(负载均衡)
  • INSERT/UPDATE/DELETE语句强制走主库
  • 事务内的所有SQL都走主库,避免一致性问题

四、扩展性说明(满足未来-proof需求)

不管用哪种方案,新增只读副本都只需要修改配置文件:

  1. 方案一:在spring.datasource.replicas列表里新增一条副本配置,重启应用后自动纳入负载均衡
  2. 方案二:新增一个replica数据源配置,然后在slave-data-source-names里加上它的名字即可

如果用Nacos/Apollo等配置中心,甚至可以做到热更新,不用重启应用。


五、额外注意点

  • 副本延迟:AWS RDS副本会有几秒的复制延迟,如果你的业务有“写后立即读”的强一致性需求,可以给对应的GET方法加个自定义注解(比如@UseMaster),在AOP里判断注解强制走主库
  • 连接池调优:根据QPS调整副本的连接池大小,避免连接耗尽
  • 监控:给主库和副本分别配置监控,关注连接数、QPS、延迟等指标

备注:内容来源于stack exchange,提问作者Sagar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.21 15:48:09