面試突擊85:爲什麼事務@Transactional會失效?

導致 @Transactional 失效的常見場景有以下 5 個:

  1. 非 public 修飾的方法;
  2. timeout 超時時間設置過小;
  3. 代碼中使用 try/catch 處理異常;
  4. 調用類內部的 @Transactional 方法;
  5. 數據庫不支持事務。

很多人只知道答案但不知道原因,這就像只談戀愛不結婚一樣,是不能讓人接受的,所以本篇我們就來討論一下,導致事務失效的背後原因到底是啥?

在以上 5 種場景中,第 2 種(timeout 超時時間設置過小)和第 5 種(數據庫不支持事務)很好理解,我們這裏就不贅述了,本文我們重點來討論其他 3 種情況。

1.非 public 修飾的方法

非 public 修飾的方法上,即使加了 @Transactional 事務依然不會生效,原因是因爲 @Transactional 使用的是 Spring AOP 實現的,而 Spring AOP 是通過動態代理實現的,而 @Transactional 在生成代理時會判斷,如果方法爲非 public 修飾的方法,則不生成代理對象,這樣也就沒辦法自動執行事務了,它的部分實現源碼如下:

protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
   // Don't allow no-public methods as required.
   // 非 public 方法,設置爲 null
   if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
   }
   // 後面代碼省略....
 }

2.try/catch 導致事務失效

@Transactional 執行流程是: @Transactional 會在方法執行前,會自動開啓事務;在方法成功執行完,會自動提交事務;如果方法在執行期間,出現了異常,那麼它會自動回滾事務。

然而如果在方法中自行添加了 try/catch 之後,事務就不會自動回滾了,這是怎麼回事呢?
造成這個問題的主要原因和 @Transactional 註解的實現有關,它的部分實現源碼如下:

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
      throws Throwable {
   // If the transaction attribute is null, the method is non-transactional.
   final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
   final PlatformTransactionManager tm = determineTransactionManager(txAttr);
   final String joinpointIdentification = methodIdentification(method, targetClass);

   if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
       // Standard transaction demarcation with getTransaction and commit/rollback calls.
       // 自動開啓事務
      TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
      Object retVal = null;
      try {
         // This is an around advice: Invoke the next interceptor in the chain.
         // This will normally result in a target object being invoked.
         // 反射調用業務方法
         retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
          // target invocation exception
          // 異常時,在 catch 邏輯中,自動回滾事務
         completeTransactionAfterThrowing(txInfo, ex);
         throw ex;
      }
      finally {
         cleanupTransactionInfo(txInfo);
      }
       // 自動提交事務
      commitTransactionAfterReturning(txInfo);
      return retVal;
   }

   else {
     // .....
   }
}

從上述實現源碼我們可以看出:當執行的方法中出現了異常,@Transactional 才能感知到,然後再執行事務回滾,而當開發者自行添加了 try/catch 之後,@Transactional 就感知不到異常了,從而就不會觸發事務的自動回滾了,這就是爲什麼當 @Transactional 遇到 try/catch 之後就不會自動回滾(事務)的原因。

3.調用類內用的 @Transactional 方法

當調用類內部的 @Transactional 修飾的方法時,事務也不會生效,如下代碼所示:

@RequestMapping("/save")
public int saveMappping(UserInfo userInfo) {
    return save(userInfo);
}
@Transactional
public int save(UserInfo userInfo) {
    // 非空效驗
    if (userInfo == null ||
        !StringUtils.hasLength(userInfo.getUsername()) ||
        !StringUtils.hasLength(userInfo.getPassword()))
        return 0;
    int result = userService.save(userInfo);
    int num = 10 / 0; // 此處設置一個異常
    return result;
}

上述代碼在添加用戶之後即使遇到了異常,程序也沒有執行回滾,這是因爲 @Transactional 是基於 Spring AOP 實現的,而 Spring AOP 又是基於動態代理實現的,而當調用類內部的方法時,不是通過代理對象完成的,而是通過 this 對象實現的,這樣就繞過了代理對象,從而事務就失效了。

總結

非 public 修飾的方法在 @Transactional 實現時做了判斷,如果是非 public 則不會生成代理對象,所以事務就失效了;而調用類內部的 @Transactional 修飾的方法時,也是因爲沒有成功調用代理對象,是通過 this 來調用方法的,所以事務也失效了;@Transactional 在遇到開發者自定義的 try/catch 也會失效,這是因爲 @Transactional 只有感知到了異常纔會自動回滾(事務),但如果用戶自定義了 try/catch,那麼 @Transactional 就感知不到異常,所以也就不會自動回滾事務了。

參考 & 鳴謝

blog.csdn.net/qq_20597727/article/details/84900994

是非審之於己,譭譽聽之於人,得失安之於數。

公衆號:Java面試真題解析

面試合集:https://gitee.com/mydb/interview

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