Spring Boot 多数据源下事务失效的底层机制与最佳解决方案
大家好,今天我们来聊聊 Spring Boot 多数据源环境下事务失效的问题。这是一个在实际开发中经常遇到的难题,理解其背后的机制并掌握正确的解决方案至关重要。
1. 多数据源配置:基础铺垫
在 Spring Boot 中配置多数据源,一般需要以下步骤:
-
引入依赖: 首先,确保项目中引入了必要的数据库驱动包和 Spring Boot Data JPA 依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> -
配置数据源: 在
application.properties或application.yml中配置多个数据源的连接信息。例如:spring.datasource.primary.url=jdbc:mysql://localhost:3306/db1?serverTimezone=UTC&useSSL=false spring.datasource.primary.username=root spring.datasource.primary.password=password spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.secondary.url=jdbc:mysql://localhost:3306/db2?serverTimezone=UTC&useSSL=false spring.datasource.secondary.username=root spring.datasource.secondary.password=password spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver -
配置数据源 Bean: 创建多个
DataSource类型的 Bean,并使用@Primary注解指定默认数据源。@Configuration public class DataSourceConfig { @Bean(name = "primaryDataSource") @Primary @ConfigurationProperties("spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "secondaryDataSource") @ConfigurationProperties("spring.datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } } -
配置 EntityManagerFactory 和 TransactionManager: 为每个数据源配置对应的
EntityManagerFactory和TransactionManager。@Configuration @EnableTransactionManagement @EnableJpaRepositories( entityManagerFactoryRef = "primaryEntityManagerFactory", transactionManagerRef = "primaryTransactionManager", basePackages = {"com.example.repository.primary"} // 指定repository所在的包 ) public class PrimaryDataSourceConfig { @Autowired @Qualifier("primaryDataSource") private DataSource primaryDataSource; @Primary @Bean(name = "primaryEntityManagerFactory") public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(EntityManagerFactoryBuilder builder) { return builder .dataSource(primaryDataSource) .packages("com.example.domain.primary") // 指定实体类所在的包 .persistenceUnit("primaryPersistenceUnit") .build(); } @Primary @Bean(name = "primaryTransactionManager") public PlatformTransactionManager primaryTransactionManager( @Qualifier("primaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } } @Configuration @EnableTransactionManagement @EnableJpaRepositories( entityManagerFactoryRef = "secondaryEntityManagerFactory", transactionManagerRef = "secondaryTransactionManager", basePackages = {"com.example.repository.secondary"} // 指定repository所在的包 ) public class SecondaryDataSourceConfig { @Autowired @Qualifier("secondaryDataSource") private DataSource secondaryDataSource; @Bean(name = "secondaryEntityManagerFactory") public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(EntityManagerFactoryBuilder builder) { return builder .dataSource(secondaryDataSource) .packages("com.example.domain.secondary") // 指定实体类所在的包 .persistenceUnit("secondaryPersistenceUnit") .build(); } @Bean(name = "secondaryTransactionManager") public PlatformTransactionManager secondaryTransactionManager( @Qualifier("secondaryEntityManagerFactory") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } }
2. 事务失效的常见原因与底层机制
配置好多个数据源后,如果在跨多个数据源的操作中期望使用事务,你可能会发现事务并未生效。以下是常见的原因和相应的底层机制:
-
默认事务管理器: Spring Boot 默认使用
@Primary注解的数据源对应的TransactionManager。如果你的操作涉及多个数据源,但只配置了一个默认的事务管理器,那么只有默认数据源上的操作会被事务管理,其他数据源上的操作则不会。- 底层机制: Spring 的事务管理基于 AOP(面向切面编程)。当方法被
@Transactional注解标记时,Spring 会创建一个事务拦截器,在方法执行前后进行事务的开启、提交或回滚。如果使用了默认的TransactionManager,那么拦截器只会管理与该TransactionManager关联的数据源的事务。
- 底层机制: Spring 的事务管理基于 AOP(面向切面编程)。当方法被
-
事务传播行为: 事务传播行为决定了当一个被
@Transactional注解的方法被另一个被@Transactional注解的方法调用时,事务如何传播。如果传播行为配置不当,可能会导致某些数据源上的操作不在同一个事务中。传播行为 描述 REQUIRED如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 REQUIRES_NEW总是创建一个新的事务。如果当前存在事务,则将当前事务挂起,并在新的事务完成后恢复。 SUPPORTS如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。 NOT_SUPPORTED总是以非事务方式执行。如果当前存在事务,则将当前事务挂起。 MANDATORY如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 NEVER总是以非事务方式执行。如果当前存在事务,则抛出异常。 NESTED如果当前存在事务,则创建一个嵌套事务作为当前事务的子事务;如果当前没有事务,则创建一个新的事务。 - 底层机制: 不同的传播行为会影响事务拦截器的行为。例如,
REQUIRES_NEW会创建一个新的事务,而REQUIRED会尝试加入现有的事务。如果多个数据源的操作分别处于不同的事务中,那么它们之间就不会有原子性保证。
- 底层机制: 不同的传播行为会影响事务拦截器的行为。例如,
-
本地事务和分布式事务: 默认情况下,Spring 使用的是本地事务,即由单个数据库管理系统提供的事务。如果你的操作涉及多个数据库,那么本地事务无法保证跨数据库的原子性。
- 底层机制: 本地事务依赖于数据库的 ACID 特性。每个数据库连接都有自己的事务上下文。如果操作跨越多个数据库连接,那么每个连接上的操作都是独立的事务,无法统一提交或回滚。
-
未配置 JTA: 要实现跨多个数据库的分布式事务,需要使用 JTA (Java Transaction API)。如果项目中没有配置 JTA,那么 Spring 无法协调多个数据库之间的事务。
- 底层机制: JTA 提供了全局事务管理器,可以协调多个资源管理器(如数据库连接)之间的事务。Spring 可以通过 JTA 接口与全局事务管理器集成,从而实现分布式事务管理。
-
没有使用正确的注解: 事务注解需要应用在 Spring 管理的 Bean 上,并且是 public 方法。如果注解用在了 private 方法或者不是 Spring 管理的 Bean 上,事务不会生效。
3. 解决方案:分布式事务与 Atomikos
要解决多数据源下的事务问题,最常见的解决方案是使用分布式事务。JTA 是 JavaEE 提供的标准分布式事务解决方案。我们可以使用 Atomikos 这样的开源 JTA 事务管理器。
-
引入 Atomikos 依赖: 首先,在
pom.xml中引入 Atomikos 的依赖。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> </dependency> -
配置 Atomikos: Spring Boot 会自动配置 Atomikos,但为了更精细的控制,我们可以手动配置 Atomikos 的 Bean。
@Configuration @EnableTransactionManagement public class JtaConfig { @Bean public UserTransaction userTransaction() throws SystemException { UserTransactionImp userTransactionImp = new UserTransactionImp(); userTransactionImp.setTransactionTimeout(10000); return userTransactionImp; } @Bean public TransactionManager atomikosTransactionManager() throws Throwable { UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setForceShutdown(false); return userTransactionManager; } @Bean public JtaTransactionManager jtaTransactionManager() throws Throwable { UserTransaction userTransaction = userTransaction(); JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(userTransaction, atomikosTransactionManager()); return jtaTransactionManager; } } -
配置数据源: 使用 Atomikos 提供的
AtomikosDataSourceBean来配置数据源。注意,这里的配置与之前略有不同,需要指定xaDataSourceClassName。@Configuration public class DataSourceConfig { @Bean(name = "primaryDataSource") @Primary @ConfigurationProperties("spring.datasource.primary") public DataSource primaryDataSource() { AtomikosDataSourceBean ds = new AtomikosDataSourceBean(); ds.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource"); ds.setUniqueResourceName("primaryDataSource"); ds.setPoolSize(5); Properties xaProperties = new Properties(); xaProperties.setProperty("user", "root"); xaProperties.setProperty("password", "password"); xaProperties.setProperty("URL", "jdbc:mysql://localhost:3306/db1?serverTimezone=UTC&useSSL=false"); ds.setXaProperties(xaProperties); return ds; } @Bean(name = "secondaryDataSource") @ConfigurationProperties("spring.datasource.secondary") public DataSource secondaryDataSource() { AtomikosDataSourceBean ds = new AtomikosDataSourceBean(); ds.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource"); ds.setUniqueResourceName("secondaryDataSource"); ds.setPoolSize(5); Properties xaProperties = new Properties(); xaProperties.setProperty("user", "root"); xaProperties.setProperty("password", "password"); xaProperties.setProperty("URL", "jdbc:mysql://localhost:3306/db2?serverTimezone=UTC&useSSL=false"); ds.setXaProperties(xaProperties); return ds; } } -
移除 JPA 配置中的 TransactionManager: 在使用 Atomikos 的 JTA 事务管理器后,我们需要移除之前 JPA 配置中的
TransactionManagerBean。Spring Boot 会自动使用 JTA 事务管理器。同时,在EnableJpaRepositories注解中移除transactionManagerRef属性。@Configuration @EnableTransactionManagement @EnableJpaRepositories( entityManagerFactoryRef = "primaryEntityManagerFactory", basePackages = {"com.example.repository.primary"} ) public class PrimaryDataSourceConfig { } @Configuration @EnableTransactionManagement @EnableJpaRepositories( entityManagerFactoryRef = "secondaryEntityManagerFactory", basePackages = {"com.example.repository.secondary"} ) public class SecondaryDataSourceConfig { } -
使用
@Transactional注解: 现在,你可以在需要事务管理的方法上使用@Transactional注解,Spring 会自动使用 JTA 事务管理器来协调多个数据源之间的事务。@Service public class MyService { @Autowired private PrimaryRepository primaryRepository; @Autowired private SecondaryRepository secondaryRepository; @Transactional public void myTransactionalMethod() { PrimaryEntity primaryEntity = new PrimaryEntity(); primaryEntity.setName("primary"); primaryRepository.save(primaryEntity); SecondaryEntity secondaryEntity = new SecondaryEntity(); secondaryEntity.setName("secondary"); secondaryRepository.save(secondaryEntity); // 如果发生异常,两个数据库的操作都会回滚 if (true) { throw new RuntimeException("Transaction rollback"); } } }
4. 其他解决方案与考虑因素
除了 Atomikos,还有其他的分布式事务解决方案,例如 Bitronix、Narayana 和 Seata。选择哪种方案取决于你的具体需求和技术栈。
-
Bitronix: 另一个流行的开源 JTA 事务管理器,与 Atomikos 类似,但可能在配置和性能上有所差异。
-
Narayana: Red Hat 提供的开源 JTA 事务管理器,支持多种事务协议。
-
Seata: 阿里巴巴开源的分布式事务解决方案,提供高性能和易用性。Seata 主要针对微服务架构,并且支持多种事务模式,例如 AT (Automatic Transaction)、TCC (Try-Confirm-Cancel) 和 Saga。
-
两阶段提交(2PC): JTA 事务管理器通常使用两阶段提交协议来保证跨多个数据库的原子性。2PC 涉及到协调者和参与者两个角色。协调者负责协调事务的提交或回滚,参与者负责执行数据库操作。
- 准备阶段: 协调者向所有参与者发送准备请求,询问是否可以提交事务。参与者执行本地事务,并将事务状态写入日志。如果参与者可以提交事务,则回复“同意”;否则回复“不同意”。
- 提交阶段: 如果所有参与者都回复“同意”,则协调者向所有参与者发送提交请求。参与者提交本地事务,并释放资源。如果任何一个参与者回复“不同意”,则协调者向所有参与者发送回滚请求。参与者回滚本地事务,并释放资源。
-
最终一致性: 在某些场景下,严格的 ACID 特性可能不是必需的。你可以考虑使用最终一致性方案,例如 Saga 模式。Saga 模式将一个大的事务分解为多个小的本地事务,每个本地事务都提交到自己的数据库。如果任何一个本地事务失败,则通过补偿事务来回滚之前的操作。
-
XA 协议性能: JTA 通常使用 XA 协议来协调事务。XA 协议的性能相对较低,因为它涉及到多次网络通信和磁盘 I/O。在高性能要求的场景下,可以考虑使用其他事务模式,例如 AT 模式或 TCC 模式。
-
CAP 理论: 在分布式系统中,CAP 理论指出一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)三个特性最多只能同时满足两个。在选择分布式事务解决方案时,需要根据你的业务需求权衡这三个特性。
5. 代码示例:使用 Atomikos 和 JPA 进行跨数据源事务
以下是一个完整的示例,演示如何使用 Atomikos 和 JPA 进行跨数据源事务。
-
实体类(Primary):
@Entity @Table(name = "primary_entity") public class PrimaryEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // Getters and setters } -
实体类(Secondary):
@Entity @Table(name = "secondary_entity") public class SecondaryEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // Getters and setters } -
Repository(Primary):
public interface PrimaryRepository extends JpaRepository<PrimaryEntity, Long> { } -
Repository(Secondary):
public interface SecondaryRepository extends JpaRepository<SecondaryEntity, Long> { } -
Service:
@Service public class MyService { @Autowired private PrimaryRepository primaryRepository; @Autowired private SecondaryRepository secondaryRepository; @Transactional public void myTransactionalMethod() { PrimaryEntity primaryEntity = new PrimaryEntity(); primaryEntity.setName("primary"); primaryRepository.save(primaryEntity); SecondaryEntity secondaryEntity = new SecondaryEntity(); secondaryEntity.setName("secondary"); secondaryRepository.save(secondaryEntity); // 模拟异常,测试事务回滚 if (true) { throw new RuntimeException("Transaction rollback"); } } } -
Controller:
@RestController public class MyController { @Autowired private MyService myService; @GetMapping("/test") public String test() { try { myService.myTransactionalMethod(); return "Success"; } catch (Exception e) { return "Error: " + e.getMessage(); } } }
6. 总结一下核心要点
多数据源事务失效通常源于默认事务管理器、事务传播行为不当、缺乏 JTA 配置等问题。解决此类问题需引入分布式事务管理器(如 Atomikos),配置XA数据源,并使用 @Transactional 注解。选择合适的事务方案,需要权衡 ACID 特性、性能和 CAP 理论,根据业务需求做出决策。