多数据源场景为什么需要动态路由
Spring Boot应用连接单一数据源是默认方案,但业务增长到一定规模后必然遇到多数据源需求:读写分离(主库写、从库读)、分库分表(按业务线或租户拆分数据库)、跨系统数据聚合(同时访问多个业务库)。静态配置多数据源可以通过@Qualifier注入不同DataSource实现,但当数据源数量不固定或需要运行时切换时,就必须引入动态路由机制。Spring Boot 3.x配合AbstractRoutingDataSource可以优雅地解决这个问题。
动态数据源核心实现
AbstractRoutingDataSource是Spring JDBC提供的抽象类,核心只需实现determineCurrentLookupKey()方法,返回当前线程需要使用的数据源标识:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
// 线程上下文持有者
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void set(String dsKey) {
CONTEXT.set(dsKey);
}
public static String get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
ThreadLocal保证线程隔离,在并发环境下每个请求切换数据源互不影响。但ThreadLocal有一个坑:线程池场景下,请求结束后如果不调用clear(),下一个复用该线程的请求会读到上一次的数据源标识。务必在Filter或Interceptor的finally块中调用clear()。
数据源配置与注册
Spring Boot 3.x使用HikariCP作为默认连接池,多数据源场景下每个DataSource需要独立的连接池配置:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("app.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("app.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("app.datasource.order")
public DataSource orderDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
public DynamicDataSource dynamicDataSource(
DataSource masterDataSource,
DataSource slaveDataSource,
DataSource orderDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(4);
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
targetDataSources.put("order", orderDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dynamicDataSource);
factory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/**/*.xml"));
return factory.getObject();
}
}
application.yml中的连接池配置:
app:
datasource:
master:
jdbc-url: jdbc:mysql://10.0.1.10:3306/app_main
username: root
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 3000
slave:
jdbc-url: jdbc:mysql://10.0.1.11:3306/app_main
username: readonly
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 30
minimum-idle: 10
order:
jdbc-url: jdbc:mysql://10.0.2.10:3306/app_order
username: root
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 15
minimum-idle: 3
从库的连接池可以适当放大,因为读请求通常比写请求多。主库连接池不宜过大,写事务持有连接的时间较长,过多连接会导致锁等待。
自定义注解 + AOP实现声明式切换
手动调用DataSourceContextHolder.set()侵入性太强,通过自定义注解+AOP可以实现声明式数据源切换:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {
String value() default "master";
}
@Aspect
@Component
public class DataSourceAspect {
@Around("@annotation(ds)")
public Object around(ProceedingJoinPoint joinPoint, DS ds) throws Throwable {
String dsKey = ds.value();
try {
DataSourceContextHolder.set(dsKey);
return joinPoint.proceed();
} finally {
DataSourceContextHolder.clear();
}
}
}
使用方式:
@Service
public class OrderService {
@DS("master")
public void createOrder(Order order) {
orderMapper.insert(order);
}
@DS("slave")
public Order getOrder(Long id) {
return orderMapper.selectById(id);
}
@DS("order")
public List<Order> queryOrderDb(OrderQuery query) {
return orderMapper.queryList(query);
}
}
读写分离的自动路由
手动标注@DS(“slave”)虽然清晰,但容易遗漏——新同事写查询方法时忘了加注解,读请求就会打到主库。更好的方案是基于Spring事务的只读标记自动路由:
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ReadOnlyRouteAspect {
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object routeReadOnly(ProceedingJoinPoint joinPoint) throws Throwable {
// 如果已经手动指定了数据源,不做覆盖
if (DataSourceContextHolder.get() != null) {
return joinPoint.proceed();
}
// 检查@Transactional的readOnly属性
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Transactional tx = signature.getMethod().getAnnotation(Transactional.class);
if (tx != null && tx.readOnly()) {
DataSourceContextHolder.set("slave");
}
try {
return joinPoint.proceed();
} finally {
DataSourceContextHolder.clear();
}
}
}
配合@Transactional(readOnly = true)自动路由到从库,写操作默认走主库,无需手动标注@DS注解。这种约定优于配置的方式在大团队协作中更可靠。
分布式事务下的数据源切换陷阱
当引入Seata等分布式事务框架时,动态数据源切换会遇到冲突:Seata通过代理DataSource来管理全局锁和分支事务,而AbstractRoutingDataSource也是DataSource的代理。两层代理的顺序如果搞反,Seata拿到的Connection可能来自错误的数据源。
解决方案是确保Seata的DataSourceProxy包裹在DynamicDataSource内部而非外部:
// 正确:Seata代理每个实际数据源,DynamicDataSource做路由
@Bean
public DataSource seataMasterProxy(DataSource masterDataSource) {
return new DataSourceProxy(masterDataSource);
}
// 错误:Seata代理DynamicDataSource,路由信息丢失
@Bean
public DataSource seataProxy(DynamicDataSource dynamicDataSource) {
return new DataSourceProxy(dynamicDataSource); // BUG!
}
另一个常见陷阱是@DS注解与@Transactional的AOP执行顺序。如果DataSourceAspect比TransactionInterceptor先执行,数据源切换正常;如果顺序反转,事务开启时拿到的还是默认数据源。通过@Order注解确保DataSourceAspect的优先级高于事务切面:
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级
public class DataSourceAspect { ... }
连接池监控与泄漏检测
多数据源环境下连接泄漏更难发现,因为问题可能出在任何一个连接池。HikariCP内置了泄漏检测:
hikari:
leak-detection-threshold: 60000 # 60秒未归还视为泄漏
maximum-pool-size: 20
metrics-tracker: true
超过阈值的连接会打印WARN日志,包含堆栈信息,直接定位泄漏点。生产环境建议设为业务最大事务耗时的2倍,避免误报。
多数据源的监控指标需要按数据源分别采集。Spring Boot Actuator的/actuator/metrics端点暴露了hikaricp.connections前缀的指标,加上tags区分数据源:
# Prometheus采集配置
- pattern: 'hikaricp_connections_active{pool="master"}'
- pattern: 'hikaricp_connections_active{pool="slave"}'
- pattern: 'hikaricp_connections_active{pool="order"}'
当某个数据源的活跃连接数持续接近maximum-pool-size时,需要排查是否存在慢查询或事务超时,而非简单调大连接池——更多的连接只会让数据库压力更大,问题根因未被解决。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-duo-shu-ju-yuan-dong-tai-lu-you-shi-zhan-du-xie/