使用 Spring Transactional 注释的最佳方式 - Vlad Mihalcea


在本文中,我将向您展示使用 Spring Transactional 注释的最佳方式。
 
Spring事务注解
从 1.0 版本开始,Spring 就提供了对基于 AOP 的事务管理的支持,允许开发人员以声明方式定义事务边界。
不久之后,在 1.2 版本中,Spring 增加了对@Transactionalannotation的支持,这使得配置业务单位的事务边界变得更加容易。
@Transactional注解提供了以下属性。

  • value和transactionManager - 这些属性可以用来提供一个TransactionManager引用,以便在处理被注释块的事务时使用。
  • 传播 - 定义了事务边界如何传播到其他将被直接或间接从注释块中调用的方法。默认的传播是REQUIRED,意味着如果还没有事务可用,就会启动一个事务。否则,正在进行的事务将被当前运行的方法所使用。
  • timeout和timeoutString - 定义当前方法在抛出TransactionTimedOutException之前允许运行的最大秒数。
  • readOnly - 定义了当前事务是只读还是读写。
  • rollbackFor和rollbackForClassName - 定义一个或多个Throwable类,当前事务将被回滚。默认情况下,如果抛出RuntimException或Error,事务将被回滚,但如果抛出一个检查过的Exception,则不会被回滚。
  • noRollbackFor和noRollbackForClassName - 定义一个或多个Throwable类,当前事务不会被回滚。通常情况下,你会对一个或多个RuntimException类使用这些属性,因为你不想回滚给定的事务。

Spring Transactional注解属于哪个层?
@Transactional注解属于服务层,因为定义事务边界是服务层的责任。

不要在Web层中使用它,因为这会增加数据库事务的响应时间,并且更难为给定的数据库事务错误提供正确的错误信息(例如,一致性、死锁、锁获取、乐观锁)。

DAO(数据访问对象)或Repository层需要一个应用层的事务,但这个事务应该从服务层传播。
  
使用Spring事务性注解的最佳方式
在服务层中,你可以有数据库相关的和非数据库相关的服务。如果一个给定的业务用例需要将它们混合在一起,比如当它必须解析一个给定的语句,建立一个报告,并将一些结果保存到数据库中时,如果数据库事务尽可能晚地开始,那是最好的。

出于这个原因,你可以有一个非事务性的网关服务,比如下面的RevolutStatementService。

@Service
public class RevolutStatementService {
 
    @Transactional(propagation = Propagation.NEVER)
    public TradeGainReport processRevolutStocksStatement(
            MultipartFile inputFile,
            ReportGenerationSettings reportGenerationSettings) {
        return processRevolutStatement(
            inputFile,
            reportGenerationSettings,
            stocksStatementParser
        );
    }
     
    private TradeGainReport processRevolutStatement(
            MultipartFile inputFile,
            ReportGenerationSettings reportGenerationSettings,
            StatementParser statementParser
    ) {
        ReportType reportType = reportGenerationSettings.getReportType();
        String statementFileName = inputFile.getOriginalFilename();
        long statementFileSize = inputFile.getSize();
 
        StatementOperationModel stocksStatementModel = statementParser.parse(
            inputFile,
            reportGenerationSettings.getFxCurrency()
        );
        int statementChecksum = stocksStatementModel.getStatementChecksum();
        TradeGainReport report = generateReport(stocksStatementModel);
 
        if(!operationService.addStatementReportOperation(
            statementFileName,
            statementFileSize,
            statementChecksum,
            reportType.toOperationType()
        )) {
            triggerInsufficientCreditsFailure(report);
        }
 
        return report;
    }
}

processRevolutStocksStatement方法是非事务性的,为此,我们可以使用Propagation.NEVER策略来确保这个方法永远不会被活动的事务调用。

因此,statementParser.parse和generateReport方法是在一个非事务性的上下文中执行的,因为我们不想在只需要执行应用级处理的时候获取一个数据库连接并保持它。

只有operationService.addStatementReportOperation需要在事务性上下文中执行,为此,addStatementReportOperation使用了@Transactional注释。

@Service
@Transactional(readOnly = true)
public class OperationService {
 
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public boolean addStatementReportOperation(
        String statementFileName,
        long statementFileSize,
        int statementChecksum,
        OperationType reportType) {
         
        ...
    }
}

请注意,addStatementReportOperation覆盖了默认的隔离级别,并指定该方法在一个SERIALIZABLE数据库事务中执行。

另一件值得注意的事情是,该类被注解为@Transactional(readOnly = true),这意味着,默认情况下,所有服务方法都将使用这一设置,并在只读事务中执行,除非该方法使用自己的@Trsnactional定义来覆盖事务设置。

对于事务性服务,好的做法是在类的层面上将只读属性设置为 "真",并在每个需要向数据库写入的服务方法上覆盖它。

例如,UserService使用同样的模式。

@Service
@Transactional(readOnly = true)
public class UserService implements UserDetailsService {
 
    @Override
    public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException {
        ...
    }
     
    @Transactional
    public void createUser(User user) {
        ...
    }
}

loadUserByUsername使用的是只读事务,由于我们使用的是Hibernate,Spring也执行了一些只读的优化。

另一方面,createUser必须写到数据库中。因此,它用@Transactional注解给出的默认设置覆盖了readOnly属性值,即readOnly=false,因此使事务成为读写。

分割读写和只读方法的另一个巨大优势是,我们可以将它们路由到不同的数据库节点。这样,我们可以通过增加副本节点的数量来扩展只读流量。