51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Boot 应用中的事务处理

在 Spring 应用中,无论使用 JdbcTemplate 还是 JPA/Hibernate 或 Spring Data JPA,都需要处理数据库事务。

数据库事务是一个事务单元,它要么全部完成,要么都不完成,并使数据库处于一致状态。在实现数据库事务时,需要考虑到 ACID(原子性、一致性、隔离性、持久性)属性。

让我们了解一下如何在 Spring Boot 应用中处理数据库事务。

使用 JDBC 进行事务处理 {#使用-jdbc-进行事务处理}

首先,让我们快速了解一下我们通常是如何在普通 JDBC 中处理数据库事务的。

class UserService {
    
    void register(User user) {
        String sql = "...";
        Connection conn = dataSource.getConnection(); // <1>
        try(conn) {  // <6>
            conn.setAutoCommit(false);  // <2>
            PreparedStatement pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, user.getEmail());
            pstmt.setString(2, user.getPassword());
            pstmt.executeUpdate();  // <3>
            conn.commit();  // <4>
        } catch(SQLException e) {
            conn.rollback();  // <5>
        }
    }
}

在上述代码片段中:

  • <1> 我们从数据源连接池中获取了一个数据库连接。
  • <2> 默认情况下,每个数据库操作都会自动在自己的事务中运行。为了自己控制事务,我们将 AutoCommit 设置为 false
  • <3> 然后,我们执行了必要的数据库操作。
  • <4> 如果一切OK,则提交事务。
  • <5> 如果抛出了异常,则回滚事务。
  • <6> 由于我们使用的是 try-with-resources 语法,连接将自动关闭。

在该事务处理的示例中,存在大量的模板代码。

Spring 通过 AOP 简化了数据库事务处理机制。我们可以使用 @Transactional 注解以声明式或使用 TransactionTemplate 以编程方式实现数据库事务处理。

Spring 声明式事务处理 - @Transactional {#spring-声明式事务处理---transactional}

通常,在三层分层架构中,web 层有 controller,业务逻辑层有 service,数据访问层有 repository。

一般来说,"事务单元" 以 service 方法为模型,并被视为 "事务边界"。

我们可以在 Service 层方法上添加 Spring 的 @Transactional 注解来定义事务范围。该方法中的所有数据库操作都将在一个事务中运行。如果方法执行成功,则事务将被提交。如果在方法执行过程中出现任何 RuntimeException 异常,则事务将被回滚。

@Service
class UserService {
    private final JdbcTemplate jdbcTemplate;
    
    @Transactional
    void register(User user) {
        String sql = "...";
        jdbcTemplate.update(sql);
    }
}

你可以在方法级别添加 @Transactional 注解,使该特定方法具有事务性;也可以在类级别添加 @Transactional 注解,使该类中的所有 public 方法都具有事务性。

当你在类或其任何方法上添加 @Transactional 注解时,Spring 会为该类创建一个代理,使用 Spring AOP 来执行事务处理逻辑。

Spring 中的事务处理模型

添加 @Transactional 注解时,默认情况下:

  • 传播(propagation)设置为 REQUIRED,即参与当前事务(如果存在)或启动新事务。
  • 如果方法抛出任何 RuntimeException,则回滚事务。
  • 如果方法抛出任何受检异常(非 runtime 异常),则不会回滚事务。

你可以通过如下方式指定 @Transactional 属性来自定义这种行为:

@Service
class UserService {
    private final JdbcTemplate jdbcTemplate;
    
    @Transactional(
            propagation = Propagation.REQUIRES_NEW,
            rollbackFor = DuplicateUserException.class,
            noRollbackFor = IllegalArgumentException.class)
    void register(User user) throws DuplicateUserException {
        String sql = "...";
        jdbcTemplate.update(sql);
    }
}

class DuplicateUserException extends Exception {}

在上面的代码片段中,我们将传播级别指定为 REQUIRES_NEW,而不是默认的 REQUIREDREQUIRES_NEW 传播级别会暂停当前事务(如果存在),启动一个新事务,执行数据库操作,提交事务,然后恢复先前暂停的事务。

此外,我们还指定了如果出现 DuplicateUserException,即使它是一个受检异常,也要回滚事务。以及,如果出现 IllegalArgumentException(即使它是一个 RuntimeException),也不回滚事务。

Spring 编程式事务处理 - TransactionTemplate {#spring-编程式事务处理---transactiontemplate}

Spring 还提供了一种编程式事务处理,即使用 TransactionTemplate。具体如下:

@Service
class UserService {
    private final TransactionTemplate transactionTemplate;

    void register(User user) {
        transactionTemplate.execute(status -> {
            String sql = "...";
            jdbcTemplate.update(sql);
            
            // 如果异常,则回滚
            //status.setRollbackOnly();

            return result;
        });
    }
}

如果想更精准地控制事务,可以使用 TransactionTemplate

使用 @Transactional 注解时的常见错误 {#使用-transactional-注解时的常见错误}

我们需要了解代理是如何工作的,以免事务配置出错。

让我们以 controller 调用事务方法为例,该方法又会调用其他事务方法。

UserController  --> UserService --> AccountService

假设我们的用户注册流程如下:

@Controller
class UserController {
    private final UserService userService;
    
    @PostMapping("/register")
    String register() {
        User user = ...;
        userService.register(user);
        return "...";
    }
}

//--------------------------------------------------------------

@Service
class UserService {
    private final UserRepository userRepository;
    private final AccountService accountService;
    
    @Transactional
    public void register(User user) {
        userRepository.save(user);
        
        Account account = ...;
        accountService.create(account);

        UserPreferences preferences = ...;
        userRepository.savePreferences(preferences);
    }
}

//--------------------------------------------------------------

@Service
class AccountService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void create(Account account) {
        ...
    }
}

在上面的示例中,我们从 UserController 调用 UserService.register(...) 方法,该方法使用了 @Transactional 注解。由于没有新事务正在进行,因此将创建一个新事务。用户详细信息将在当前事务中保存到数据库中。之后,我们将调用 AccountService.create(account),它注解了 @Transactional(propagation = Propagation.REQUIRES_NEW)。现在,正在进行的当前事务将被暂时中止,一个新事务将被启动。账户创建将在新事务中执行,如果没有异常发生,该事务将立即提交。然后恢复前一个事务并保存用户首选项(UserPreferences)。

如果在保存首选项时出现 RuntimeException 异常,那么只有"用户记录插入"会回滚,但"账户创建"不会,因为"账户创建"发生在一个已成功提交的单独事务中。

这将如期运行。

但是,假设"账户创建"方法也在 UserService 中,如下所示:

@Controller
class UserController {
    private final UserService userService;
    
    @PostMapping("/register")
    String register() {
        User user = ...;
        userService.register(user);
        return "...";
    }
}

//--------------------------------------------------------------

@Service
class UserService {
    private final UserRepository userRepository;
    
    @Transactional
    public void register(User user) {
        userRepository.save(user);
        
        Account account = ...;
        this.create(account);

        UserPreferences preferences = ...;
        userRepository.savePreferences(preferences);
    }

    // 账户创建,和 register() 方法在同一个类中
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void create(Account account) {
        ...
    }
}

我们配置了 REQUIRES_NEW 传播级别,我们可能会认为 "账户创建" 逻辑将在新事务中执行,但这是错误的。

UserService 上添加 @Transactional 注解时,会创建一个代理,并将该代理注入到 UserController 中。从 UserController 调用 userService.register(...) 方法时,就是在调用代理的 register() 方法。代理将根据 @Transactional 语义应用事务处理逻辑,然后将实际逻辑的执行委托给实际的 UserService 实例。

现在,在 UserService.register() 方法中调用 create(account) 方法时,它将是对实际 UserService 对象(非代理对象)的本地方法调用,而实际 UserService 对象感知不到事务处理。

Spring 事务处理模型

在上述情况下,create(account) 逻辑也将在调用 UserService.register() 方法时创建的同一个父事务中执行。

因此,如果要调用一个具有不同事务语义的方法,最好的办法是将该方法移到一个单独的类中。也有其他方法,比如使用自注入,但不推荐使用这种方法。


参考:https://www.sivalabs.in/spring-boot-database-transaction-management-tutorial/

赞(3)
未经允许不得转载:工具盒子 » Spring Boot 应用中的事务处理