问题背景
在上一篇文章中,我们讨论了如何在Spring中配置多个数据源。但在某些场景下,我们可能需要在运行时动态切换数据源,比如:
- 读写分离场景
- 多租户系统
- 分库分表
- 数据源故障切换
本文将介绍如何实现动态数据源切换。
实现方案
1. 创建动态数据源类
首先,我们需要继承 AbstractRoutingDataSource 类来实现动态数据源:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}2. 创建数据源上下文持有者
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}3. 创建数据源切换注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "primary";
}4. 实现AOP切面
@Aspect
@Component
@Order(1) // 保证该AOP在@Transactional之前执行
public class DataSourceAspect {
@Pointcut("@annotation(com.example.annotation.DataSource)")
public void dataSourcePointCut() {}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource ds = method.getAnnotation(DataSource.class);
if (ds == null) {
ds = point.getTarget().getClass().getAnnotation(DataSource.class);
}
if (ds != null) {
DataSourceContextHolder.setDataSourceType(ds.value());
}
try {
return point.proceed();
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
}5. 配置动态数据源
@Configuration
public class DynamicDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DynamicDataSource dataSource(
DataSource primaryDataSource,
DataSource secondaryDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("primary", primaryDataSource);
targetDataSources.put("secondary", secondaryDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(primaryDataSource);
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
}使用示例
1. 在Service层使用
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@DataSource("primary")
public User getPrimaryUser(Long id) {
return userMapper.selectById(id);
}
@DataSource("secondary")
public User getSecondaryUser(Long id) {
return userMapper.selectById(id);
}
// 在类上使用注解,对所有方法生效
@DataSource("primary")
@Service
public class OrderService {
// 所有方法默认使用primary数据源
}
}2. 实现读写分离
@Aspect
@Component
public class ReadWriteDataSourceAspect {
@Pointcut("execution(* com.example.service..*.select*(..))" +
"|| execution(* com.example.service..*.get*(..))" +
"|| execution(* com.example.service..*.find*(..))")
public void readPointcut() {}
@Pointcut("execution(* com.example.service..*.insert*(..))" +
"|| execution(* com.example.service..*.add*(..))" +
"|| execution(* com.example.service..*.update*(..))" +
"|| execution(* com.example.service..*.delete*(..))")
public void writePointcut() {}
@Before("readPointcut()")
public void setReadDataSource() {
DataSourceContextHolder.setDataSourceType("read");
}
@Before("writePointcut()")
public void setWriteDataSource() {
DataSourceContextHolder.setDataSourceType("write");
}
@After("readPointcut() || writePointcut()")
public void clearDataSource() {
DataSourceContextHolder.clearDataSourceType();
}
}最佳实践建议
合理使用注解:优先使用注解方式切换数据源,使代码更清晰。
注意事务:确保数据源切换在事务开启之前完成。
异常处理:在切换数据源时做好异常处理,确保 ThreadLocal 能够正确清理。
性能考虑:
- 避免频繁切换数据源
- 使用数据源连接池
- 合理设置连接池参数
常见问题
事务失效问题
// 错误示例 @Transactional @DataSource("secondary") public void wrongMethod() {} // 事务可能失效 // 正确示例 @DataSource("secondary") @Transactional public void correctMethod() {} // 确保数据源切换在事务开启前线程安全问题
- 使用 ThreadLocal 确保线程安全
- 注意在使用完后清理 ThreadLocal
数据源切换失效
- 检查 AOP 切面优先级
- 确保在事务开启前切换数据源
总结
动态数据源为我们提供了更灵活的数据源管理方案,主要应用场景包括:
- 读写分离
- 多租户系统
- 分库分表
- 故障转移
通过合理使用动态数据源,我们可以实现更灵活的数据库访问策略。

