题目
Spring框架中如何实现多数据源的动态切换,并保证事务的正确性?
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
多数据源配置, 动态数据源路由, 事务管理, AOP, AbstractRoutingDataSource
快速回答
实现多数据源动态切换的核心步骤:
- 继承
AbstractRoutingDataSource实现动态数据源路由 - 使用
ThreadLocal存储当前线程数据源标识 - 通过AOP切面在Service层方法执行前切换数据源
- 结合
@Transactional和TransactionManager管理事务 - 使用
ChainedTransactionManager或JTA解决跨数据源事务
1. 核心原理
Spring通过AbstractRoutingDataSource实现数据源路由抽象,其核心方法是determineCurrentLookupKey()。动态切换的关键在于:
- 使用
ThreadLocal保存当前线程的数据源标识(如:db1, db2) - 在方法执行前(通过AOP)设置标识,执行后清除
- 事务管理器需特殊处理以保证事务边界内数据源一致性
2. 代码实现示例
步骤1:定义动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
CONTEXT.set(key);
}
public static String getDataSourceKey() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}步骤2:配置多数据源Bean
@Bean
@Primary
public DataSource dynamicDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("db1", db1DataSource());
targetDataSources.put("db2", db2DataSource());
DynamicDataSource ds = new DynamicDataSource();
ds.setTargetDataSources(targetDataSources);
ds.setDefaultTargetDataSource(db1DataSource()); // 默认数据源
return ds;
}步骤3:实现AOP切面切换数据源
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(DataSourceSelector)")
public void switchDataSource(JoinPoint joinPoint) {
DataSourceSelector selector = ((MethodSignature) joinPoint.getSignature())
.getMethod().getAnnotation(DataSourceSelector.class);
DataSourceContextHolder.setDataSourceKey(selector.value());
}
@After("@annotation(DataSourceSelector)")
public void restoreDataSource() {
DataSourceContextHolder.clear();
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSelector {
String value();
}步骤4:配置事务管理器(关键难点)
// 方案1:链式事务管理器(非严格事务)
@Bean
public PlatformTransactionManager transactionManager() {
return new ChainedTransactionManager(
new JpaTransactionManager(entityManagerFactoryForDb1()),
new JpaTransactionManager(entityManagerFactoryForDb2())
);
}
// 方案2:JTA分布式事务(XA协议)
@Bean
public JtaTransactionManager transactionManager() {
return new JtaTransactionManager();
}3. 事务一致性保障方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| ChainedTransactionManager | 按顺序提交/回滚 | 实现简单 | 非原子操作,可能部分提交 |
| JTA (Atomikos) | XA两阶段提交 | 强一致性 | 性能损耗大,配置复杂 |
4. 常见错误与解决方案
- 错误1:事务内切换数据源失效
原因:Spring事务在创建Connection时缓存数据源
解决:在@Transactional方法外部切换数据源 - 错误2:ThreadLocal未清除导致内存泄漏
解决:在finally块中调用DataSourceContextHolder.clear() - 错误3:跨数据源更新无事务保障
解决:使用JTA或最终一致性补偿机制
5. 最佳实践
- 读写分离场景:主库默认,从库用注解切换
- 微服务架构:优先考虑分库分表而非多数据源
- 性能优化:对非事务查询使用
@DataSourceSelector - 监控:实现
DataSource包装类记录连接使用情况
6. 扩展知识
- Spring Boot 2.x+:使用
AbstractRoutingDataSource+@ConfigurationProperties简化配置 - MyBatis集成:通过
SqlSessionFactoryBean注入动态数据源 - 连接池选择:HikariCP在多数据源场景性能优于Druid
- 新方案:Spring Data R2DBC支持响应式多数据源