SpringBoot实战:优雅实现动态数据源切换(附完整代码)
在实际业务开发中,你是否遇到过以下场景?
步骤3:配置多个数据源并注入路由
在Spring配置类中,我们将主从等多个真实数据源注册为Bean,并将它们装配到我们的
在
然后,我们创建一个AOP切面,在带有
注意:以上代码为示例片段,在实际项目中请根据你的包结构和业务逻辑进行适当调整,并做好异常处理。
- 读写分离:写操作发往主库,读操作发往从库,以提升系统性能。
- 多租户系统:每个租户拥有独立的数据库,需要根据当前租户动态连接。
- 业务分库:订单、用户等不同模块的数据存放在不同的物理数据库中。
JdbcTemplate或SqlSessionFactory,然后在代码中硬编码切换。这种方式不仅繁琐,而且难以维护。今天,我们将介绍如何利用Spring框架提供的AbstractRoutingDataSource,以一种高度可扩展和优雅的方式实现动态数据源切换。
一、核心原理:路由数据源
Spring的AbstractRoutingDataSource类是实现动态数据源的基石。它是一个“数据源的路由器”,其核心方法是determineCurrentLookupKey()。该方法会在每次数据库操作前被调用,我们只需告诉它当前应该使用哪个数据源的key(例如 “master”, “slave”, “tenant_123”),它就能自动路由到对应的真实数据源。我们的实现方案主要包含三个部分:- 动态数据源上下文:使用ThreadLocal来保存当前线程需要使用的数据源key,确保线程安全。
- 自定义路由数据源:继承
AbstractRoutingDataSource,从其上下文中获取key。 - 注解与AOP切面:通过自定义注解和AOP,在方法执行前自动切换数据源,实现声明式编程。
二、步步为营:代码实现
步骤1:定义数据源上下文持有者
首先,我们需要一个工具类来管理当前线程的数据源标识。这里我们使用ThreadLocal,因为它能保证每个线程的变量独立,避免多线程环境下的互相干扰。
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置当前线程的数据源key
*/
public static void setDataSourceKey(String dataSourceKey) {
CONTEXT_HOLDER.set(dataSourceKey);
}
/**
* 获取当前线程的数据源key
*/
public static String getDataSourceKey() {
return CONTEXT_HOLDER.get();
}
/**
* 清除当前线程的数据源key
* !!! 非常重要,防止内存泄漏 !!!
*/
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
}
步骤2:创建自定义路由数据源
接下来,我们创建核心的路由数据源类,它负责根据上下文中的key进行路由。public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 关键方法:从上下文中获取数据源key
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
DynamicDataSource中。
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master") // 对应application.yml中的配置
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build(); // 推荐使用Druid连接池
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary // 标记为默认数据源
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 配置默认数据源(当没有指定key时使用)
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
// 配置所有可选的数据源映射Map
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("master", masterDataSource());
dataSourceMap.put("slave", slaveDataSource());
// 未来可以继续添加新的数据源,如 dataSourceMap.put("log", logDataSource());
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
}
application.yml中配置数据源连接信息:
spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/master_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3307/slave_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
步骤4:使用注解驱动切换(最优雅的方式)
为了摆脱手动调用setDataSourceKey的繁琐,我们定义一个注解。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "master"; // 默认为主库
}
@DataSource注解的方法执行前后,自动设置和清理数据源上下文。
@Aspect
@Component
@Slf4j // 可选,用于日志记录
public class DataSourceAspect {
/**
* 定义切点:所有带有@DataSource注解的方法
*/
@Pointcut("@annotation(com.yourpackage.annotation.DataSource)")
public void dataSourcePointCut() {}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 获取方法上的注解
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource dsAnnotation = method.getAnnotation(DataSource.class);
if (dsAnnotation != null) {
String dataSourceKey = dsAnnotation.value();
// 在方法执行前设置数据源
DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey);
log.debug("Set datasource to : {}", dataSourceKey);
}
try {
// 执行目标方法
return point.proceed();
} finally {
// 方法执行完毕后,无论如何都要清空数据源key
DynamicDataSourceContextHolder.clearDataSourceKey();
log.debug("Clear datasource.");
}
}
}
三、实践应用:如何在业务中使用
现在,一切准备就绪!在Service层中,你可以通过一个简单的注解来轻松切换数据源。场景1:读写分离
@Service
@Slf4j
public class UserService {
@Autowired
private UserMapper userMapper;
// 写操作使用主库
@DataSource("master")
public void createUser(User user) {
userMapper.insert(user);
}
// 读操作使用从库,减轻主库压力
@DataSource("slave")
public User getUserById(Long id) {
return userMapper.selectById(id);
}
@DataSource("slave")
public List<User> getUserList() {
return userMapper.selectList();
}
}
场景2:多租户系统
假设你的租户数据源是动态加载的,你可以通过一个工具类在拦截器或过滤器中设置数据源。@Component
public class TenantContext {
public static void setCurrentTenant(String tenantId) {
// 假设数据源key的规则是 "tenant_" + tenantId
// 这些数据源可以在系统启动时或首次使用时加载到DynamicDataSource的映射中
String dataSourceKey = "tenant_" + tenantId;
DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey);
}
}
四、重要注意事项(避坑指南)
- 事务管理:
@Transactional注解开启的事务,其数据源是在方法开始时确定的。务必确保@DataSource注解在事务注解之前生效。通常AOP默认顺序可能有问题,可以通过@Order注解调整切面优先级,使其高于事务切面。 - 资源清理:AOP切面中的
finally块至关重要,它保证了即使在方法抛出异常的情况下,也能清空ThreadLocal,避免内存泄漏和后续操作的数据源污染。 - 异步场景:在
@Async异步方法中,由于线程切换,ThreadLocal值不会自动传递。你需要在调用异步方法前,将数据源key作为参数显式传递,并在异步方法的开头重新设置。
总结
通过AbstractRoutingDataSource+ ThreadLocal+ 注解&AOP的组合,我们构建了一个灵活、非侵入式的动态数据源切换方案。这套架构不仅代码清晰,而且扩展性极强,新增一个数据源只需在配置Map中添加即可,业务代码几乎无需改动。希望这篇实战博文能帮助你轻松搞定SpringBoot中的多数据源管理,让你的系统架构更加优雅和强大!注意:以上代码为示例片段,在实际项目中请根据你的包结构和业务逻辑进行适当调整,并做好异常处理。

浙公网安备 33010602011771号