SpringBoot + AbstractRoutingDataSource实现动态切换数据源,这样做才更优雅!
引言:多数据源的那些事儿
系统刚开始只有一个数据库,后来业务发展了,需要分库分表?或者要连接多个不同的数据库,比如主库、从库、日志库?再或者要做多租户系统,每个租户独立数据库?这时候,动态切换数据源就成了必须掌握的技能。今天我们就来聊聊如何在SpringBoot中优雅地实现动态数据源切换,让多数据源管理变得简单高效。
为什么需要动态数据源?
先说说为什么我们需要动态切换数据源。
想象一下,你是一家SaaS公司的后端工程师。公司有1000个租户,如果所有租户都用同一个数据库,随着租户增多,数据库压力会越来越大,查询变慢,维护困难。但如果每个租户独立数据库,又需要动态切换数据源。
动态数据源的优势:
- 性能提升:分库分表减轻单库压力
- 数据隔离:不同业务数据物理隔离
- 扩展性好:便于横向扩展
- 维护方便:独立管理不同业务数据
传统做法的问题
在介绍优雅方案之前,先看看传统做法有什么问题:
问题一:硬编码方式
// 硬编码,不灵活
public User getUserFromMaster(Long id) {
// 直接指定连接主库
return jdbcTemplateMaster.queryForObject(sql, User.class, id);
}
public User getUserFromSlave(Long id) {
// 直接指定连接从库
return jdbcTemplateSlave.queryForObject(sql, User.class, id);
}
这种方式的问题:
- 代码重复多
- 不灵活,无法动态调整
- 维护困难
问题二:配置繁琐
传统的多数据源配置往往需要为每个数据源都配置一套完整的Bean,代码冗余严重。
优雅实现方案
核心思路
Spring的AbstractRoutingDataSource为我们提供了解决方案。它是一个数据源路由处理器,可以根据不同的key动态选择目标数据源。
实现步骤
1. 自定义路由数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从ThreadLocal中获取当前数据源key
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
2. 数据源上下文管理
public class DynamicDataSourceContextHolder {
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. 配置多数据源
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
// 设置所有数据源
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("master", masterDataSource());
dataSourceMap.put("slave", slaveDataSource());
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
}
高级特性实现
1. 基于注解的数据源切换
定义一个注解来标识数据源:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "master";
}
创建切面来处理注解:
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(ds)")
public void setDataSource(JoinPoint point, DataSource ds) {
DynamicDataSourceContextHolder.setDataSourceKey(ds.value());
}
@After("@annotation(ds)")
public void clearDataSource(JoinPoint point, DataSource ds) {
DynamicDataSourceContextHolder.clearDataSourceKey();
}
}
2. 读写分离实现
在服务层使用注解实现读写分离:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@DataSource("master") // 写操作使用主库
public void saveUser(User user) {
userMapper.insert(user);
}
@DataSource("slave") // 读操作使用从库
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
3. 多租户数据源切换
对于多租户场景,可以根据租户ID动态切换数据源:
@Component
public class TenantDataSourceRouter {
public void setTenantDataSource(String tenantId) {
// 根据租户ID构建数据源key
String dataSourceKey = "tenant_" + tenantId;
DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey);
}
}
实现细节与最佳实践
1. 事务处理
在使用动态数据源时,事务处理需要特别注意:
@Service
public class OrderService {
@Transactional
@DataSource("master")
public void processOrder(Order order) {
// 所有操作都在同一个数据源和事务中
orderMapper.insert(order);
orderItemMapper.insertBatch(order.getItems());
}
}
2. 连接池管理
合理配置连接池参数,避免连接泄露:
spring:
datasource:
master:
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
3. 异步处理中的数据源切换
在异步方法中需要特别处理数据源切换:
@Async
@DataSource("slave")
public CompletableFuture<List<User>> getUsersAsync() {
// 异步方法中数据源切换
List<User> users = userMapper.selectAll();
return CompletableFuture.completedFuture(users);
}
性能优化建议
- 缓存数据源key:避免频繁的ThreadLocal操作
- 合理配置连接池:根据业务特点调整连接池大小
- 监控数据源使用情况:及时发现数据源切换问题
- 连接复用:在同一线程中复用数据源连接
常见问题与解决方案
问题1:数据源切换失效
原因:AOP切面优先级问题
解决方案:调整切面优先级,确保数据源切换在事务之前
问题2:事务跨数据源
原因:分布式事务处理不当
解决方案:避免跨数据源事务,或使用分布式事务框架
问题3:内存泄漏
原因:ThreadLocal未清理
解决方案:确保在方法结束后清理ThreadLocal
总结
通过SpringBoot + AbstractRoutingDataSource,我们可以优雅地实现动态数据源切换。关键在于:
- 合理架构:路由数据源、上下文管理、AOP切面
- 注解驱动:使用注解简化数据源切换
- 性能优化:合理配置连接池、监控使用情况
- 问题处理:注意事务、内存泄漏等常见问题
记住,动态数据源虽然强大,但也要根据业务场景合理使用。掌握了这些技巧,你就能轻松应对各种多数据源场景,让系统更加灵活和高效。
标题:SpringBoot + AbstractRoutingDataSource实现动态切换数据源,这样做才更优雅!
作者:jiangyi
地址:http://jiangyi.space/articles/2025/12/30/1767073873374.html