業務代碼中的Spring聲明式事務,你都處理正確了嗎?

今天,該篇博客主要聊聊業務代碼中與數據庫事務相關的坑。

Spring 針對 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API(JPA) 等事務 API,實現了一致的編程模型,而 Spring 的聲明式事務功能更是提供了極其方便的事務配置方式,配合 Spring Boot 的自動配置,大多數 Spring Boot 項目只需要在方法上標記 @Transactional 註解,即可一鍵開啓方法的事務性配置。

大多數業務開發同學都有事務的概念,也知道如果整體考慮多個數據庫操作要麼成功要麼失敗時,需要通過數據庫事務來實現多個操作的一致性和原子性。但,在使用上大多僅限於爲方法標記 @Transactional,不會去關注事務是否有效、出錯後事務是否正確回滾,也不會考慮複雜的業務代碼中涉及多個子業務邏輯時,怎麼正確處理事務。事務沒有被正確處理,一般來說不會過於影響正常流程,也不容易在測試階段被發現。但當系統越來越複雜、壓力越來越大之後,就會帶來大量的數據不一致問題,隨後就是大量的人工介入查看和修復數據(也就是所謂的髒數據)。

小心Spring 的事務可能沒有生效

在使用 @Transactional 註解開啓聲明式事務時, 第一個最容易忽略的問題是,很可能事務並沒有生效。

事務失效場景一:

定義一個 UserService 類,負責業務邏輯處理。如果不清楚 @Transactional 的實現方式,只考慮代碼邏輯的話,這段代碼看起來沒有問題。
定義一個入口方法 createUserWrong1 來調用另一個私有方法 createUserPrivate,私有方法上標記了 @Transactional 註解。當傳入的用戶名包含 test 關鍵字時判斷爲用戶名不合法,拋出異常,讓用戶創建操作失敗,期望事務可以回滾:

@Service
@Slf4j
public class UserService {
   @Autowired
   private UserRepository userRepository;

   //一個公共方法供Controller調用,內部調用事務性的私有方法
   public int createUserWrong1(String name) {
   try {
         this.createUserPrivate(new UserEntity(name));
   } catch (Exception ex) {
         log.error("create user failed because {}", ex.getMessage());
   }
     return userRepository.findByName(name).size();
 }
   //標記了@Transactional的private方法
   @Transactional
   private void createUserPrivate(UserEntity entity) {
      userRepository.save(entity);
      if (entity.getName().contains("test"))
        throw new RuntimeException("invalid username!");
    }
  }
}

調用接口後發現,即便用戶名不合法,用戶也能創建成功。刷新瀏覽器,多次發現有十幾個的非法用戶註冊。

這裏給出 @Transactional 生效原則 1,除非特殊配置(比如使用 AspectJ 靜態織入實現AOP),否則只有定義在 public 方法上的 @Transactional 才能生效。原因是,Spring默認通過動態代理的方式實現 AOP,對目標方法進行增強,private 方法無法代理到,Spring 自然也無法動態增強事務處理邏輯。

測試發現,調用新的 createUserWrong2 方法事務同樣不生效。這裏,我給出@Transactional 生效原則 2,必須通過代理過的類從外部調用目標方法才能生效。

調用方式:userService.createUserPublic(new UserEntity(name)); 原因是:通過 this 自調用,沒有機會走到 Spring 的代理類;後兩種改進方案調用的是 Spring 注入的 UserService,通過代理調用纔有機會對 createUserPublic 方法進行動態增強。

事務即便生效也不一定能回滾

通過 AOP 實現事務處理可以理解爲,使用 try…catch…來包裹標記了 @Transactional 註解的方法,當方法出現了異常並且滿足一定條件的時候,在 catch 裏面我們可以設置事務回滾,沒有異常則直接提交事務。

這裏的“一定條件”,主要包括兩點。
第一,只有異常傳播出了標記了 @Transactional 註解的方法,事務才能回滾。在 Spring的 TransactionAspectSupport 裏有個 invokeWithinTransaction 方法,裏面就是處理事務的邏輯。可以看到,只有捕獲到異常才能進行後續事務處理:

第二,默認情況下,出現 RuntimeException(非受檢異常)或 Error 的時候,Spring
纔會回滾事務。

①如果你希望自己捕獲異常進行處理的話,也沒關係,可以手動設置讓當前事務處於回滾狀態:

@Transactional
public void createUserRight1(String name) {
try {
      userRepository.save(new UserEntity(name));
      throw new RuntimeException("error");
} catch (Exception ex) {
      log.error("create user failed", ex);  
      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
   }
}

②在註解中聲明,期望遇到所有的 Exception 都回滾事務(來突破默認不回滾受檢異常的限制):

@Transactional(rollbackFor = Exception.class)
public void createUserRight2(String name) throws IOException {
   userRepository.save(new UserEntity(name));
   otherTask();
}

同樣也是可以回滾。

總結

業務代碼中最常見的使用數據庫事務的方式,即 Spring 聲明式事務,與你總結了使用上可能遇到的三類坑,包括:

第一,因爲配置不正確,導致方法上的事務沒生效。我們務必確認調用 @Transactional 註解標記的方法是 public 的,並且是通過 Spring 注入的 Bean 進行調用的。

第二,因爲異常處理不正確,導致事務雖然生效但出現異常時沒回滾。Spring 默認只會對標記 @Transactional 註解的方法出現了 RuntimeException 和 Error 的時候回滾,如果我們的方法捕獲了異常,那麼需要通過手動編碼處理事務回滾。如果希望 Spring 針對其他異常也可以回滾,那麼可以相應配置@Transactional 註解的 rollbackFor 和noRollbackFor 屬性來覆蓋其默認設置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章