使用 Spring Transactional 註釋的最佳方式 - Vlad Mihalcea

banq發表於2022-01-27

在本文中,我將向您展示使用 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,因此使事務成為讀寫。

分割讀寫和只讀方法的另一個巨大優勢是,我們可以將它們路由到不同的資料庫節點。這樣,我們可以通過增加副本節點的數量來擴充套件只讀流量。

 

相關文章