天天看點

從源碼中窺探出事務失效的8種原因

核心流程解讀

我們從一段簡單的代碼入手,從頭到尾分析以下其中的奧秘。

如果在一個controller中調用service方法,該方法被@Transaction注解修飾。

controller方法:

@GetMapping("/save")
    public String saveStudent() {
        testService.save();
        return "success";
    }      

service方法:

@Transactional
    public void save() {
        //資料操作
    }      

那麼Spring在啟動時,将會在主流程refresh的registerBeanPostProcessors的方法中首先加載所有的BeanPostProcessor,接着在

finishBeanFactoryInitialization中去初始化所有非懶加載的Bean。

從源碼中窺探出事務失效的8種原因

首先對該controller進行加載,在屬性指派階段,發現依賴service,于是轉頭去加載該service。

Bean的生命周期主要包含以下階段:

從源碼中窺探出事務失效的8種原因

接着執行個體化該service,執行屬性指派與初始化,在初始化的末尾,會執行BeanPostProcessor實作類AbstractAutoProxyCreator的postProcessAfterInitialization方法,該方法會判斷是否需要為該service對象生成代理。

在Bean的生命周期全貌中,postProcessAfterInitialization方法位于綠色箭頭标記的地方。

從源碼中窺探出事務失效的8種原因

(對Spring Bean的生命周期不熟悉的同學,可以參考我的另外一篇文章​​還記不住Spring Bean的生命周期?看這篇你就知道方法了!​​)

接着進行判斷是否進行事務代理,如果需要代理,則進入到AbstractFallbackTransactionAttributeSource的computeTransactionAttribute方法中,嘗試擷取事務屬性。

從源碼中窺探出事務失效的8種原因

到這裡,我們可以從代理角度得到事務不生效的兩個原因

從代理角度來看

computeTransactionAttribute方法中,明确表示,如果目前方法的修飾為不為public,則傳回null,代表不支援事務。

Spring可能覺得既然不是公開的方法,代表應該由本類自己控制,而不應該被代理控制。另外final方法、static方法也不支援事務。

當然,如果你對private方法應用@Transaction注解,則你的idea會進行警告,但真正運作時,并不會出現任何報錯,給人一種沒有問題的假象。

如果,你就是想對private應用該注解,并且也想讓事務生效,則可以使用AspectJ。

AspectJ是一種靜态代理方式,直接在編譯期間修改位元組碼,性能比較好。而且不受修飾符的限制,不管你是private、final還是static,都可以進行代理。

還有一個原因比較簡單,如果你的service沒有加上@Service注解,則代表該service沒有被Spring容器所管理,Spring更不會閑着沒事生成代理對象,自然就不具有事務的功能。

我們繼續走完代碼,最終會來自SpringTransactionAnnotationParser的parseTransactionAnnotation方法,該方法會檢查是否存在@Transaction注解。

如果存在@Transaction注解,則為該service生成代理對象,注入到controller的service變量中。

之後在調用controll的接口時,首先tomcat還會為本次請求配置設定一個線程,先執行所有的過濾器,接着進入DispatcherServlet的doDispatch方法中。

doDispatch調用handle方法,接着使用反射調用controller的接口方法,調用handle方法在SpringMVC主流程中被綠色箭頭标記的位置。

從源碼中窺探出事務失效的8種原因

(對Spring MVC主流程、過濾器與攔截器不清楚的同學,可以參考我的另外一篇文章​​從源碼角度結合詳細圖例剖析過濾器與攔截器​​)

接着controller調用service方法,注意,spring為該service變量注入的是service的代理對象。

曆經千辛萬苦終于調用到了service的代理對象方法,接着代理方法會回調CglibAopProxy的靜态内部類DynamicAdvisedInterceptor的intercept方法,該方法内部會拿到事務攔截器TransactionInterceptor,調用攔截器的invoke方法:

public Object invoke(MethodInvocation invocation) throws Throwable {
        //擷取代理方法所處的被代理類,即原始類
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
  }      

invokeWithinTransaction方法處于父類TransactionAspectSupport中

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
      final InvocationCallback invocation) throws Throwable {

    //擷取事務屬性源,如果為null的話,代表該方法沒有開啟事務
    TransactionAttributeSource tas = getTransactionAttributeSource();
        //擷取事務屬性
    final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
        //擷取事務管理器
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
        //擷取連接配接點辨別,格式為被代理類的全限定名+方法名
        //例如:com.yang.ym.service.StudentService.save
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

    if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
            //根據事務傳播行為,看是否有必要建立出一個事務,判斷邏輯位于AbstractPlatformTransactionManager的getTransaction方法中
            //如果有必要建立一個事務時,會立即開啟一個新事務,并關閉自動送出
            //最後會利用ThreadLocal将事務資訊綁定到目前線程中
      TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
      Object retVal = null;
      try {
                //調用攔截鍊中的下一個攔截器,位于ReflectiveMethodInvocation中
                //proceedWithInvocation會判斷目前攔截器是否是最後一個,如果是的話,則直接調用連接配接點方法
        retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
                //當被代理方法執行異常時,會依據rollBackFor判斷是否進行復原
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
      }
      finally {
                //将此事務資訊從目前線程中移除
        cleanupTransactionInfo(txInfo);
      }
            //送出事務
      commitTransactionAfterReturning(txInfo);
      return retVal;
    }

    else {
               //程式設計式事務,本次不作分析
    }
  }      

這裡給到了我們新的思路,在異常處理這塊,也可能導緻事務不生效或未復原。

從異常處理以及異常類型角度來看

從invokeWithinTransaction中看出,如果我們在事務方法中,手動捕獲了異常,但并未向上抛出,則已經修改的資料不會復原。

例如:

@Transactional
    public void save() {
        try {
            //a将不會進行復原 
            Student a = new Student(null, "a", 18);
            studentDao.insert(a);

            //模拟業務異常
            int i = 1 / 0;
            
            Student b = new Student(null, "b", 18);
            studentDao.insert(b);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }      

當然,也不是抛出什麼異常都會進行復原的。

首先當原方法出現異常時,會進入到completeTransactionAfterThrowing中,最終會在RuleBasedTransactionAttribute的rollbackOn方法中進行判斷。

public boolean rollbackOn(Throwable ex) {

    RollbackRuleAttribute winner = null;
    int deepest = Integer.MAX_VALUE;

        //@Transactional中的rollbackFor的值将會在項目啟動的時候注入到rollbackRules中
        //将抛出的異常在rollbackRules中進行比對
    if (this.rollbackRules != null) {
      for (RollbackRuleAttribute rule : this.rollbackRules) {
        int depth = rule.getDepth(ex);
        if (depth >= 0 && depth < deepest) {
          deepest = depth;
          winner = rule;
        }
      }
    }


    //如果沒有比對到,則采用預設比對政策
    if (winner == null) {
      return super.rollbackOn(ex);
    }

    return !(winner instanceof NoRollbackRuleAttribute);
  }      

預設比對政策處于父類DefaultTransactionAttribute中

public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
  }      

可以看出,如果抛出的異常在rollbackFor中比對不到時,将會采用預設政策。在預設政策中,如果異常屬于運作時異常或錯誤時,也将進行復原。

在Java的異常體系中,Throwable下的繼承關系如下圖:

從源碼中窺探出事務失效的8種原因

Throwable分為Error和Exception,其中Exception又分為受檢異常(也稱運作時異常)和受檢查異常(編譯器強制手動捕獲)。

如果不在rollbackFor中指定受檢異常時,事務方法将不會進行復原。

例如:

@Transactional
    public void save() throws FileNotFoundException {
        //不會進行復原
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);
        throw new FileNotFoundException();
    }      

隻有在rollbackFor中指定受檢異常,或直接指定Exception時,才會進行復原。

@Transactional(rollbackFor = Exception.class)
    public void save() throws FileNotFoundException {
        //會進行復原
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);
        throw new FileNotFoundException();
    }      

從傳播行為角度來看

多個事務之間可能存在互相調用,那麼在發生異常的情況下,怎麼控制多個事務的復原呢?

怎麼指定部分事務不復原,其餘部分事務進行復原呢?這就用到了事務的傳播行為。

在Propagation枚舉類中,共定義了七種傳播行為,它們的特性為(針對于子方法):

傳播行為 特性
REQUIRED
  1. 外圍方法開啟事務,子方法直接加入到外圍事務中,形成一個整體事務。
  2. 外圍方法沒開啟事務,則子方法建立獨立的事務,不同子方法建立的事務互不幹擾,可以獨立復原或送出。
SUPPORTS
  1. 外圍方法開啟事務,子方法一起加入到外圍事務中,形成一個整體事務。
  2. 外圍方法沒開啟事務,則子方法直接不使用事務。
MANDATORY
  1. 外圍方法開啟事務,子方法一起加入到外圍事務中,形成一個整體事務。
  2. 外圍方法沒開啟事務,則子方法直接抛出異常,強制需要外圍方法有事務。
REQUIRES_NEW
  1. 不管外圍方法有沒有開啟事務,子方法都會建立一個屬于自己的事務,子方法建立的事務互不幹擾,與外圍方法也互不幹擾。
NOT_SUPPORTED
  1. 不管外圍方法有沒有開啟事務,子方法都不想去使用事務,外圍方法復原時,不會復原子方法。
NEVER
  1. 外圍方法開啟事務,則子方法直接抛出異常。
  2. 外圍方法沒開啟事務,子方法也不會使用事務。
NESTED
  1. 外圍方法開啟事務,外圍事務復原時,子事務會全部復原。但某一個子事務由于異常復原時,不會影響外圍事務與其他子事務。
  2. 外圍方法沒開啟事務,同REQUIRED(2)。

假如在某次開發中,本來所有的方法調用應該在一個事務中(propagation = Propagation.REQUIRED),但錯誤地指定為了REQUIRES_NEW。

首先我們調用TestService的save方法:

@Transactional
    public void save() {
        studentService.saveA();
        studentService.saveB();
    }      

saveA與saveB在另外一個service類中:

@Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveA() {
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveB() {
        Student b = new Student(null, "b", 20);
        studentDao.insert(b);
        //模拟業務異常
        int i = 1 / 0;
    }      

這樣saveB在出現異常後,資料b可以被復原,但資料a不會被復原,形成髒資料。

 對七種傳播行為的測試,可以參考我的另外一篇部落格​​Spring事務的傳播行為​​

從方法調用角度來看

如果在一個service中,非事務方法調用事務方法,接着controller調用非事務方法時,則事務方法内的事務失效。

即調用saveA時,saveB裡的事務會失效,即資料a不會被復原。

public void saveA(){
        saveB();
    }

    @Transactional
    public void saveB() {
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);
        int i = 1 / 0;
    }      

這是為什麼呢?很簡單

當controller調用saveA時,并不會被代理對象攔截到,因為不需要為saveA生成代理方法。那麼就會直接執行原對象的方法,這時候的saveB,相當于this.saveB,即調用原對象的saveB方法,沒經過代理對象調用,是以事務不會生效。

那怎麼修改,使得事務生效呢?

可以把this.saveB改為代理對象.saveB,需要先在該service中注入自己。

@Service
public class TestService {
    
    @Autowired
    TestService testService;

    public void saveA() {
        testService.saveB();
    }

    @Transactional
    public void saveB() {
        //事務生效
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);
        int i = 1 / 0;
    }

}      

在本類中注入自己,很奇怪啊,不會發生循環依賴嗎?

當然,Spring本身具有解決循環依賴的能力,下圖以A依賴A為例,解釋解決循環依賴的具體過程:

從源碼中窺探出事務失效的8種原因

如果想要了解更多關于Spring是如何解決循環依賴的知識點,可以參考我的另外一篇文章​​手把手教你解決循環依賴,一步一步地來窺探出三級緩存的奧秘​​

那事務方法調用事務方法,不過是以多線程的方式調用的呢?

例如:

@Transactional
    public void saveA() {
        Student a = new Student(null, "a", 18);
        studentDao.insert(a);

        new Thread(()->{
            Student b = new Student(null, "b", 18);
            studentDao.insert(b);
        }).start();

        int i=1/0;
    }      

資料b也不會被復原,事務失效了。

在TransactionSynchronizationManager類中,會将目前的事務資訊利用ThreadLocal綁定到線程中,也就是說,每個線程使用的是單獨的資料庫連接配接,分别存在于不同的事務中。是以出現異常後,資料a所處的事務會復原,而資料b處在另外一個事務中,不會進行復原。

從源碼中窺探出事務失效的8種原因

從外部條件角度來看

在MySQL5.5版本之前,其預設的存儲引擎為MyISAM,它特點有

  • 不支援事務
  • 不支援外鍵
  • 不支援行鎖,隻支援表鎖
  • 為聚集索引
  • 支援全文索引

如果我們操作的表屬于MyISAM類型,則事務是不可能生效的。

MySQL5.5之後,預設存儲引擎改為了InnoDB,這個是支援事務的。

是以,小夥伴們在發現事務不生效,抓耳撓腮始終找不到原因時,别忘了偷瞄一下表的類型。

總結

事務失效或不復原的情況,可以從以下的角度來思考:

從代理角度來看

1、方法的修飾符不是public類型的,另外final與static也是不支援事務的。

2、事務方法所處的對象并沒有Spring管理,自然也不會生成代理對象。

從異常處理以及異常類型角度來看

1、事務方法中手動捕獲異常,但并未向上抛出,那麼該方法不會進行復原。

2、事務方法中抛出的異常,不在rollbackFor中,也不屬于RuntimeException或者Error時,不會進行復原。

從傳播行為角度來看

1、傳播行為指定錯誤,例如:内部方法和外部方法不在一個事務中,内部方法復原後,外部方法沒有進行復原

從方法調用角度來看

從外部條件角度來看