Spring Boot多数据源、EntityManager与事务管理异常问题求助
兄弟,我之前折腾过几乎一模一样的场景——Spring Boot多数据源+共享DAO/Service+事务失效,结合你给的代码片段,问题大概率出在事务管理器没和动态数据源绑定上:你现在用@Primary标注了主数据源,但切换数据源后,事务管理器还是会默认绑定主数据源的EntityManagerFactory,不会跟着动态切换,导致切换后的数据源操作要么不在事务里,要么事务逻辑跑错了数据源,最终出现事务失效、数据不一致这类问题。
下面给你一步步拆解解决方案:
1. 先搞定动态数据源的核心路由类
首先需要一个能根据ThreadLocal上下文切换数据源的路由类,替代你现在的dataSource() Bean:
public class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { // 这里用ThreadLocal存储当前要使用的数据源名称 return DynamicDataSourceContextHolder.getDataSource(); } }
配套的ThreadLocal上下文工具类:
public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); // 设置当前数据源 public static void setDataSource(String dataSourceName) { CONTEXT_HOLDER.set(dataSourceName); } // 获取当前数据源 public static String getDataSource() { return CONTEXT_HOLDER.get(); } // 清除上下文,避免线程池复用导致的数据源混乱 public static void clearDataSource() { CONTEXT_HOLDER.remove(); } }
2. 为每个数据源创建独立的EntityManagerFactory
JPA的EntityManagerFactory是和数据源强绑定的,你不能只用一个Factory对应多个数据源。需要遍历你的toLoadDatasources列表,为每个数据源创建专属的EntityManagerFactory:
@Configuration @EnableJpaRepositories(basePackages = "org.foo") @EnableTransactionManagement public class DataSourceConfiguration { @Value("#{'${load.datasources}'.split(',')}") private List<String> toLoadDatasources; @Value("${default.datasource}") private String defaultDatasource; @Autowired private Environment env; @Bean @ConfigurationProperties("spring.jpa") public JpaProperties jpaProperties() { return new JpaProperties(); } // 存储所有数据源的Map,key为数据源名称 @Bean public Map<String, DataSource> dataSourceMap() { Map<String, DataSource> dataSourceMap = new HashMap<>(); for (String dsName : toLoadDatasources) { // 读取对应数据源的配置(假设你的配置是spring.datasource.{dsName}.xxx格式) DataSource dataSource = DataSourceBuilder.create() .driverClassName(env.getProperty("spring.datasource." + dsName + ".driver-class-name")) .url(env.getProperty("spring.datasource." + dsName + ".url")) .username(env.getProperty("spring.datasource." + dsName + ".username")) .password(env.getProperty("spring.datasource." + dsName + ".password")) .build(); dataSourceMap.put(dsName, dataSource); } return dataSourceMap; } // 动态数据源Bean,作为主数据源 @Bean @Primary public DataSource dynamicDataSource(Map<String, DataSource> dataSourceMap) { DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource(); dynamicDataSource.setDefaultTargetDataSource(dataSourceMap.get(defaultDatasource)); dynamicDataSource.setTargetDataSources(new HashMap<>(dataSourceMap)); // 初始化数据源路由 dynamicDataSource.afterPropertiesSet(); return dynamicDataSource; } // 存储所有EntityManagerFactory的Map,key为数据源名称 @Bean public Map<String, EntityManagerFactory> entityManagerFactoryMap(Map<String, DataSource> dataSourceMap, JpaProperties jpaProperties) { Map<String, EntityManagerFactory> emfMap = new HashMap<>(); LocalContainerEntityManagerFactoryBean factoryBean; HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); for (String dsName : toLoadDatasources) { factoryBean = new LocalContainerEntityManagerFactoryBean(); factoryBean.setDataSource(dataSourceMap.get(dsName)); factoryBean.setPackagesToScan("org.foo"); // 给每个持久化单元起唯一名字 factoryBean.setPersistenceUnitName(dsName + "-persistence-unit"); factoryBean.setJpaVendorAdapter(vendorAdapter); factoryBean.setJpaProperties(jpaProperties.getProperties()); factoryBean.afterPropertiesSet(); emfMap.put(dsName, factoryBean.getObject()); } return emfMap; } }
3. 自定义动态事务管理器,绑定EntityManagerFactoryMap
默认的JpaTransactionManager只会绑定一个EntityManagerFactory,我们需要重写它,让它能根据当前数据源名称获取对应的Factory:
public class DynamicJpaTransactionManager extends JpaTransactionManager { private Map<String, EntityManagerFactory> entityManagerFactoryMap; @Override protected EntityManagerFactory obtainEntityManagerFactory() { String currentDs = DynamicDataSourceContextHolder.getDataSource(); if (currentDs != null && entityManagerFactoryMap.containsKey(currentDs)) { return entityManagerFactoryMap.get(currentDs); } // 默认用主数据源的EntityManagerFactory return super.obtainEntityManagerFactory(); } // 注入EntityManagerFactoryMap public void setEntityManagerFactoryMap(Map<String, EntityManagerFactory> entityManagerFactoryMap) { this.entityManagerFactoryMap = entityManagerFactoryMap; } }
然后在配置类里注册这个事务管理器:
// 在DataSourceConfiguration里添加 @Bean @Primary public PlatformTransactionManager transactionManager(Map<String, EntityManagerFactory> entityManagerFactoryMap) { DynamicJpaTransactionManager transactionManager = new DynamicJpaTransactionManager(); transactionManager.setEntityManagerFactoryMap(entityManagerFactoryMap); // 设置默认的EntityManagerFactory transactionManager.setEntityManagerFactory(entityManagerFactoryMap.get(defaultDatasource)); return transactionManager; }
4. 用AOP实现数据源切换的自动绑定
写一个自定义注解,用来标记方法要使用的数据源,再配合AOP切面自动切换:
// 自定义数据源注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { String value(); // 数据源名称 }
AOP切面类:
@Aspect @Component public class DataSourceSwitchAspect { // 拦截所有标注了@DataSource的方法 @Before("@annotation(dataSource)") public void switchDataSource(JoinPoint point, DataSource dataSource) { String dsName = dataSource.value(); DynamicDataSourceContextHolder.setDataSource(dsName); } // 方法执行完后清除上下文 @After("@annotation(org.foo.annotation.DataSource)") public void restoreDataSource() { DynamicDataSourceContextHolder.clearDataSource(); } }
5. 验证事务是否生效
在你的Service方法上同时标注@DataSource和@Transactional,测试事务回滚:
@Service public class CustomerService { @Autowired private CustomerRepository customerRepository; @DataSource("client1") @Transactional(rollbackFor = Exception.class) public void createCustomer(Customer customer) { customerRepository.save(customer); // 故意抛出异常,验证是否回滚 if (true) { throw new RuntimeException("测试事务回滚"); } } }
执行后检查client1的数据库,如果没有新增数据,说明事务已经正常工作了。
关键注意点
- 不要在同一个
@Transactional方法里切换多个数据源:一个事务只能绑定一个EntityManagerFactory,强行切换会导致事务逻辑混乱。 - 线程池场景一定要清除ThreadLocal:如果你的项目用了线程池(比如@Async),必须保证方法执行完后调用
DynamicDataSourceContextHolder.clearDataSource(),否则线程复用会拿错数据源。 - 如果有多个事务管理器,可以在
@Transactional里指定transactionManager = "transactionManager"(对应你注册的Bean名称),不过我们已经用@Primary标记了动态事务管理器,所以不用指定也会优先使用它。
内容的提问来源于stack exchange,提问作者Walid Ammou

