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需求)
不管用哪种方案,新增只读副本都只需要修改配置文件:
- 方案一:在
spring.datasource.replicas列表里新增一条副本配置,重启应用后自动纳入负载均衡 - 方案二:新增一个replica数据源配置,然后在
slave-data-source-names里加上它的名字即可
如果用Nacos/Apollo等配置中心,甚至可以做到热更新,不用重启应用。
五、额外注意点
- 副本延迟:AWS RDS副本会有几秒的复制延迟,如果你的业务有“写后立即读”的强一致性需求,可以给对应的GET方法加个自定义注解(比如
@UseMaster),在AOP里判断注解强制走主库 - 连接池调优:根据QPS调整副本的连接池大小,避免连接耗尽
- 监控:给主库和副本分别配置监控,关注连接数、QPS、延迟等指标
备注:内容来源于stack exchange,提问作者Sagar
相关产品推荐
相关产品推荐

