Spring 事务机制

1. 引言

1.1 什么是事务

事务是由用户定义的一系列操作序列所组成的最小工作单元;这些操作要么全部完成,要么全部不完成,是一个不可分割的工作单元。常见于数据库中的并发控制和数据一致性处理场景。

1.2 事务的特性

事务具有以下特性:

  • 原子性(Atomicity):事务是由操作序列组成的不可分割的最小工作单元。
  • 一致性(Consistency):事务中操作的状态最终是一致的,要么全都完成,要么全都失败。
  • 隔离性(Isolation):事务与事务之间的操作与状态是隔离开的,事务只受到自身操作状态的影响。
  • 持久性(Durability):事务完成后对数据的修改是永久的,是长期不变的。
1.3 事务的基本概念

事务是一个并发控制单元,是用户定义的一个操作序列,这些操作要么全部完成,要么全部不完成,是一个不可分割的工作单元。事务有 ACID 四个特性即原子性(Atomicity)、一致性(Consistency)、事务隔离(Isolation)、持久性(Durability)。

1.4 为什么需要事务管理

事务管理的重要性在于它确保了数据的一致性和完整性,可以防止出现脏数据,防止数据库数据出现问题等。综上,事务管理具体作用总结如下:

  • 数据一致性:确保一系列操作要么全部成功,要么全部失败,避免数据处于不一致的状态。
  • 错误恢复:当事务中的某一步骤出错时,事务管理器可以回滚整个事务,撤销之前的所有更改。
  • 并发控制:在多用户环境中,多个事务可能同时运行。事务管理确保了这些事务之间的隔离性。
  • 资源锁定:事务管理通过锁定机制来控制对共享资源的访问。
  • 简化开发:事务管理可以帮助开发者简化代码,特别是通过 Spring 框架提供的声明式事务管理。
  • 业务逻辑保证:对于复杂的业务流程,事务可以确保所有相关的操作作为一个整体被正确执行。
  • 易于调试:事务管理提供了清晰的操作边界,有助于调试和定位问题。
  • 提升应用程序可靠性:通过确保数据的一致性和完整性,事务管理提高了应用程序的整体可靠性。
1.5 Spring 事务管理的优势

Spring 事务的本质是通过 Spring AOP 切面,在合适的地方开启事务,并在合适的地方提交事务或回滚事务,从而实现了业务编程层面的事务操作,具体有以下优势。

  • 统一的事务管理:Spring 提供了一种统一的方式来管理不同的持久层技术中的事务。
  • 声明式事务管理:Spring 允许通过 XML 配置或注解来声明事务边界,减少了编写显式事务管理代码的需求。
  • 编程式事务管理:对于需要更细粒度控制事务的应用程序,Spring 支持编程式的事务管理。
  • 异常传播机制:Spring 事务管理支持异常传播机制,可以根据异常类型自动决定是否回滚事务。
  • 事务隔离级别和传播行为:Spring 允许指定事务的隔离级别和传播行为,以提高应用的性能和一致性。
  • 事务超时:Spring 事务管理支持设置事务的超时时间,以防止长时间运行的事务导致的问题。
  • 事务回滚规则:Spring 支持配置事务的回滚规则,以避免不必要的数据损失。
  • 易于测试:Spring 事务管理的机制使得编写单元测试和集成测试变得更加容易。
  • 与 Spring AOP 的集成:事务管理可以无缝地与 Spring 的面向切面编程(AOP)集成。
  • 支持嵌套事务:Spring 支持嵌套事务

2. Spring 事务管理概述

2.1 编程式事务管理

编程式事务管理允许开发人员通过代码编程的方式控制事务的处理逻辑,即开始、提交和回滚。这种方式通常用于需要更细粒度控制事务的应用程序中,可以更灵活精准地自主控制事务。在 Spring 框架中,编程式事务管理是通过 TransactionTemplatePlatformTransactionManager 接口实现的。

2.2 声明式事务管理

声明式事务管理是 Spring 中最常用的事务管理方式,因为它能够以非侵入的方式集成到现有的应用程序中,减少代码量并提高可维护性。

2.2.1 原理

声明式事务管理的核心思想是将事务管理的责任从业务逻辑中抽离出来,由框架自动处理事务的开始、提交或回滚。这可以通过 XML 配置文件或者注解来实现。

2.2.2 核心组件
  • 事务管理器 (Transaction Manager):负责管理事务的生命周期,比如开启、提交或回滚事务。在 Spring 中,你可以使用不同的事务管理器,如 DataSourceTransactionManager 用于 JDBC 事务,HibernateTransactionManager 用于 Hibernate 事务等。
  • AOP (Aspect-Oriented Programming):Spring 利用 AOP 机制来实现在方法执行前后自动处理事务的逻辑。这通常通过使用 Spring 的代理机制来实现。
2.2.3 如何实现声明式事务管理
  1. 通过 XML 文件来配置事务管理器以及事务的属性

    1<!-- Spring 配置文件 -->
    2<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    3    <property name="dataSource" ref="dataSource"/>
    4</bean>
    
  2. 使用注解驱动 @Transactional 注解 在需要事务管理的方法或类上添加 @Transactional 注解。

    1@Service
    2public class UserService {
    3
    4    @Autowired
    5    private UserRepository userRepository;
    6
    7    @Transactional
    8    public void createUser(User user) {
    9        userRepository.save(user);
    10        // 其他业务逻辑...
    11    }
    12}
    

    在这个例子中,createUser 方法被标记为 @Transactional,这意味着该方法会在事务上下文中执行。如果该方法抛出未被捕获的异常,事务将会被回滚;否则,事务将在方法完成后提交。

  3. @Transactional 注解

    • 应用位置:可以应用于接口、接口方法、类或者类的方法上。
    • 默认行为
      • 如果在方法执行过程中抛出异常,则回滚事务。
      • 如果没有异常抛出,则在方法返回后提交事务。
    • 可配置属性
      • propagation:指定事务传播行为,默认值为 REQUIRED,表示如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。
      • isolation:指定事务隔离级别,默认值为 DEFAULT,即采用底层数据库系统的默认隔离级别。
      • readOnly:指定事务是否只读,默认值为 false,表示事务不是只读的。
      • rollbackFor:指定哪些类型的异常导致事务回滚
      • noRollbackFor:指定哪些类型的异常不导致事务回滚
2.3 事务传播行为 (Propagation)

事务传播行为定义了当一个方法调用另一个方法时,应该如何处理事务。Spring 支持七种不同的传播行为,其中最常见的有:

  • PROPAGATION_REQUIRED: 如果当前存在事务则加入该事务,否则创建一个新的事务。
  • PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务则挂起。
  • PROPAGATION_SUPPORTS: 如果当前存在事务则加入,否则以非事务的方式执行。
  • PROPAGATION_MANDATORY: 如果当前存在事务则加入,否则抛出异常。
  • PROPAGATION_NOT_SUPPORTED: 以非事务方式执行操作,如果当前存在事务则挂起。
  • PROPAGATION_NEVER: 以非事务方式执行,如果当前存在事务则抛出异常。
  • PROPAGATION_NESTED: 如果当前存在事务则创建一个嵌套事务,否则效果等同于 PROPAGATION_REQUIRED

示例:

1@Transactional(propagation = Propagation.REQUIRED)
2public void requiredMethod() {
3    // ...
4}
5
6@Transactional(propagation = Propagation.REQUIRES_NEW)
7public void requiresNewMethod() {
8    // ...
9}
2.4 事务隔离级别 (Isolation Levels)

事务隔离级别定义了事务之间是如何隔离的,Spring 支持四种不同的隔离级别:

  • ISOLATION_DEFAULT: 数据库默认的隔离级别。
  • ISOLATION_READ_UNCOMMITTED: 事务可以读取未提交的数据。
  • ISOLATION_READ_COMMITTED: 事务只能读取已提交的数据。
  • ISOLATION_REPEATABLE_READ: 对同一字段的多次读取将返回第一次读取的结果。
  • ISOLATION_SERIALIZABLE: 最严格的隔离级别,确保事务顺序执行。

示例:

1@Transactional(isolation = Isolation.REPEATABLE_READ)
2public void repeatableReadMethod() {
3    // ...
4}
2.5 事务只读属性

如果一个方法只是查询数据而不修改任何数据,可以设置 readOnly 属性为 true。这可以优化性能并减少锁的持有时间。

示例:

1@Transactional(readOnly = true)
2public List<Account> findAllAccounts() {
3    // ...
4}
2.6 自定义异常与回滚规则

默认情况下,如果一个方法抛出 RuntimeExceptionError,Spring 会自动回滚事务。你可以自定义异常以控制回滚策略。

示例:

1@Service
2public class MyTransactionalService {
3
4    @Transactional(rollbackFor = RuntimeException.class)
5    public void methodWithRollbackOnRuntimeException() {
6        // 模拟可能抛出 RuntimeException 的操作
7        throw new RuntimeException("这是一个运行时异常,导致事务回滚");
8    }
9
10    @Transactional(noRollbackFor = ArithmeticException.class)
11    public void methodWithNoRollbackOnArithmeticException() {
12        // 模拟可能抛出 ArithmeticException 的操作
13        int result = 1 / 0;
14    }
15}
  • methodWithRollbackOnRuntimeException 方法标注了 @Transactional ,并且指定了 rollbackFor = RuntimeException.class ,当方法内部抛出 RuntimeException 时,事务会回滚。
  • methodWithNoRollbackOnArithmeticException 方法标注了 @Transactional ,并且指定了 noRollbackFor = ArithmeticException.class ,当方法内部抛出 ArithmeticException 时,事务不会回滚。

3. 编程式事务管理

编程式事务管理允许开发者以编程方式控制事务的生命周期,包括开始、提交和回滚事务。这种方式通常用于需要更细粒度控制事务的行为或处理更复杂的事务逻辑的场景。

3.1 核心组件
  1. PlatformTransactionManager:这是一个接口,定义了事务管理的基本操作,包括开始事务、提交事务和回滚事务。Spring提供了多种实现,例如 DataSourceTransactionManager 用于JDBC事务,HibernateTransactionManager 用于Hibernate事务。
  2. TransactionDefinition:这是定义事务特性的接口,例如事务的传播行为、隔离级别等。
  3. TransactionStatus:这是描述事务状态的对象,包含了事务的状态信息,如是否新事务、是否已提交等。
3.2 示例
  • 使用 PlatformTransactionManager:

    @Autowired
    private PlatformTransactionManager transactionManager;
    
    public void testTransaction() {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            // 具体的业务操作
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
        }
    }
    
  • 使用 TransactionTemplate:

    @Autowired
    private TransactionTemplate transactionTemplate;
    
    public void testTransaction() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                try {
                    // 具体的业务操作
                } catch (Exception e) {
                    // 出现异常时回滚事务
                    transactionStatus.setRollbackOnly();
                }
            }
        });
    }
    
3.3 基本步骤

以下是使用编程式事务管理的基本步骤:

  1. 获取事务管理器:首先需要注入一个实现了 PlatformTransactionManager 接口的事务管理器。
  2. 定义事务属性:通过 TransactionDefinition 或者其子类来定义事务的属性,如传播行为、隔离级别等。
  3. 开始事务:使用事务管理器的 getTransaction 方法来开始一个新的事务。
  4. 执行业务逻辑:在事务的上下文中执行业务逻辑。
  5. 提交事务:如果业务逻辑执行成功,调用事务管理器的 commit 方法来提交事务。
  6. 回滚事务:如果业务逻辑执行过程中出现异常,调用事务管理器的 rollback 方法来回滚事务。
  • 示例代码展示

    public class AccountService {
    
        private final PlatformTransactionManager transactionManager;
        private final AccountRepository accountRepository;
    
        @Autowired
        public AccountService(PlatformTransactionManager transactionManager, AccountRepository accountRepository) {
            this.transactionManager = transactionManager;
            this.accountRepository = accountRepository;
        }
    
        public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
    
            // 1. 获取事务管理器:已经在构造函数中完成注入
    
            // 2. 定义事务属性
            DefaultTransactionDefinition def = new DefaultTransactionDefinition();
            def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 设置事务传播行为
    
            // 3. 开始事务
            TransactionStatus status = transactionManager.getTransaction(def);
    
            try {
                // 4. 执行业务逻辑
                Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new EmptyResultDataAccessException(1));
                Account toAccount = accountRepository.findById(toAccountId).orElseThrow(() -> new EmptyResultDataAccessException(1));
                fromAccount.withdraw(amount);
                toAccount.deposit(amount);
                accountRepository.save(fromAccount);
                accountRepository.save(toAccount);
    
                // 5. 提交事务
                transactionManager.commit(status);
            } catch (Exception e) {
                // 6. 回滚事务
                transactionManager.rollback(status);
                throw e; // 重新抛出异常,以便上层处理
            }
        }
    }
    
3.4 注意事项
  1. 异常处理:在事务方法中捕获异常并决定是否回滚事务非常重要。通常,如果方法中抛出了未被捕获的运行时异常 (RuntimeException),Spring会自动回滚事务;而对于检查异常(需要显式抛出的异常),则需要显式地调用 rollback 方法。
  2. 资源管理:确保在业务逻辑中正确管理资源,例如关闭数据库连接等,以避免资源泄露。
  3. 事务传播行为:了解并正确设置事务的传播行为,例如 PROPAGATION_REQUIRED 表示如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。

4. 声明式事务管理

声明式事务管理是Spring框架中一种常用的事务管理方式,它允许开发者通过配置而非编程的方式来控制事务的边界。这种方式使得事务管理更加简单和易于维护。

4.1 基本步骤
  1. 配置事务管理器

    • 确保已经配置了一个事务管理器,例如对于JDBC操作,可以使用DataSourceTransactionManager;对于JPA/Hibernate操作,则使用JpaTransactionManager
  2. 启用事务管理

    • 在Spring配置文件中启用基于注解的事务管理。可以通过XML配置或者Java配置来实现。
      • XML配置示例
        <tx:annotation-driven transaction-manager="transactionManager"/>
        
      • Java配置示例
        @Configuration
        @EnableTransactionManagement
        public class AppConfig {
            @Bean
            public PlatformTransactionManager transactionManager(DataSource dataSource) {
                return new DataSourceTransactionManager(dataSource);
            }
        }
        
  3. 标注事务方法

    • 使用@Transactional注解来标注需要进行事务管理的方法或者类。
    • 如果标注在类上,那么该类中的所有方法都会默认使用相同的事务属性。
    • 如果标注在方法上,那么只有被标注的方法才会受到事务的影响。
    • 可以通过@Transactional注解来指定事务的属性,比如传播行为、隔离级别等。
  4. 定义事务属性

    • 默认情况下,Spring会为标记了@Transactional的方法提供一些合理的默认值,例如PROPAGATION_REQUIRED作为传播行为。
    • 如果需要定制事务的行为,可以在@Transactional注解中指定相应的属性,例如:
      @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 30)
      public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
          // 业务逻辑
      }
      
  5. 测试事务边界

    • 编写单元测试来验证事务是否正确工作。例如,当发生异常时,事务应该回滚;如果没有异常,事务应该正常提交。
4.2 示例代码

下面是一个简单的示例代码,展示了如何使用声明式事务管理:

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.dao.EmptyResultDataAccessException;

@Service
public class AccountService {

    private final AccountRepository accountRepository;

    @Autowired
    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Transactional
    public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new EmptyResultDataAccessException(1));
        Account toAccount = accountRepository.findById(toAccountId).orElseThrow(() => new EmptyResultDataAccessException(1));

        fromAccount.withdraw(amount);
        toAccount.deposit(amount);

        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
    }
}

在这个例子中,transferMoney方法被@Transactional注解所标记,这意味着整个方法将被包含在一个数据库事务中。如果方法中抛出了未被捕获的异常,那么事务将会自动回滚。如果没有异常发生,事务会在方法结束时自动提交。这样,不需要手动管理事务的开始、提交或回滚,这使得代码更简洁并且减少了出错的可能性。

4.3 基础项目示例
  1. 项目构建

    • 数据库表构建

      CREATE TABLE `tablea` (
        `id` int NOT NULL AUTO_INCREMENT,
        `name` varchar(45) DEFAULT NULL,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB AUTO_INCREMENT=1;
       
      CREATE TABLE `tableb` (
        `id` int NOT NULL AUTO_INCREMENT,
        `name` varchar(45) DEFAULT NULL,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB AUTO_INCREMENT=1;
      
    • 实体定义

      @Data
      public class TableEntity {
          private static final long serialVersionUID = 1L;
       
          private Long id;
       
          private String name;
       
          public TableEntity() {
          }
       
          public TableEntity(String name) {
              this.name = name;
          }
      }
      
    • 项目依赖

      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
      </dependency>
      <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
          <version>2.1.0</version>
      </dependency>
      
    • Application.yml配置

      server:
        port: 8080
       
      spring:
        datasource:
          url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver
        profiles:
          active: dev
       
      # MyBatis 配置
      mybatis:
        type-aliases-package: com.noob.framework.transaction.entity
        configuration:
          map-underscore-to-camel-case: true
      
  2. 项目代码构建

    • Mapper接口

      @Mapper
      public interface TableMapper {
          @Insert("INSERT INTO tablea(id, name) VALUES(#{id}, #{name})")
          @Options(useGeneratedKeys = true, keyProperty = "id")
          void insertTableA(TableEntity tableEntity);
       
          @Insert("INSERT INTO tableb(id, name) VALUES(#{id}, #{name})")
          @Options(useGeneratedKeys = true, keyProperty = "id")
          void insertTableB(TableEntity tableEntity);
      }
      
    • Service类

      @Service
      public class TransactionServiceA {
          @Autowired
          private TableMapper tableMapper;
       
          @Autowired
          private TransactionServiceB transactionServiceB;
       
          public void methodA() {
              System.out.println("methodA 执行 插入数据");
              tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
              transactionServiceB.methodB();
          }
      }
      
      @Service
      public class TransactionServiceB {
          @Autowired
          private TableMapper tableMapper;
       
          @Transactional(propagation = Propagation.NESTED)
          public void methodB() {
              System.out.println("methodB 执行 插入数据");
              tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
          }
      }
      
    • Controller类

      @SpringBootApplication
      @RestController
      @RequestMapping("/api")
      public class SpringTransactionController {
          @Autowired
          private TransactionServiceA transactionServiceA;
       
          @RequestMapping("/spring-transaction")
          public String testTransaction() {
              transactionServiceA.methodA();
              return "SUCCESS";
          }
      }
      
  3. 异常处理

    上述案例的处理逻辑是调用方法完成两个表的数据插入,现模拟其中一条数据插入失败查看具体的体现。原处理逻辑是先插入表A后插入表B,现模拟表B插入失败,查看结果。

    public class TransactionServiceB {
        @Autowired
        private TableMapper tableMapper;
    
        public void methodB() {
            System.out.println("methodB 执行 插入数据");
            tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
            // 模拟业务处理异常
            throw new RuntimeException();
        }
    }
    

    预期状态是两条数据要么都成功要么都不成功。当插入B数据触发业务异常时,期望的结果应该是A和B数据都没有插入成功。然而,目前的操作结果是A和B数据都插入成功了,因为插入语句在前所以先执行了插入操作,在后续的业务逻辑触发异常并没有让这个操作"撤销",导致不符合预期。

    引入Spring事务,通过@Transactional注解来完成事务控制,此时在TransactionServiceATransactionServiceBmethod方法中都加上@Transactional注解,让Spring接手事务控制,然后再次查看测试效果。

    @Transactional
    public void methodA() {
        System.out.println("methodA 执行 插入数据");
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        transactionServiceB.methodB();
    }
    
    @Transactional
    public void methodB() {
        System.out.println("methodB 执行 插入数据");
        tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        // 模拟业务处理异常
        throw new RuntimeException();
    }
    

    访问测试,异常正常触发,但是发现数据库中两个表都没有插入数据,说明事务控制生效了,也符合业务场景。

4.4 @Transactional注解详解
  • 作用(修饰)范围

    • @Transactional作用在类上,表示该注解对该类中所有的 public 方法都生效。
    • @Transactional作用在 public 方法上(推荐)。
    • 不建议作用于接口上。
  • 常用配置参数参考

    属性名说明
    propagation事务的传播行为,默认值为 REQUIRED
    isolation事务的隔离级别,默认值采用 DEFAULT
    timeout事务的超时时间,默认值为 -1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务
    readOnly指定事务是否为只读事务,默认值为 false
    rollbackFor用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型
  • @Transactional注解原理

    • @Transactional的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理;如果目标对象没有实现接口,则会使用 CGLIB 动态代理。
    • 如果一个类或者一个类中的 public 方法上被标注@Transactional注解的话,Spring容器会在启动的时候为其创建一个代理类,在调用被@Transactional注解的 public 方法的时候,实际调用的是TransactionInterceptor类中的invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。

5. 事务边界与嵌套事务

5.1 事务边界

事务边界指的是一个事务的开始和结束范围。在数据库中,一个事务通常包括一系列的操作,这些操作作为一个整体要么全部成功,要么全部失败。在Spring中,你可以使用@Transactional注解来定义一个方法的事务边界。

5.1.1 示例

假设你有这样一个服务方法:

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
        Account toAccount = accountRepository.findById(toAccountId).orElseThrow(() -> new RuntimeException("Account not found"));

        fromAccount.withdraw(amount);
        toAccount.deposit(amount);

        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
    }
}

在这个例子中,transferMoney方法被@Transactional注解标记,这意味着整个方法执行过程都处于一个事务中。如果在方法执行过程中发生了任何未被捕获的异常,事务会被回滚;如果没有异常,事务会在方法完成后提交。

5.2 嵌套事务

嵌套事务是指在一个事务中启动另一个事务的情况。在传统的编程式事务管理中,你可能需要显式地开始和结束事务,因此嵌套事务的处理需要特别注意。但在Spring的声明式事务管理中,嵌套事务的处理变得更加容易,因为Spring提供了对嵌套事务的支持。

5.2.1 嵌套事务的行为

默认情况下,在Spring中使用@Transactional注解时,如果一个被注解的方法被另一个也带有@Transactional注解的方法调用,那么Spring会识别这种嵌套情况,并且不会为内部方法创建一个新的事务实例,而是将内部方法纳入外部方法的事务中。

然而,如果你想要改变这种行为,可以通过设置@Transactional注解中的propagation属性来实现。

5.2.2 示例

假设你有这样一个服务类:

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void processTransfer(Long fromAccountId, Long toAccountId, double amount) {
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
        Account toAccount = accountRepository.findById(toAccountId).orElseThrow(() -> new RuntimeException("Account not found"));

        fromAccount.withdraw(amount);
        toAccount.deposit(amount);

        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);

        // 假设这里要调用另一个事务方法
        validateAccountBalance(fromAccount, toAccount);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void validateAccountBalance(Account fromAccount, Account toAccount) {
        // 这里执行一些验证逻辑
        if (fromAccount.getBalance() < 0) {
            throw new RuntimeException("Insufficient funds");
        }
    }
}

在这个例子中,processTransfer方法被@Transactional注解标记,并且它调用了另一个方法validateAccountBalance,后者也被@Transactional注解标记。由于validateAccountBalance方法的@Transactional注解设置了propagation = Propagation.REQUIRES_NEW,这意味着它会在一个新事务中运行,即使它是在另一个事务中被调用的。如果validateAccountBalance方法抛出了异常,那么这个新的事务会被回滚,但是processTransfer方法所在的外部事务仍然会继续执行,除非它也抛出了异常。

5.2.3 注意事项
  • 当使用Propagation.REQUIRES_NEW时,如果内部事务抛出异常导致回滚,外部事务不受影响,除非它自己也抛出了异常。
  • 如果你需要确保内部事务的回滚会导致外部事务也回滚,你可以使用Propagation.NESTED。在这种情况下,内部事务的回滚会触发一个保存点(savepoint),这样如果内部事务回滚,外部事务可以恢复到保存点的状态。
5.3 处理嵌套事务的方法

处理嵌套事务的方法主要依赖于Spring框架提供的事务传播行为(Propagation Behavior)。Spring支持多种类型的传播行为,其中包括用于处理嵌套事务的Propagation.NESTED

事务传播行为

(重复一下加深印象就当巩固了)在Spring中,当你使用@Transactional注解时,你可以通过propagation属性来指定一个事务方法应该如何与另一个事务方法交互。以下是几种常见的传播行为:

  1. Propagation.REQUIRED:这是默认值,如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。
  2. Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续执行。
  3. Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
  4. Propagation.REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务,并且在新事务的上下文中执行。
  5. Propagation.NOT_SUPPORTED:如果当前存在事务,则挂起该事务;如果当前不存在事务,则以非事务的方式继续执行。
  6. Propagation.NEVER:如果当前存在事务,则抛出异常;如果当前不存在事务,则以非事务的方式继续执行。
  7. Propagation.NESTED:如果当前存在事务,则创建一个嵌套事务作为当前事务的保存点;如果当前不存在事务,则该行为等同于Propagation.REQUIRED

处理嵌套事务

当你需要在已有的事务中执行另一个事务时,通常会选择Propagation.REQUIRES_NEWPropagation.NESTED

5.3.1 使用Propagation.REQUIRES_NEW

当你使用Propagation.REQUIRES_NEW时,无论当前是否已经有事务,都会创建一个新的事务。如果内部事务失败并回滚,这不会影响外部事务的结果。例如:

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
        // 外部事务
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
        Account toAccount = accountRepository.findById(toAccountId).orElseThrow(() -> new RuntimeException("Account not found"));

        fromAccount.withdraw(amount);
        toAccount.deposit(amount);

        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);

        // 内部事务
        validateAccountBalance(fromAccount, toAccount);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void validateAccountBalance(Account fromAccount, Account toAccount) {
        if (fromAccount.getBalance() < 0) {
            throw new RuntimeException("Insufficient funds");
        }
    }
}
5.3.2 使用Propagation.NESTED

当你使用Propagation.NESTED时,如果当前已经存在事务,则会在当前事务中创建一个嵌套事务。嵌套事务的回滚不会影响外部事务,除非嵌套事务中抛出了RollbackException。这种方式允许你更细粒度地控制事务边界。例如:

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
        // 外部事务
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
        Account toAccount = accountRepository.findById(toAccountId).orElseThrow(() -> new RuntimeException("Account not found"));

        fromAccount.withdraw(amount);
        toAccount.deposit(amount);

        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);

        // 内部事务
        validateAccountBalance(fromAccount, toAccount);
    }

    @Transactional(propagation = Propagation.NESTED)
    public void validateAccountBalance(Account fromAccount, Account toAccount) {
        if (fromAccount.getBalance() < 0) {
            throw new RollbackException("Insufficient funds");
        }
    }
}

小结

  • Propagation.REQUIRES_NEW总是创建一个新的事务,无论当前是否存在事务,但内部事务的回滚不会影响外部事务。
  • Propagation.NESTED创建一个嵌套事务,如果当前存在事务,则使用保存点机制,允许更细粒度的控制,内部事务的回滚可以通过抛出RollbackException来影响外部事务。

这两种方法都有其适用场景,选择哪一种取决于你的业务需求。如果内部事务的回滚不应该影响外部事务,可以使用Propagation.REQUIRES_NEW;如果需要更细粒度的控制,则可以考虑使用Propagation.NESTED

5.4 示例代码:如何控制内部方法调用的事务边界
5.4.1 问题说明

如果你在一个被@Transactional注解的方法中调用同一个类中的另一个被@Transactional注解的方法,那么默认情况下这两个方法不会处于同一个事务中。这是因为Spring AOP是基于代理模式实现的,而这种调用方式实际上是在同一对象实例中发生的,而不是通过代理。

5.4.2 解决方案

要确保方法间的调用仍然遵循事务边界,你需要确保所有的方法调用都是通过代理进行的。通常的做法有两种:

  1. 使用代理对象进行方法调用

    在类内部不要直接调用方法,而是通过注入该类的引用进行调用。

    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    public class AccountServiceImpl implements AccountService {
    
        private final AccountRepository accountRepository;
        private final AccountService accountService;//注入自己
    
        public AccountServiceImpl(AccountRepository accountRepository, AccountService accountService) {
            this.accountRepository = accountRepository;
            this.accountService = accountService;
        }
    
        @Override
        @Transactional
        public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
            Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
            fromAccount.withdraw(amount);
    
            accountService.depositToAnotherAccount(toAccountId, amount);
        }
    
        @Override
        @Transactional
        public void depositToAnotherAccount(Long accountId, double amount) {
            Account account = accountRepository.findById(accountId).orElseThrow(() -> new RuntimeException("Account not found"));
            account.deposit(amount);
            accountRepository.save(account);
        }
    }
    

    这里我们通过注入AccountService的引用来进行调用,这样即使是在同一个类内,也会触发Spring的事务管理机制。

  2. 使用@Transactional(propagation = Propagation.REQUIRES_NEW)

    如果需要在现有事务中开启新的事务,则可以在子方法上使用这个注解来显式指定传播行为。

    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    public class AccountService {
    
        private final AccountRepository accountRepository;
    
        public AccountService(AccountRepository accountRepository) {
            this.accountRepository = accountRepository;
        }
    
        @Transactional
        public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
            Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
            fromAccount.withdraw(amount);
    
            // 开启新的事务来执行存款操作
            depositToAnotherAccount(toAccountId, amount);
        }
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void depositToAnotherAccount(Long accountId, double amount) {
            Account account = accountRepository.findById(accountId).orElseThrow(() => new RuntimeException("Account not found"));
            account.deposit(amount);
            accountRepository.save(account);
        }
    }
    

    在这个例子中,depositToAnotherAccount方法使用了@Transactional(propagation = Propagation.REQUIRES_NEW)注解。这意味着不管当前是否存在事务,该方法都将启动一个新的事务。

5.4 嵌套事务的传播行为PROPAGATION_NESTED

当使用PROPAGATION_NESTED时,如果一个方法调用另一个方法,而这两个方法都标注了@Transactional,那么第二个方法将在一个嵌套事务中运行。这意味着:

  • 第二个事务将获得一个保存点(savepoint),这样即使第二个事务回滚,第一个事务仍然可以提交。
  • 如果外部事务失败,所有嵌套事务都将回滚。
  • 如果内部事务失败,它将回滚到它的保存点,但不会影响外部事务的继续执行。
  • 内部事务可以独立于外部事务提交或回滚。
5.4.1 示例代码

假设有一个AccountService类,其中包含两个方法:transferMoneydoSomethingElse。我们将使用PROPAGATION_NESTED来展示嵌套事务的行为。

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountService {

    private final AccountRepository accountRepository;

    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    /**
     * 从一个账户向另一个账户转账。
     *
     * @param fromAccountId 转出账户的ID
     * @param toAccountId   存入账户的ID
     * @param amount        转账的金额
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
        fromAccount.withdraw(amount);

        // 使用嵌套事务调用 doSomethingElse()
        doSomethingElse(fromAccountId, toAccountId, amount);
    }

    /**
     * 执行一些额外的操作。
     *
     * @param fromAccountId 转出账户的ID
     * @param toAccountId   存入账户的ID
     * @param amount        转账的金额
     */
    @Transactional(propagation = Propagation.NESTED)
    public void doSomethingElse(Long fromAccountId, Long toAccountId, double amount) {
        Account toAccount = accountRepository.findById(toAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
        toAccount.deposit(amount);

        // 如果这里发生异常,只会影响 doSomethingElse() 方法,而不会影响外部的 transferMoney() 方法
        if (amount > 1000) {
            throw new RuntimeException("Amount too large");
        }
    }
}
5.4.2 小结

在上面的例子中,如果doSomethingElse方法抛出异常,它只会回滚到其开始时设置的保存点,而transferMoney方法的事务仍然可以正常提交。如果transferMoney方法本身抛出异常,则整个事务都会回滚,包括doSomethingElse中所做的更改。

使用PROPAGATION_NESTED可以让你在不破坏外部事务的情况下处理内部逻辑中的错误情况。这对于那些需要在同一个事务中执行多个操作的场景非常有用。

以上就是关于如何控制内部方法调用的事务边界以及如何使用嵌套事务的示例代码和解释。

6. 最佳实践与常见问题

6.1 何时使用编程式事务

编程式事务管理提供了对事务的细粒度控制,适用于以下情况:

6.1.1. 复杂的事务逻辑
  • 多数据源操作: 当需要在一个事务中操作多个数据源时,编程式事务管理可以更好地控制事务边界。
  • 复杂的业务逻辑: 如果业务逻辑非常复杂,需要在事务中执行多个步骤,并且这些步骤之间有特定的顺序依赖关系,使用编程式事务管理可以更灵活地控制事务。
6.1.2. 事务嵌套
  • 嵌套事务: 当需要在现有事务中创建嵌套事务时,编程式事务管理可以提供更精细的控制。
6.1.3. 异常处理
  • 自定义异常处理: 如果需要对异常进行更复杂的处理,例如根据不同的异常类型采取不同的策略(如回滚、提交或重试),编程式事务管理可以提供更灵活的异常处理机制。
6.1.4. 事务的显式控制
  • 显式事务管理: 在某些情况下,你可能需要显式地控制事务的开始和结束,例如在循环中处理大量数据,每处理一定数量的数据后提交一次事务。
6.1.5. 与其他框架集成
  • 与其他框架的集成: 当与其他框架集成时,如消息队列、事件驱动系统等,可能需要在特定的点开始或结束事务,这时使用编程式事务管理更为合适。
6.1.6. 测试和调试
  • 测试和调试: 在编写单元测试或进行调试时,有时需要手动控制事务的边界,以便更好地模拟特定的场景。
6.2 何时使用声明式事务

声明式事务管理简化了事务管理逻辑,适用于以下情况:

6.2.1. 简单的业务逻辑
  • 单一数据库操作: 对于简单的 CRUD 操作,声明式事务管理可以很好地满足需求。
  • 单一方法: 如果一个方法只需执行一些简单的业务逻辑,并且这些逻辑最好在一个事务中完成,那么声明式事务管理非常适合。
6.2.2. 易于维护
  • 代码整洁: 由于不需要显式地编写事务管理代码,声明式事务可以使代码更加清晰和易于维护。
  • 可读性: 通过注解和配置文件,事务管理逻辑与业务逻辑分离,提高了代码的可读性。
6.2.3. 快速开发
  • 减少样板代码: 声明式事务管理减少了需要编写的样板代码量,加快了开发速度。
6.2.4. 单一事务范围
  • 单一数据源: 当你的应用只涉及一个数据源时,声明式事务管理可以提供足够的事务控制。
6.2.5. 易于配置
  • 配置简单: 声明式事务管理可以通过简单的配置或注解来启用,而无需大量的编程式事务管理代码。
6.2.6. 自动化事务管理
  • 自动化: 声明式事务管理自动处理事务的开始、提交和回滚,减轻了开发者的工作负担。
6.2.7. 非嵌套事务
  • 非嵌套事务: 当不需要在现有事务中创建嵌套事务时,声明式事务管理是首选。
6.3 事务管理中的常见陷阱

事务管理是确保数据一致性和完整性的关键部分,以下是一些常见的陷阱及解决方案:

6.3.1. 未正确使用事务隔离级别
  • 问题: 不同的事务隔离级别可以防止不同程度的数据不一致性问题(如脏读、不可重复读等),但选择不当可能会导致性能下降或数据不一致。
  • 解决方案: 根据应用程序的需求合理设置事务隔离级别。
6.3.2. 过长的事务
  • 问题: 事务持续时间过长可能导致锁竞争加剧、死锁、资源占用增加等问题。
  • 解决方案: 尽可能缩短事务的生命周期。
6.3.3. 锁竞争和死锁
  • 问题: 多个事务试图获取相同的资源锁时可能会导致锁竞争甚至死锁。
  • 解决方案: 使用悲观锁或乐观锁减少锁的竞争。
6.3.4. 不正确的异常处理
  • 问题: 忽略异常或不正确地处理异常可能导致事务没有正确回滚。
  • 解决方案: 确保所有异常都被正确捕获,并在捕获异常后回滚事务。
6.3.5. 未关闭的连接和资源泄露
  • 问题: 数据库连接和其他资源如果没有正确关闭,可能会导致资源泄露。
  • 解决方案: 使用 try-with-resources 语句来自动关闭资源。
6.3.6. 不正确的事务传播行为
  • 问题: 当方法被其他带有 @Transactional 注解的方法调用时,如果不正确配置传播行为,可能导致嵌套事务问题。
  • 解决方案: 使用 @Transactional(propagation = Propagation.REQUIRED) 明确指定事务的传播行为。
6.3.7. 异步调用中的事务丢失
  • 问题: 当事务内的方法异步调用时,可能会丢失事务上下文。
  • 解决方案: 使用 Spring 的 @Async@Transactional 一起工作时要特别注意,确保异步方法也在事务范围内。
6.3.8. 缺乏测试
  • 问题: 事务管理逻辑往往比较复杂,缺乏充分的测试可能导致难以发现的问题。
  • 解决方案: 实施单元测试和集成测试,特别是针对事务边界和异常路径的测试。
6.3.9. 不一致的异常抛出
  • 问题: 如果业务层抛出运行时异常而不是受检异常,这可能会导致事务管理框架无法正确回滚事务。
  • 解决方案: 业务层抛出的异常应该被声明为 RuntimeException 或者继承自 RuntimeException
6.3.10. 依赖外部资源
  • 问题: 当事务中涉及到外部资源时,如远程服务调用或消息队列,这些操作通常不在本地事务管理范围内。
  • 解决方案: 使用 XA 两阶段提交协议来协调分布式事务。
6.4 如何调试事务问题

调试事务问题是软件开发中的一项重要技能,以下是一些调试事务问题的策略和技巧:

6.4.1. 日志记录
  • 详细日志: 确保应用有详细的日志记录,尤其是在事务的状态信息方面。
  • 日志级别: 调整日志级别,以便在调试期间输出更多的调试信息。
6.4.2. 使用调试器
  • 断点: 在关键位置设置断点,观察事务的状态和数据的变化。
  • 跟踪: 使用调试器跟踪方法调用,了解事务的传播行为。
6.4.3. 单元测试
  • 模拟环境: 使用单元测试来模拟事务中的关键路径。
  • 异常测试: 测试异常路径,确保事务正确回滚。
6.4.4. 分布式事务跟踪
  • 跟踪 ID: 使用全局唯一的跟踪 ID 来标识事务。
  • 日志关联: 在日志记录中包含跟踪 ID。
6.4.5. 数据库监控
  • SQL 语句: 查看数据库执行的 SQL 语句。
  • 事务状态: 监控数据库中的事务状态。
  • 性能监控: 使用数据库监控工具查看事务的性能指标。
6.4.6. 分析异常
  • 异常类型: 确定抛出的异常类型,理解异常的原因。
  • 异常处理: 检查异常处理逻辑是否正确。
6.4.7. 代码审查
  • 逻辑审查: 仔细审查事务相关的代码逻辑。
  • 依赖审查: 检查方法之间的依赖关系。
6.4.8. 使用事务管理器的诊断工具
  • Spring Boot Actuator: 如果使用 Spring Boot,可以利用 Spring Boot Actuator 的 actuator/healthactuator/trace 端点来获取有关事务的信息。

示例:使用 Spring Boot Actuator 进行事务诊断

如果你使用 Spring Boot 并启用了 Actuator,可以通过访问 /actuator/trace 端点来获取事务的跟踪信息。这可以帮助你了解事务的执行流程和状态。

curl http://localhost:8080/actuator/trace

这将返回一个 JSON 对象,包含了最近几次请求的跟踪信息,包括事务的开始、提交或回滚等。

6.5 事务与服务层设计模式

事务管理与服务层设计模式密切相关,以下是一些与事务管理相关的服务层设计模式:

6.5.1. 服务层设计模式与事务管理
  • Repository 模式: Repository 模式封装了对数据的访问,并且通常在 Repository 层中进行事务管理。
  • Service 模式: Service 层负责业务逻辑的实现,并可以使用声明式事务管理来控制业务逻辑的事务边界。
  • Transaction Script 模式: Transaction Script 是一种简单的服务层模式,其中每个业务操作都封装在一个单独的事务脚本中,这种方式可以很好地配合声明式事务管理。
6.5.2. 事务与领域驱动设计 (DDD)
  • 领域服务: 领域服务可以用来封装复杂的业务规则,并且可以使用声明式事务管理来确保这些规则在一个事务中执行。
  • 聚合根: 聚合根通常是领域模型中的核心实体,它们通常会涉及到多个操作,这些操作需要在一个事务中完成以保持数据的一致性。
  • 命令模式: 命令模式可以用于封装业务操作,并且可以在命令的执行过程中使用事务管理。
6.5.3. 事务与 CQRS 架构
  • 命令查询职责分离 (CQRS): 在 CQRS 架构中,写操作通常在一个事务中完成,而读操作则独立于写操作,这种分离可以优化事务管理。

通过结合这些设计模式和架构原则,可以构建出既高效又可靠的事务管理系统。

7. 高级主题

7.1. 批量事务处理

批量事务处理是指在一个事务中处理多条记录或者多个操作的过程。这种处理方式可以提高系统的性能和效率,尤其是在处理大量数据时。在数据库操作中,批量处理通常涉及多个插入、更新或删除操作。下面是一些关于批量事务处理的关键点:

7.1.1. 批量处理的好处
  • 减少网络通信开销:批量处理可以减少客户端与服务器之间的网络往返次数,从而降低网络延迟。
  • 减少锁定时间:对于某些类型的数据库系统,批量处理可以减少锁定的时间,从而提高并发性。
  • 提高性能:减少数据库的日志记录和磁盘 I/O 操作次数,从而提高整体性能。
7.1.2. 批量处理的挑战
  • 回滚和恢复:如果批量操作的一部分失败,可能需要回滚整个事务,这可能会导致数据丢失或不一致性。
  • 资源管理:大量的数据操作可能会消耗更多的内存和 CPU 资源。
  • 错误处理:需要处理可能出现的各种错误情况,包括部分成功的情况。
7.1.3. 批量事务处理的实现方法

JDBC 批量更新

在 Java 中使用 JDBC 进行批量更新可以通过以下步骤实现:

  1. 开启一个事务。
  2. 对每个 SQL 语句调用 addBatch() 方法。
  3. 调用 executeBatch() 方法执行所有 SQL 语句。
  4. 提交或回滚事务。

ORM 框架支持

  • HibernateJPA 支持批量处理,可以通过设置批量大小 (batchSize) 或者使用 EntityManagercreateNativeQuery 方法结合 addBatchexecuteBatch

  • Spring Data JPA 可以利用 @Transactional 注解和 JpaRepository 的原生 SQL 查询能力来实现批量处理。

7.1.4. 批量插入
  • 在 SQL 语句中使用单个 INSERT 语句插入多行数据,例如 INSERT INTO table (column1, column2) VALUES (value1, value2), (value3, value4);

示例代码

下面是一个使用 Hibernate 进行批量处理的示例:

import org.hibernate.Session;
import org.hibernate.Transaction;

public class BatchProcessingExample {

    public static void main(String[] args) {
        Session session = HibernateUtil.getSessionFactory().openSession();
        Transaction tx = null;
        
        try {
            tx = session.beginTransaction();

            for (int i = 0; i < 1000; i++) {
                User user = new User();
                user.setName("User" + i);
                session.save(user);
                
                // 如果达到批量大小,则立即执行批量操作
                if ((i + 1) % 50 == 0) {
                    session.flush();
                    session.clear();
                }
            }

            tx.commit();
        } catch (Exception e) {
            if (tx != null) {
                tx.rollback();
            }
            e.printStackTrace();
        } finally {
            session.close();
        }
    }
}
7.1.5. 性能优化技巧
  • 使用批处理:通过增加批量大小来减少网络通信次数。
  • 使用连接池:复用数据库连接可以减少建立连接和断开连接的开销。
  • 减少锁等待时间:使用乐观锁或者合适的隔离级别来减少锁竞争。
  • 预编译 SQL 语句:对于频繁使用的 SQL 语句,可以使用预编译语句来提高性能。
7.1.6. 错误处理
  • 在批量处理过程中,需要特别注意异常处理。例如,在上述示例中,如果在保存用户的过程中出现任何异常,事务将被回滚,保证数据的一致性。
  • 对于部分成功的操作,可能需要记录哪些操作成功,哪些失败,并进行相应的处理。
7.1.7. 并发控制
  • 当多个应用程序尝试同时对相同的资源进行批量操作时,可能会出现并发问题。使用适当的锁机制和事务隔离级别可以减轻这些问题。

批量事务处理是一种非常有用的模式,特别是在大数据量的应用场景中。正确的设计和实现可以显著提高应用程序的性能和稳定性。

7.2. 事务与Spring集成测试

在进行 Spring 集成测试时,正确地处理事务是非常重要的,因为它可以确保测试的可靠性和隔离性。下面是如何在 Spring 集成测试中使用事务的一些关键点和示例代码。

7.2.1. 使用 Spring TestContext Framework

Spring TestContext Framework 提供了一种简单的方式来集成测试 Spring 应用程序。它可以自动配置和管理测试所需的上下文。

7.2.2. 使用 @Transactional 注解

在 Spring 测试中,@Transactional 注解可以用于测试类或方法,以便每个测试方法都在一个新的事务中运行,并且在测试完成后自动回滚。

7.2.3. 使用 @Rollback 注解

@Rollback 注解可以用来控制事务是否应该在测试结束后回滚。默认情况下,@Transactional 会回滚事务。如果希望测试方法提交事务,可以使用 @Rollback(false)

7.2.4. 使用 @TestConfiguration

在测试类中使用 @TestConfiguration 来定义测试相关的配置,例如数据初始化、Mock 对象等。

7.2.5. 使用 @DataJpaTest@SpringBootTest

@DataJpaTest@SpringBootTest 是常用的注解,用于加载 Spring 上下文并准备测试环境。@DataJpaTest 通常用于 JPA 测试,而 @SpringBootTest 更通用,可以用于整个应用程序的测试。

7.2.6. 使用 @AutoConfigureTestDatabase

@AutoConfigureTestDatabase 可以用来控制测试数据库的配置。例如,可以使用 replace = AutoConfigureTestDatabase.Replace.NONE 来禁用自动配置的测试数据库,从而使用真实的数据库。

示例代码

下面是一个使用 Spring 测试框架进行事务测试的示例:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@Transactional
class AccountRepositoryTest {

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private EntityManager entityManager;

    @Test
    void shouldFindNoAccountsInitially() {
        List<Account> accounts = accountRepository.findAll();
        assertThat(accounts).isEmpty();
    }

    @Test
    void shouldPersistAndFindAccount() {
        // Given
        Account account = new Account();
        account.setBalance(1000.0);
        accountRepository.save(account);

        // When
        List<Account> accounts = accountRepository.findAll();

        // Then
        assertThat(accounts).hasSize(1);
        assertThat(accounts.get(0).getBalance()).isEqualTo(1000.0);
    }

    @Test
    @Rollback(false)
    void shouldCommitTransaction() {
        // Given
        Account account = new Account();
        account.setBalance(1000.0);
        accountRepository.save(account);

        // When
        List<Account> accounts = accountRepository.findAll();

        // Then
        assertThat(accounts).hasSize(1);
        assertThat(accounts.get(0).getBalance()).isEqualTo(1000.0);

        // 确认事务被提交
        entityManager.clear(); // 清除缓存,确保从数据库中重新加载数据
        List<Account> committedAccounts = accountRepository.findAll();
        assertThat(committedAccounts).hasSize(1);
        assertThat(committedAccounts.get(0).getBalance()).isEqualTo(1000.0);
    }
}

关键点解释

  1. @DataJpaTest: 加载最小的 Spring 上下文,仅包含 JPA 组件(如实体、Repository 等)。
  2. @Transactional: 每个测试方法都在一个新的事务中运行,并在测试完成后自动回滚。
  3. @Rollback(false): 控制测试方法提交事务,而不是回滚。
  4. EntityManager: 用于执行数据库操作,如查询、保存等。
  5. assertThat: 断言库,用于验证结果。

小结

在 Spring 集成测试中使用事务可以确保每个测试方法都是隔离的,并且不会影响其他测试方法的结果。通过使用 @Transactional@Rollback 注解,可以轻松地控制事务的行为,并确保测试的可靠性和一致性。

7.3. 事务与异步方法

在 Spring 中,事务与异步方法的结合可以带来一些挑战,因为异步方法通常会在不同的线程中执行,而事务管理通常是基于当前线程的。下面是如何在 Spring 中处理事务与异步方法的结合的一些关键点和注意事项。

7.3.1. 理解 Spring 的事务传播行为
  • PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • PROPAGATION_NESTED: 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则行为与 PROPAGATION_REQUIRED 相同。
7.3.2. 事务与异步方法的默认行为
  • 默认情况下,当一个异步方法被调用时,事务上下文不会传递给异步方法所在的线程。这意味着异步方法将在一个新的事务中执行,或者在没有事务的情况下执行,取决于异步方法的事务配置。
7.3.3. 使用 @Async@Transactional
  • 在同一个方法上使用 @Async@Transactional 可能会导致事务丢失,因为异步方法通常会在一个新的线程中执行,而事务管理是基于线程的。
7.3.4. 解决方案
  1. 使用 PROPAGATION_REQUIRES_NEW
  • 异步方法可以配置为 PROPAGATION_REQUIRES_NEW,这样即使在事务方法中调用异步方法,也会创建一个新的事务。
  1. 使用 PROPAGATION_NESTED
  • 异步方法可以配置为 PROPAGATION_NESTED,这样异步方法将在当前事务的嵌套事务中执行。但是需要注意的是,不是所有的数据库都支持嵌套事务。
  1. 使用 AsyncConfigurer
  • 实现 AsyncConfigurer 接口 来配置 TaskExecutor,并确保事务上下文能够被正确传播到异步线程中。
  1. 使用 TaskDecorator
  • 实现 TaskDecorator 接口 来装饰任务,以便在任务执行前和执行后能够处理事务上下文。
7.3.5. 示例代码

下面是一个使用 Spring 的 @Async@Transactional 注解来处理事务与异步方法结合的示例:

  1. 配置 AsyncConfigurer
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public TaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("Async-");
        executor.initialize();
        return new DelegatingTaskExecutor(executor);
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new SimpleAsyncUncaughtExceptionHandler();
    }
}
  1. 创建异步方法
@Service
public class AsyncService {

    @Autowired
    private AccountRepository accountRepository;

    @Async
    @Transactional(propagation = Propagation.NESTED)
    public void processAsyncTransfer(final Long fromAccountId, final Long toAccountId, final double amount) {
        // 从一个账户转账到另一个账户
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
        fromAccount.withdraw(amount);

        Account toAccount = accountRepository.findById(toAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
        toAccount.deposit(amount);

        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
    }
}
  1. 调用异步方法
@Service
public class AccountService {

    @Autowired
    private AsyncService asyncService;

    @Transactional
    public void transferMoney(final Long fromAccountId, final Long toAccountId, final double amount) {
        // 同步转账操作
        asyncService.processAsyncTransfer(fromAccountId, toAccountId, amount);
    }
}

关键点解释

  1. AsyncConfigurer: 配置 TaskExecutor 来支持异步执行。
  2. DelegatingTaskExecutor: 装饰 ThreadPoolTaskExecutor,确保事务上下文能够在异步线程中正确传播。
  3. @Async: 标记方法为异步执行。
  4. @Transactional(propagation = Propagation.NESTED): 确保异步方法在当前事务的嵌套事务中执行。
  5. AccountService.transferMoney: 在事务方法中调用异步方法。

注意事项

  • 数据库支持: 不是所有的数据库都支持嵌套事务。在使用 PROPAGATION_NESTED 之前,请确保你的数据库支持嵌套事务。
  • 事务隔离级别: 考虑事务隔离级别的影响,特别是在异步环境中。
  • 异常处理: 确保异常能够正确处理,特别是在异步方法中发生的异常。

小结

在 Spring 中,事务与异步方法的结合需要特别注意事务传播行为和事务上下文的传播。通过使用适当的配置和注解,可以确保事务在异步方法中得到正确的处理。务必注意数据库的支持程度和事务隔离级别的选择,以确保数据的一致性和完整性。

7.4. 分布式事务处理简介

分布式事务处理是处理跨多个资源管理器(如数据库、消息队列等)的事务的一种机制。在分布式系统中,事务可能涉及到多个服务、数据库或者其他持久化存储系统。这些系统通常运行在不同的节点上,并且可能由不同的技术栈支持。因此,确保这些跨系统的操作能够作为一个整体成功或失败,就需要一种协调机制来管理这些操作,这就是分布式事务处理的目的。

7.4.1. 分布式事务的关键概念
  1. ACID属性:

    • 原子性(Atomicity): 所有操作要么全部完成,要么一个也不完成。
    • 一致性(Consistency): 事务开始前后的数据状态保持一致。
    • 隔离性(Isolation): 并发执行的事务之间不能互相干扰。
    • 持久性(Durability): 一旦事务提交,它对系统的影响就是永久性的。
  2. 两阶段提交(2PC,Two-Phase Commit):

    • 准备阶段(Prepare Phase): 协调者询问参与者是否准备好提交事务。
    • 提交阶段(Commit Phase): 如果所有参与者准备好,则协调者发出提交指令;否则,参与者回滚事务。
  3. 三阶段提交(3PC,Three-Phase Commit):

    • 预准备阶段(Pre-prepare Phase): 协调者向参与者发送预准备请求。
    • 准备阶段(Prepare Phase): 参与者回复是否准备好。
    • 提交阶段(Commit Phase): 如果所有参与者准备好,则协调者发出提交指令;否则,参与者回滚事务。
  4. 最终一致性(Eventual Consistency):

    • 有时为了提高性能,系统会暂时放弃强一致性而采用最终一致性模型,即系统经过一段时间后会达到一致的状态。
7.4.2. 分布式事务处理的技术
  1. 两阶段提交(2PC):

    • 2PC 是最经典的分布式事务处理协议。
    • 它要求所有参与节点在提交前达成一致意见。
    • 缺点是性能瓶颈明显,尤其是在网络不稳定的情况下容易导致参与者长时间处于锁定状态。
  2. 三阶段提交(3PC):

    • 3PC 在 2PC 的基础上增加了一个预准备阶段。
    • 这样可以降低阻塞的可能性,提高系统的可用性。
  3. Saga 事务:

    • Saga 事务是一系列局部事务的编排。
    • 当某个局部事务失败时,会触发补偿操作(Compensation Operations)来回滚之前成功的事务。
    • Saga 通常用于实现最终一致性。
  4. TCC 事务(Try-Confirm-Cancel):

    • TCC 事务是一种特殊的 Saga 事务,其中每个参与者都实现了 Try、Confirm 和 Cancel 方法。
    • Try 方法尝试执行操作并预留资源。
    • Confirm 方法正式提交事务。
    • Cancel 方法取消并释放预留的资源。
  5. 基于事件的事务:

    • 通过发布和订阅事件的方式实现事务处理。
    • 适用于微服务架构中的事务处理。
  6. 分布式事务中间件:

    • 如 X/Open XA 规范,用于支持两阶段提交。
    • 中间件负责协调多个资源管理器之间的事务。
7.4.3. 实现细节
  • 事务协调者(Coordinator): 负责发起和管理整个分布式事务的过程。
  • 事务参与者(Participants): 指那些需要参与到分布式事务中的资源管理器。
  • 补偿逻辑(Compensation Logic): 当一个事务的一部分失败时,用来撤销已成功部分的逻辑。
7.4.4. 适用场景
  • 金融交易:例如银行转账需要保证资金在两个账户之间的正确转移。
  • 电子商务:如订单系统和库存管理系统之间的同步。
  • 物联网应用:设备间的交互可能需要跨多个系统的一致性保障。
7.4.5. 挑战
  • 网络延迟:网络问题可能会导致参与者长时间等待协调者的指令。
  • 故障恢复:协调者或参与者出现故障时,如何恢复事务状态。
  • 性能开销:分布式事务通常比本地事务更耗时。
7.4.6. 结论

分布式事务处理是一个复杂但至关重要的领域,尤其对于现代分布式系统来说。选择合适的分布式事务处理策略需要考虑系统的具体需求、性能目标以及容错能力等因素。

7.5. Spring事务与数据库连接池管理

在 Spring 中,事务管理和数据库连接池管理是紧密相关的,因为事务的生命周期和数据库连接的生命周期密切相关。下面是一些关于 Spring 事务管理与数据库连接池管理的关键点和最佳实践。

7.5.1. 数据库连接池的作用
  • 连接池 是一组预先创建好的数据库连接,这些连接被重用,以减少创建和销毁连接的开销。
  • 性能提升:通过复用连接,减少连接创建和关闭的次数,提高应用程序的性能。
  • 资源管理:限制最大连接数,避免过多的数据库连接导致资源耗尽。
7.5.2. Spring 与数据库连接池集成
  • Spring 支持多种连接池,包括 HikariCP、C3P0、Tomcat JDBC Connection Pool 等。
  • 配置连接池:在 Spring 配置文件中配置连接池的参数,例如最大连接数、最小空闲连接数等。
  • 使用 DataSource:Spring 通过 DataSource 接口与连接池进行交互。
7.5.3. Spring 事务管理与连接池的交互
  • 事务开始:当事务开始时,Spring 会从连接池中获取一个连接。
  • 事务提交:当事务提交时,连接被归还给连接池。
  • 事务回滚:如果事务回滚,连接同样被归还给连接池。
7.5.4. 事务管理与连接池的最佳实践
  • 配置合理的连接池参数:根据应用的需求调整连接池的最大连接数、最小连接数等。
  • 避免长活连接:确保事务及时提交或回滚,避免连接长时间占用。
  • 使用合适的事务隔离级别:根据应用的需求选择合适的隔离级别,以平衡并发性能和数据一致性。
  • 避免过度使用嵌套事务:嵌套事务可能会导致连接被长时间占用,影响性能。
7.5.5. 示例代码

下面是一个使用 Spring Boot 和 HikariCP 数据库连接池的示例配置:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
        dataSource.setUsername("root");
        dataSource.setPassword("password");

        // 配置 HikariCP 连接池
        dataSource.setConnectionProperties("dataSource.connectionTimeout=30000;dataSource.idleTimeout=60000;dataSource.maxLifetime=1800000;dataSource.maximumPoolSize=10;dataSource.minimumIdle=5;");
        return dataSource;
    }
}
7.5.6. 使用 Spring Boot 自动配置连接池
  • Spring Boot 自动配置:Spring Boot 会自动配置 HikariCP 作为连接池。
  • 配置文件:可以通过 application.propertiesapplication.yml 文件配置连接池参数。
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
7.5.7. 使用 Spring 的事务管理
  • @Transactional 注解:用于方法或类级别,定义事务的边界。
  • TransactionTemplate:用于编程式事务管理。
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountService {

    private final AccountRepository accountRepository;

    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Transactional
    public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
        // 从一个账户转账到另一个账户
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
        fromAccount.withdraw(amount);

        Account toAccount = accountRepository.findById(toAccountId).orElseThrow(() -> new RuntimeException("Account not found"));
        toAccount.deposit(amount);

        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
    }
}

小结

在 Spring 中,事务管理与数据库连接池的管理是相辅相成的。通过合理配置连接池参数和使用合适的事务管理策略,可以有效提升应用程序的性能和稳定性。确保事务的正确提交或回滚,并且避免连接长时间占用,是优化事务管理的关键。

7.6. 事务与性能优化

在软件开发中,事务处理对于保证数据的一致性和完整性至关重要。然而,在高性能系统设计中,事务处理也可能成为性能瓶颈。为了在保证数据一致性的前提下优化性能,我们需要考虑一些关键因素和技术。以下是关于事务与性能优化的一些要点:

7.6.1. 减少事务范围
  • 最小化事务范围:尽量减少事务处理的范围,即减少事务内执行的操作数量。这有助于减少锁定的时间,从而降低死锁的可能性,并减少事务处理的时间。
  • 避免在循环中使用事务:如果可能的话,不要在循环内部开启事务,而是将整个操作作为一个事务来处理。
7.6.2. 选择合适的事务隔离级别
  • 隔离级别:不同的事务隔离级别对性能的影响不同。通常情况下,较低的隔离级别(如 READ UNCOMMITTED)能提供更好的性能,但可能引入脏读、不可重复读等问题;而较高的隔离级别(如 SERIALIZABLE)虽然提供了更强的数据一致性保证,但可能导致更多的锁定和等待。
  • 使用 READ COMMITTED 或 REPEATABLE READ:这些隔离级别在大多数情况下都能提供较好的性能和一致性。
7.6.3. 锁定机制
  • 行级锁定:尽可能使用行级锁定而不是表级锁定,以减少锁定范围。
  • 乐观锁定:在不需要显式锁定的情况下,使用版本号或时间戳来实现乐观锁定,这样可以减少锁定冲突。
  • 避免死锁:确保事务按照一致的顺序访问资源,或者使用死锁检测和恢复机制。
7.6.4. 批量处理
  • 批量更新:对于大量数据的更新操作,可以考虑使用批量更新语句来减少网络往返次数和事务数量。
  • 批量加载:在事务中批量加载数据,减少数据库查询次数。
7.6.5. 事务批处理
  • 事务批处理:将多个独立的小事务合并成一个大的事务进行处理,可以减少事务开销。但需要注意的是,这也会增加失败后回滚的成本。
7.6.6. 使用读写分离
  • 读写分离:对于读密集型的应用,可以使用读写分离技术,将读操作和写操作分发到不同的数据库实例上,从而减轻主数据库的压力。
7.6.7. 缓存
  • 缓存结果:使用缓存来存储频繁访问的数据,减少对数据库的直接访问。
  • 缓存一致性:确保缓存和数据库之间的一致性,尤其是在事务提交之后更新缓存。
7.6.8. 异步处理
  • 异步处理:对于非关键路径的操作,可以考虑使用消息队列或事件驱动架构来异步处理,从而减少事务处理的延迟。
7.6.9. 数据库优化
  • 索引优化:确保经常查询的字段有适当的索引,避免全表扫描。
  • SQL 语句优化:编写高效的 SQL 查询,减少不必要的数据检索和计算。
7.6.10. 使用分布式事务
  • 分布式事务:对于跨数据库或多服务的操作,可以使用分布式事务处理方案,如两阶段提交(2PC)或 Saga 模式。
7.6.11. 监控和调优
  • 性能监控:持续监控事务的执行时间和锁定情况,以便识别性能瓶颈。
  • 负载测试:使用负载测试工具模拟高并发场景,观察事务处理的表现。
7.6.12. 实例分析

假设有一个电商系统需要处理大量的用户订单。在这个系统中,订单创建涉及到库存扣减和订单记录创建两个步骤。为了优化性能,我们可以采取以下措施:

  1. 事务范围最小化:确保库存扣减和订单记录创建在一个事务中完成,但不包含其他不必要的操作。
  2. 隔离级别选择:可以将隔离级别设置为 READ COMMITTED 或 REPEATABLE READ,以减少锁定。
  3. 批量处理:如果系统需要同时处理多个订单,可以将这些操作打包成一个事务进行批量处理。
  4. 缓存使用:对于库存信息,可以使用缓存来减少频繁的数据库访问,并在事务完成后更新缓存中的库存值。

通过上述措施,我们可以在保持数据一致性的同时提高系统的性能。例如,可以使用 Redis 作为缓存层来存储库存信息,并在事务提交后更新缓存中的库存值,从而减少对数据库的访问频率。此外,还可以利用消息队列(如 RabbitMQ 或 Kafka)来异步处理订单确认邮件的发送,进一步减少事务处理的延迟。这些优化措施可以帮助电商系统更好地应对高并发场景下的性能挑战。

8. Spring Boot与Spring事务

8.1. Spring Boot中自动配置的事务管理

在 Spring Boot 中集成 Spring 事务管理非常方便,Spring Boot 自动配置了许多事务管理的相关组件。下面是如何在 Spring Boot 项目中使用 Spring 事务管理的一些关键点和示例代码。

8.1.1. 添加依赖

确保你的项目中包含了 Spring Data JPA 和 Spring Boot Starter Data JPA 的依赖。如果你使用的是 Maven,可以在 pom.xml 文件中添加如下依赖:

<dependencies>
    <!-- Spring Boot Starter Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- H2 database for development -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
    </dependency>
</dependencies>

如果你使用的是 Gradle,可以在 build.gradle 文件中添加如下依赖:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
}
8.1.2. 配置数据库

application.propertiesapplication.yml 文件中配置数据库连接信息:

spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
8.1.3. 创建实体和 Repository

假设我们有一个 Order 实体类和对应的 OrderRepository

// Order.java
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String customerName;
    private Double totalAmount;

    // Getters and setters
}

// OrderRepository.java
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {
}
8.1.4. 创建 Service

在 Service 层使用 @Transactional 注解来声明事务边界:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public void createOrder(String customerName, Double totalAmount) {
        // 创建订单
        Order order = new Order();
        order.setCustomerName(customerName);
        order.setTotalAmount(totalAmount);
        orderRepository.save(order);

        // 其他业务逻辑
    }
}
8.1.5. 使用 @Transactional 注解
  • 方法级注解:将 @Transactional 注解放在方法上,该方法就会在一个事务中执行。
  • 类级注解:将 @Transactional 注解放在类上,那么该类的所有公共方法都会在一个事务中执行。
  • 配置属性:可以通过 @Transactional 注解的属性来定制事务行为,例如 propagation, isolation, readOnly, timeout 等。
8.1.6. 配置事务管理器

Spring Boot 默认会自动配置一个 PlatformTransactionManager,但也可以通过 @Bean 显式配置:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        // 自定义事务管理器
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
        return new TransactionTemplate(transactionManager);
    }
}

注意:在上面的配置中,entityManagerFactory 应该是从 Spring 上下文中注入的。这里需要进行相应的修改以确保正确注入 EntityManagerFactory

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private EntityManagerFactory entityManagerFactory;

    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
        return new TransactionTemplate(transactionManager);
    }
}
8.1.7. 使用编程式事务管理

除了声明式事务管理外,还可以使用编程式事务管理:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void createOrder(String customerName, Double totalAmount) {
        TransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = transactionManager.getTransaction(def);

        try {
            Order order = new Order();
            order.setCustomerName(customerName);
            order.setTotalAmount(totalAmount);
            orderRepository.save(order);

            // 其他业务逻辑

            transactionManager.commit(status);
        } catch (Exception ex) {
            transactionManager.rollback(status);
            throw ex;
        }
    }
}
8.1.8. 异常处理

了解异常与事务的关系也很重要。默认情况下,未捕获的 RuntimeExceptionError 会使事务回滚,而 checked 异常则不会导致事务回滚。可以通过 @TransactionalrollbackFornoRollbackFor 属性来定制异常处理行为。

小结

Spring Boot 使 Spring 事务管理变得非常简单。通过使用 @Transactional 注解,可以很容易地将事务边界添加到 Service 方法中。此外,还可以通过配置事务管理器和使用编程式事务管理来进一步定制事务行为。理解事务传播行为和异常处理对于编写健壮的应用程序至关重要。

8.2. Spring Boot下的事务配置选项

在 Spring Boot 中配置事务管理非常灵活且强大。Spring Boot 自动配置了许多事务管理的组件,但也提供了许多自定义选项,允许开发者根据具体需求进行细粒度的控制。下面是一些关于 Spring Boot 下事务配置的选项和示例。

8.2.1. 自动配置

Spring Boot 会自动配置一个 DataSourceJpaTransactionManager。如果你使用了 Spring Data JPA 或 Hibernate,则 Spring Boot 会自动配置事务管理器。

8.2.2. 使用 @EnableTransactionManagement

要启用 Spring 的事务管理,你需要在配置类上添加 @EnableTransactionManagement 注解。通常情况下,这是不需要的,因为 Spring Boot 已经为你启用了事务管理。

8.2.3. 使用 @Transactional 注解
  • 方法级注解:将 @Transactional 注解放在方法上,该方法就会在一个事务中执行。
  • 类级注解:将 @Transactional 注解放在类上,那么该类的所有公共方法都会在一个事务中执行。
  • 配置属性:可以通过 @Transactional 注解的属性来定制事务行为,例如 propagation, isolation, readOnly, timeout 等。
8.2.4. 配置事务管理器

如果需要自定义事务管理器,可以通过 @Bean 注解显式配置:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
        return new TransactionTemplate(transactionManager);
    }
}

注意:在上面的配置中,entityManagerFactory 应该是从 Spring 上下文中注入的。这里需要进行相应的修改以确保正确注入 EntityManagerFactory

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private EntityManagerFactory entityManagerFactory;

    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
        return new TransactionTemplate(transactionManager);
    }
}
8.2.5. 自定义事务属性

可以通过 @Transactional 注解的属性来自定义事务行为。以下是一些常用的属性:

  • propagation:指定事务的传播行为,默认为 PROPAGATION_REQUIRED
  • isolation:指定事务的隔离级别,默认为 ISOLATION_DEFAULT
  • readOnly:指定事务是否只读,默认为 false
  • timeout:指定事务的超时时间(以秒为单位),默认为 -1 表示使用全局默认值。
  • rollbackFor:指定哪些类型的异常会导致事务回滚。
  • noRollbackFor:指定哪些类型的异常不会导致事务回滚。

示例:

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED,
    readOnly = false,
    timeout = 30,
    rollbackFor = Exception.class,
    noRollbackFor = MyCustomException.class
)
public void someTransactionalMethod() {
    // 业务逻辑
}
8.2.6. 使用编程式事务管理

除了声明式事务管理外,还可以使用编程式事务管理:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void createOrder(String customerName, Double totalAmount) {
        TransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = transactionManager.getTransaction(def);

        try {
            Order order = new Order();
            order.setCustomerName(customerName);
            order.setTotalAmount(totalAmount);
            orderRepository.save(order);

            // 其他业务逻辑

            transactionManager.commit(status);
        } catch (Exception ex) {
            transactionManager.rollback(status);
            throw ex;
        }
    }
}
8.2.7. 配置事务管理器的属性

可以通过 @Configuration 类中的 @Bean 方法来自定义 PlatformTransactionManager 的属性,例如:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        // 自定义事务管理器的其他属性
        return transactionManager;
    }
}
8.2.8. 配置事务顾问

在某些情况下,你可能希望使用 AOP(面向切面编程)来更细粒度地控制事务行为。可以通过配置事务顾问来实现这一点:

import org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.RollbackRuleAttribute;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionInterceptor;

@Configuration
public class TransactionAdvisorConfig {

    @Bean
    public TransactionInterceptor transactionInterceptor(PlatformTransactionManager transactionManager) {
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        RuleBasedTransactionAttribute txAttr = new RuleBasedTransactionAttribute();
        txAttr.setPropagationBehavior(RuleBasedTransactionAttribute.PROPAGATION_REQUIRED);
        txAttr.setRollbackRules(new RollbackRuleAttribute(Exception.class));

        source.addTransactionalMethod("someTransactionalMethod");
        source.addTransactionalMethodPattern("create*");
        source.registerTransactionAttribute("someTransactionalMethod", txAttr);

        return new TransactionInterceptor(transactionManager, source);
    }

    @Bean
    public AbstractAdvisorAutoProxyCreator transactionAdvisor(TransactionInterceptor transactionInterceptor) {
        AbstractAdvisorAutoProxyCreator advisor = new DefaultAdvisorAutoProxyCreator();
        advisor.setAdvice(transactionInterceptor);
        return advisor;
    }
}

Spring Boot 提供了丰富的事务管理选项,从自动配置的基础事务管理到高度定制化的事务管理方案。通过使用 @Transactional 注解和编程式事务管理,可以根据具体的业务需求来配置事务的行为。理解事务传播行为和异常处理对于编写健壮的应用程序至关重要。

9. Spring 事务失效场景

9.1 什么时候Spring事务会失效?
  • 事务配置失效

    • 在传统的项目中,如果未正确配置事务管理器或未启用事务管理,事务将不会生效。而在 Spring Boot 项目中,事务管理通常会被自动配置,除非手动禁用或配置错误。
  • 默认情况下,Spring 事务执行过程中,如果抛出非 RuntimeException 和非 Error 错误的其他异常,是不会回滚的

    • 这是因为 Spring 事务管理默认只对未检查异常(即 RuntimeException 和其子类)进行回滚。可以通过 @Transactional(rollbackFor = {Exception.class}) 明确指定哪些异常类型应该导致事务回滚。
  • @Transactional 必须作用于被 Spring 管理的类和方法

    • 类必须被 Spring 管理:只有通过 Spring 容器创建的 Bean 才能应用事务。
    • 方法必须为 public:默认情况下,@Transactional 只对 public 方法有效,这是因为 Spring AOP 的实现基于 JDK 动态代理或 CGLIB 代理,而这些代理默认只能代理 public 方法。
  • 数据库必须支持事务机制

    • 如果底层数据库不支持事务,比如 MySQL 使用 MyISAM 存储引擎,那么事务将不起作用。
  • Spring AOP 代理机制下可能存在的 Spring 事务失效场景

    • 自调用问题:在同一类中的其他方法内部调用带有 @Transactional 注解的方法时,事务将不会生效
    • 静态方法和 final 方法:由于静态方法不属于任何特定对象实例,它们不能被代理,因此事务无效。
    • final 方法也不能被重写,因此也无法被代理。
  • 多线程场景调用

    • 当事务逻辑在不同的线程中执行时,事务会失效。这是因为每个线程都有自己的事务上下文。
9.2. 案例分析
9.2.1. SpringAOP 的自调用问题

​ 当一个方法被标记了@Transactional 注解的时候,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效,这是由Spring AOP 工作原理决定的

​ 因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 @Transactional 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,代理对象就无法拦截到这个内部调用,因此事务也就失效了。

例如MyService 类中的method1()调用method2()就会导致method2()的事务失效

@Service
public class MyService {

private void method1() {
     method2();
     //......
}
@Transactional
 public void method2() {
     //......
  }
}

基于上述案例基础,构建TrabsactionalServiceC服务测试Spring事务失效案例:一个没有被@Transactional的方法methodC中内部直接调用了一个带有@Transactional的add方法,则会导致add方法的事务失效

@Service
public class MyService {

    @Autowired
    private TableMapper tableMapper;

    public void methodC() {
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        add();
    }

    @Transactional
    public void add() {
        tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        throw new RuntimeException();
    }
}

在这个例子中,methodC 调用 add 方法。由于 add 方法是在同一个类中调用的,Spring AOP 无法拦截此调用,因此事务不会生效。

9.2.2. 解决方案
  • 一、避免直接自调用

    • 自己注入自己:
    @Service
    public class TransactionServiceC {
    
        @Autowired
        private TableMapper tableMapper;
    
        @Lazy
        @Autowired
        private TransactionServiceC transactionServiceC;
    
        public void methodC() {
            tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
            transactionServiceC.add();
        }
    
        @Transactional
        public void add() {
            tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
            throw new RuntimeException();
        }
    }
    
    • 使用另一个 Service 类
    public class TransactionServiceC {
    
        @Autowired
        private TableMapper tableMapper;
    
        @Lazy
        @Autowired
        private MyService myService;
    
        // 内部方法
        public void methodC(){
            tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
            myService.add();
        }
    
        @Service
        public class MyService {
            @Transactional(rollbackFor=Exception.class)
            public void add(){
                tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-","")));
                throw new RuntimeException();
            }
        }
    }
    
  • 二、转为代理方式调用

    public class MyService {
    
    private void method1() {
         ((MyService)AopContext.currentProxy()).method2(); // 先获取该类的代理对象,然后通过代理对象调用method2
         //......
    }
    @Transactional
     public void method2() {
         //......
      }
    }
    
    • 使用 AopContext.currentProxy() 获取代理对象:
    public void methodC() {
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        ((TransactionServiceC) AopContext.currentProxy()).add();
    }
    
    @Transactional
    public void add() {
        tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        throw new RuntimeException();
    }
    

    例如在上述的案例中,将add()方法直接调用转化为通过代理方式进行调用

9.2.3. 多线程场景事务失效

事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的,这样会导致两个方法不在同一个线程中,获取到的数据库连接不同,则涉及两个不同的事务。

​ Spring源码中的事务时通过数据库连接来实现的,所谓同一个事务其实是同一个数据库连接,只有拥有同一个数据库连接才能确保同时提交和回滚,如果基于当前场景,两个线程拿到的数据库连接不同,所以对应不同的事务机制,则其失效机制要结合具体的情况去分析(例如另外连接的数据库不支持事务机制也会导致事务失效)

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

在这个例子中,事务操作在另一个线程中执行,事务上下文不会被传递到新线程中,因此事务将不会生效。

9.3. Spring事务不回滚场景
9.3.1. Spring事务不回滚浅析

Spring事务不回滚场景本质上可以理解为是Spring事务失效的一部分场景,此处单独摘出来主要是为了区分一些开发场景下比较容易出现事务失效的开发误区

事务不回滚场景

​ 所谓事务不回滚概念,即业务异常触发的时候没有按照预期回滚事务,这种情况主要和Spring事务配置相关,可以着重考虑Spring事务配置是否生效?是否根据场景选择了合适的事务配置?

  • 例如Spring事务失效,则自然不会出现回滚
  • 使用了不符合场景的Spring事务配置:例如传播行为设定、异常处理等都要结合实际的业务场景去开发
    • 传播行为配置:要结合实际业务场景选择合适的配置(例如如果配置了Propagation.NEVER,这类的的传播特性不支持事务,如果有事务则会抛异常)
    • 针对独立事务需要注意一些异常的处理:==异常场景:==例如自己吞了异常、或者抛出其他异常、自定义回滚异常等都有可能导致事务不回滚
  • 嵌套事务回滚太多
9.3.2. Spring事务不回滚场景案例

Spring事务不回滚的情况通常发生在事务配置不当或异常处理不当的情况下。下面我们将通过具体的案例来分析几种常见的事务不回滚情况。

9.3.3. 错误的异常处理导致独立事务失效

假设我们有一个简单的事务性方法,用于向数据库插入两条记录。当方法抛出异常时,事务应该回滚,阻止这两条记录被插入。

@Service
public class TransactionServiceD {

    @Autowired
    private TableMapper tableMapper;

    @Transactional
    public void add() {
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        throw new RuntimeException();
    }
}
9.3.4. 场景1:自己吞了异常

在这种情况下,正常逻辑下是异常触发Spring事务回滚机制,但是如果直接捕获了异常又没有做任何的抛出处理,就会导致Spring事务不回滚。

@Service
public class TransactionServiceD {

    @Autowired
    private TableMapper tableMapper;

    @Transactional
    public void add() {
        try {
            tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
            tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
            throw new RuntimeException();
        } catch (Exception e) {
            System.out.println("捕获异常"); // 异常被捕获但没有重新抛出
        }
    }
}
9.3.5. 场景2:抛出了其他异常

如果捕获了异常,但是抛出的异常类型不正确(抛出非 RuntimeException 和非 Error 错误的其他异常),则Spring事务也不会回滚

@Service
public class TransactionServiceD {

    @Autowired
    private TableMapper tableMapper;

    @Transactional
    public void add() throws Exception {
        try {
            tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
            tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
            throw new RuntimeException();
        } catch (Exception e) {
            System.out.println("捕获异常并抛出");
            throw new Exception(); // 抛出非 RuntimeException 的异常
        }
    }
}
9.3.6. 场景3:自定义回滚异常

通过设置 @Transactional(rollbackFor = BusinessException.class) 可以指定哪些异常类型会导致事务回滚。如果配置错误,则事务可能不会按预期回滚。

spring支持自定义回滚的异常,在使用@Transactional注解声明事务时,可以通过设置rollbackFor参数,来完成这个功能。但如果这个参数的值设置错了,就会引出一些莫名其妙的问题

​ 例如自定义BusinessException异常来进行处理:

public class BusinessException extends RuntimeException{
    public BusinessException() {
        System.out.println("自定义异常");
    }
}

@Service
public class TransactionServiceD {

    @Autowired
    private TableMapper tableMapper;

    @Transactional(rollbackFor = BusinessException.class)
    public void add(){
        tableMapper.insertTableA(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        tableMapper.insertTableB(new TableEntity(UUID.randomUUID().toString().replaceAll("-", "")));
        throw new BusinessException();
    }
}

因为自定义异常本质上是通过extends RuntimeException实现的,spring支持自定义异常回滚机制,因此当上述程序抛出自定义异常时,相应的事务会触发回滚。如果在执行过程中抛出的是SqlException、DuplicateKeyException等异常,其并不在相应的回滚策略设定的范围内,因此就会出现回滚失败的情况,例如将上述的 throw new BusinessException();调整为throw new SqlException();然后再次访问,会发现事务不会正常回滚(两个数据表中都插入了数据);如果throw new SqlSessionException();这类RuntimeException 子类异常则事务会正常回滚

​ 针对上述场景分析:可以从这句话去理解,默认情况下,Spring 事务执行过程中,如果抛出非 RuntimeException 和非 Error 错误的其他异常,是不会回滚的,而上述的Exception、SqlException本质上都是属于非RuntimeException ,所以会间接导致事务回滚失败

​ 使用rollbackFor参数配置限定异常,让事务回滚也支持相应的场景,例如此处设定@Transactional(rollbackFor = Exception.class)则此时抛出其本身或者其子类异常也是可以支持事务回滚的。一般情况下可将原有的默认值设定为Exception或Throwable

9.3.7. 场景4:嵌套事务回滚太多

当事务属性的传播类型为 NESTED 时,子事务可以嵌套在父事务中。如果子事务回滚,这并不会影响父事务的状态,除非子事务中的异常被父事务捕获。

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

例如此处设定UserService的add方法为父事务,RoleService的doOtherThing方法为子事务,则当doOtherThing方法中触发异常会导致事务回滚,但doOtherThing方法中并没有对异常做捕获处理,就会将异常向上抛给父事务,而父方法也没有对异常做捕获处理就会相应触发异常导致父事务也触发回滚,整体表现就是父子事务都回滚了。

​ 但实际业务场景可能只希望回滚doOtherThing的执行内容,而不希望回滚add方法的执行内容(类似回滚保存点),那么依据NESTED特性,此处只需要在调用doOtherThing方法的时候捕获异常不让它向上抛出即可

public void add(UserModel userModel) throws Exception {
    userMapper.insertUser(userModel);
    roleService.doOtherThing();
}
9.3.8. 解决方案

对于场景1和场景2,正确的做法是确保事务中的异常被重新抛出为 RuntimeExceptionError,或者使用 @Transactional(rollbackFor = Exception.class) 来捕获所有类型的异常。

对于场景3,需要确保 rollbackFor 参数正确地指定了应该导致回滚的异常类型。

对于场景4,可以考虑在子事务中捕获异常并处理,而不是将其抛给父事务,以避免不必要的回滚。

9.4. 大事务问题
9.4.1. 问题描述

在开发过程中,我们通常会在方法上添加 @Transactional 注解以添加事务功能。然而,有时候一个方法中涉及到需要进行事务控制的节点可能只有一两个,如果直接在整个方法外围添加事务控制,就会将整个方法包含在事务中,这可能导致所谓的“大事务”问题。

例如:

@Service
public class UserService {

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        query1();
        query2();
        query3();
        roleService.save(userModel);
        update(userModel);
    }
}

@Service
public class RoleService {

    @Autowired
    private RoleService roleService;

    @Transactional
    public void save(UserModel userModel) throws Exception {
        query4();
        query5();
        query6();
        saveData(userModel);
    }
}

在这个例子中,UserServiceadd 方法和 RoleServicesave 方法都被声明为事务性的。实际上,真正需要进行事务控制的则是

  • UserService:roleService.save(userModel);、update(userModel);
  • RoleService:save方法

如果 query 方法较多,且某些查询较为耗时,那么整个事务将会变得非常耗时,从而引起一系列问题,如:

  • 死锁
  • 回滚时间长
  • 并发情况下数据库连接池被占满
  • 锁等待
  • 接口超时
  • 数据库主从延迟
9.4.2. 解决方案

为了解决大事务问题,我们可以采用编程式事务管理来更细粒度地控制事务范围。

@Autowired
private TransactionTemplate transactionTemplate;

...

public void save(final UserModel user) {
    queryData1();
    queryData2();
    transactionTemplate.execute(status -> {
        addData1();
        updateData2();
        return Boolean.TRUE;
    });
}

使用编程式事务管理,我们可以通过 TransactionTemplatePlatformTransactionManager 来显式地控制事务的开始、提交或回滚。这种方式可以解决两个问题:

  1. 避免由于Spring AOP问题,导致事务失效的问题:通过编程式事务管理,我们可以确保事务逻辑正确地执行,即使在自调用或其他AOP限制的情况下。
  2. 能够更小粒度地控制事务的范围:这使得我们可以更精确地控制哪些代码块需要被事务包围,从而避免大事务带来的问题。
9.4.3. 选择合适的事务管理方式
  • 声明式事务:适用于业务逻辑比较简单且不经常变动的场景,这种方式简单且开发效率高,但需要注意事务失效的场景。
  • 编程式事务:适用于需要更细粒度控制事务范围的场景,这种方式能够提供更灵活的事务管理,但需要更多的手动代码编写。

总结来说,选择合适的方式取决于具体的业务需求和事务管理的复杂度。在大多数情况下,声明式事务足以满足需求,但在面对复杂的事务管理需求时,编程式事务则提供了更多的灵活性和控制能力。

10. 案例研究

10.1. 实际项目中的事务管理应用
10.2. 事务管理与业务逻辑的结合
  • 18
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aries263

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值