Spring Boot多数据源下事务不生效的底层机制与最佳解决方案

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.propertiesapplication.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: 为每个数据源配置对应的 EntityManagerFactoryTransactionManager

    @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 关联的数据源的事务。
  • 事务传播行为: 事务传播行为决定了当一个被 @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 配置中的 TransactionManager Bean。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 理论,根据业务需求做出决策。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注