深入理解 Spring 之 SpringBoot 事務原理

前言

今天是平安夜,先祝大家平安夜快樂。

我們之前的數十篇文章分析了 Spring 和 Mybatis 的原理,基本上從源碼層面都瞭解了他們的基本原理,那麼。在我們日常使用這些框架的時候,還有哪些疑問呢?就樓主而言,樓主已經明白了 IOC ,AOP 的原理,也明白了 Mybatis 的原理,也明白了 Spring 和 Mybatis 是如何整合的。但是,我們漏掉了 JavaEE 中一個非常重要的特性:事務。事務是 Java 程序員開發程序時不可避免的問題。我們就不討論 ACID 的事務特性,樓主這裏假定大家都已經了了解了事務的原理。如果還不瞭解,可以先去谷歌看看。那麼,我們今天的任務是剖析源碼,看看Spring 是怎麼運行事務的,並且是基於當前最流行的SpringBoot。還有,我們之前剖析Mybatis 的時候,也知道,Mybatis 也有事務,那麼,他倆融合之後,事務是交給誰的?又是怎麼切換的?今天這幾個問題,我們都要從源碼中找到答案。

1. Spring 的事務如何運行?

如果各位使用過SpringBoot ,那麼就一定知道如何在Spring中使用註解,比如在一個類或者一個方法上使用 @Transactional 註解,在一個配置類上加入一個 @EnableTransactionManagement 註解代表啓動事務。而這個配置類需要實現 TransactionManagementConfigurer 事務管理器配置接口。並實現 annotationDrivenTransactionManager 方法返回一個包含了 配置好數據源的 DataSourceTransactionManager 事務對象。這樣就完成了事務配置,就可以在Spring使用事務的回滾或者提交功能了。

這個 saveList 方法就在Spring事務的控制之下,如果發生了異常,就會回滾事務。如果各位知道更多的Spring的事務特性,可以在註解中配置,比如什麼異常才能回滾,比如超時時間,比如隔離級別,比如事務的傳播。就更有利於理解今天的文章了。

我們基於一個 Junit 測試用例,來看看Spring的事務時如何運行的。

在測試用例中執行該方法,參數時一個空的List,這個Sql的運行肯定是失敗的。我們主要看看他的運行過程。我們講斷點打在該方法上。斷點進入該方法。

注意,dataCollectionShareService 對象已經被 Cglib 代理了,那麼他肯定會走 DynamicAdvisedInterceptor 的 intercept 方法,我們斷點進入該方法查看,這個方法我們已經很屬性了,該方法中,最重要的事情就是執行通知器或者攔截器的方法,那麼,該代理有通知器嗎?

有一個通知器。是什麼呢?

一個事務攔截器,也就是說,如果通知器鏈不爲空,就會依次執行通知器鏈的方法。那麼 TransactionInterceptor 到底是什麼呢?

該類實現了通知器接口,也實現類 MethodInterceptor 接口,並實現了該接口的 invoke 方法,在 DynamicAdvisedInterceptor 的 intercept 方法中,最終會調用每個 MethodInterceptor 的 invoke 方法,那麼,TransactionInterceptor 的 invoke 方法是如何實現的呢?

invoke 方法中會調用自身的 invokeWithinTransaction 方法,看名字,該方法和事務相關。該方法參數是由目標方法,目標類,一個回調對象構成。 那麼我們就進入該方法查看,該方法很長:

    /**
     * General delegate for around-advice-based subclasses, delegating to several other template
     * methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager}
     * as well as regular {@link PlatformTransactionManager} implementations.
     * @param method the Method being invoked
     * @param targetClass the target class that we're invoking the method on
     * @param invocation the callback to use for proceeding with the target invocation
     * @return the return value of the method, if any
     * @throws Throwable propagated from the target invocation
     */
    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, txAttr);

        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
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                cleanupTransactionInfo(txInfo);
            }
            commitTransactionAfterReturning(txInfo);
            return retVal;
        }

        else {
            // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
            try {
                Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr,
                        new TransactionCallback<Object>() {
                            @Override
                            public Object doInTransaction(TransactionStatus status) {
                                TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
                                try {
                                    return invocation.proceedWithInvocation();
                                }
                                catch (Throwable ex) {
                                    if (txAttr.rollbackOn(ex)) {
                                        // A RuntimeException: will lead to a rollback.
                                        if (ex instanceof RuntimeException) {
                                            throw (RuntimeException) ex;
                                        }
                                        else {
                                            throw new ThrowableHolderException(ex);
                                        }
                                    }
                                    else {
                                        // A normal return value: will lead to a commit.
                                        return new ThrowableHolder(ex);
                                    }
                                }
                                finally {
                                    cleanupTransactionInfo(txInfo);
                                }
                            }
                        });

                // Check result: It might indicate a Throwable to rethrow.
                if (result instanceof ThrowableHolder) {
                    throw ((ThrowableHolder) result).getThrowable();
                }
                else {
                    return result;
                }
            }
            catch (ThrowableHolderException ex) {
                throw ex.getCause();
            }
        }
    }

該方法主要邏輯:

  1. 獲取事務屬性,根據事務屬性,獲取事務管理器。
  2. 判斷屬性是否空,或者事務管理器是否不是 CallbackPreferringPlatformTransactionManager 類型,如果是該類型,則會執行事務管理器的 execute 方法。
  3. 生成一個封裝了事務管理器,事務屬性,方法簽名字符串,事務狀態對象 的 TransactionInfo 事務信息對象。該對象會在事務回滾或者失敗時起作用。
  4. 調用目標對象方法或者是下一個過濾器的方法。
  5. 如果方法由異常則執行 completeTransactionAfterThrowing 方法,調用事務管理器的回滾方法。如果沒有異常,調用 commitTransactionAfterReturning 提交方法。最後返回返回值。

可以說,該方法就是Spring 事務的核心調用,根據目標方法是否有異常進行事務的回滾。

那麼,我們需要一行一行的看看該方法實現。

首先看事務的屬性。

2. TransactionAttribute 事務屬性

invokeWithinTransaction 方法中調用了 自身的 getTransactionAttributeSource 方法返回一個TransactionAttributeSource 對象,並調用該對象的 getTransactionAttribute 方法,參數是目標方法和目標類對象。首先看 getTransactionAttributeSource 方法,該方法直接返回了抽象類 TransactionAspectSupport 中定義的 TransactionAttributeSource 屬性。該屬性的是什麼時候生成的我們稍後再說。我們debug 後返回的是 TransactionAttributeSource 接口的實現類 AnnotationTransactionAttributeSource ,看名字,註解事務屬性資源,名字起的好很重要啊。我們進入該類查看。

這是該類的繼承機構圖。我們重點還是關注該類的 getTransactionAttribute 方法,該方法有抽象類 AbstractFallbackTransactionAttributeSource 也就是 AnnotationTransactionAttributeSource 的父類完成。我們看看該方法。

該方法大部分都是緩存判斷,最重要的一行代碼樓主已紅框標出。computeTransactionAttribute 方法,計算事務屬性。進入該方法查看:

該方法是返回事務屬性的核心方法,首先,根據 class 和 method 對象,生成一個完整的method 對象,然後調用 findTransactionAttribute 方法,參數就是該 method 對象,findTransactionAttribute 方法是抽象方法,由子類實現,可見 computeTransactionAttribute 是個模板方法模式。那麼我們就看看他的子類 AnnotationTransactionAttributeSource 是如何實現的。該方法調用了自身的 determineTransactionAttribute 方法。該方法實現入下:

該方法會判斷該 Method 對象是否含有註解。並循環 AnnotationTransactionAttributeSource 對象的 annotationParsers 註解解析器集合,對該方法進行解析。如果解析成功,則返回該註解元素。我想我們也已經猜到了,這個註解解析器解析的就是 @Transactional 註解。

3. @Transactional 註解解析器 SpringTransactionAnnotationParser

我們說AnnotationTransactionAttributeSource 對象中又多個解析器。那麼這些解析器是什麼時候生成的呢?構造方法中生成的。

該構造方法由一個布爾屬性,然後創建一個鏈表,也創建一個 SpringTransactionAnnotationParser 對象添加進鏈表中。這樣就完成了解析器的創建。構造方法什麼時候調用的呢?我們稍後再講。

我們看看註解解析器是怎麼解析方法對象的。

首先根據指定的 Transactional 註解和給定的方法,調用工具方法 getMergedAnnotationAttributes ,獲取方法上的註解屬性。然後調用重載方法 parseTransactionAnnotation 。

可以看到,該方法首先創建了一個 RuleBasedTransactionAttribute 對象,然後一個個解析註解中的元素,並將這些元素設置到 RuleBasedTransactionAttribute 對象中,注意,其中有個 RollbackRuleAttribute 的集合,存儲着該註解屬性的回滾相關的屬性。最後添加到 RuleBasedTransactionAttribute 的RollbackRules 集合中。

到這裏,就完成了解析器的解析。返回了一個 RuleBasedTransactionAttribute 對象。

回到 攔截器的 invokeWithinTransaction 方法中,此時已經獲取了 屬性對象。根據方法,也就是說,如果返回值是null,說明該方法沒有事務註解,在 getTransactionAttribute 方法中,也會將該方法作爲key ,NULL_TRANSACTION_ATTRIBUTE 作爲 value,放入緩存,如果不爲null,那麼就將 TransactionAttribute 作爲 value 放入緩存。

有了事務屬性,再獲取事務管理器。也就是 determineTransactionManager 方法。

4. 事務管理器。

我們注意到,調用了自身的 determineTransactionManager 方法,返回了一個 PlatformTransactionManager 事務管理器。這個事務管理器就是我們在我們的配置類中寫的:

那麼這個事務管理器是什麼呢?事務管理器就是真正執行事務回滾或提交的執行單位,我們看看該類:

繼承圖:

 

結構圖:

 

紅框標註的方法就是執行正在事務邏輯的方法,其中又封裝了數據源,也就是 JDBC 的 Connection 。比如 doCommit 方法:

我們看看determineTransactionManager 是如何獲取事務管理器的。

該方法步驟入下:

  1. 如果事務屬性爲null 或者 容器工廠爲null,則返會自身的 transactionManager 事務管理器。
  2. 如果都不爲null,則獲取事務屬性的限定符號,根據限定符從容器中獲取 事務管理器。
  3. 如果沒有限定符,則根據事務管理器的BeanName從容器中獲取。
  4. 如果都沒有,則獲取自身的事務管理器,如果自身還沒有,則從緩存中取出默認的。如果默認的還沒有,則從容器中獲取PlatformTransactionManager 類型的事務管理器,最後返回。

這裏重點是自身的事務管理器從何而來?我們先按下不表。

到這裏,我們已經有了事務管理器。就需要執行 invokeWithinTransaction 下面的邏輯了。回到 invokeWithinTransaction 方法,我們的返回值肯定滿足第一個if 條件,因爲我們的事務管理器不是 CallbackPreferringPlatformTransactionManager 類型的。進入if 塊。

首先創建一個事務信息對象。該類是什麼呢?

屬性:

 

構造方法:

 

該類包含了一個 事務管理器,事務屬性,事務方法字符串。

接着執行回調類InvocationCallback 的 proceedWithInvocation 方法,該方法會執行下一個通知器的攔截方法(如果有的話),最後執行目標方法,這裏,目標方法被 try 住了,如果發生異常,則執行completeTransactionAfterThrowing 方法,並拋出異常,在 finally 塊中執行清理工作。如果成功執行,則執行
commitTransactionAfterReturning 方法。最後返回目標方法返回值。

我們重點看看 completeTransactionAfterThrowing 方法和 commitTransactionAfterReturning 方法。

5. TransactionInterceptor 的 completeTransactionAfterThrowing 方法(事務如何回滾)。

該方法主要內容在紅框中,首先判斷該事務對象是否和該異常匹配,如果匹配,則回滾,否則,則提交。那麼,是否匹配的邏輯是怎麼樣的呢?我們的事務屬性是什麼類型的?RuleBasedTransactionAttribute ,就是我們剛剛創建解析註解後創建的。那麼我就看看該類的 rollbackOn 方法:

首先,循環解析註解時添加進集合的回滾元素。並遞歸調用RollbackRuleAttribute 的 getDepth 方法,如果這個異常的名字和註解中的異常名字匹配,則返回該異常的回滾類型。最後判斷,如果沒有匹配到,則調用父類的 rollbackOn 方法,如果匹配到了,並且該屬性類型不是 NoRollbackRuleAttribute 類型,返回true。表示匹配到了,可以回滾。那麼父類的 rollbackOn 方法肯定就是默認的回滾方法了。

這是父類的 rollbackOn 方法:

該方法判斷,該異常如果是 RuntimeException 類型異常或者 是 Error 類型的,就回滾。這就是默認的回滾策略。

那麼我們的方法肯定是匹配的 RuntimeException 異常,就會執行下面的方法。

可以看到,這行代碼就是執行了我們的事務管理器的 rollback 方法,並且攜帶了事務狀態對象。該方法實現在抽象類 AbstractPlatformTransactionManager 中,調用了自身的 processRollback 方法做真正的實現。

該方法首先切換事務狀態,其實就是關閉SqlSession。

然後調用 doRollback 方法。

首先,從狀態對象中獲取數據庫連接持有對象,然後獲取數據庫連接,調用 Connection 的 rollback 方法,也就是我們學習JDBC 時使用的方法。最後修改事務的狀態。

到這裏,事務的回滾就結束了。

那麼,事務時如何提交的呢?

6. TransactionInterceptor 的 commitTransactionAfterReturning 方法(事務如何提交)。

該方法簡單的調用了事務管理器的 commit 方法。

AbstractPlatformTransactionManager 的 commit 方法。

首先判斷了事務的狀態,如果狀態不匹配,則調用回滾方法。如果狀態正常,執行 processCommit 方法。該方法很長,樓主只截取其中一段:

首先,commit 之前做一些狀態切換工作。最重要的是執行 doCommit 方法,如果異常了,則回滾。那麼 DataSourceTransactionManager 的 doCommit 是如何執行的呢?

可以看到,底層也是調用 JDBC 的 Connection 的 commit 方法。

到這裏,我們就完成了數據庫的提交。

7. 事務運行之前做了哪些工作?

從前面的分析,我們已經知道了事務是如何運行的,如何回滾的,又是如何提交的。在這是交互型的框架裏,事務系統肯定做了很多的準備工作,同時,我們留下了很多的疑問,比如事務管理器從何而來? TransactionAttributeSource 屬性何時生成?AnnotationTransactionAttributeSource 構造什麼時候調用?

我們一個個的來解釋。

在Spring 中,有一個現成的類,ProxyTransactionManagementConfiguration,我們看看該類:

看到這個類,應該可以解開我們的疑惑,這個類標註了配置註解,會在IOC的時候實例化該類,而該類中產生了幾個Bean,比如事務攔截器 TransactionInterceptor,創建了 AnnotationTransactionAttributeSource 對象,並向事務攔截器添加了事務管理器。最後,將事務攔截器封裝成通知器。那麼,剩下最後一個問題就是,事務管理器從何而來?答案是他的父類 AbstractTransactionManagementConfiguration :

該類也是個配置類,自動注入了 TransactionManagementConfigurer 的配置集合,而並且尋找了配置 EnableTransactionManagement 註解的類,而我們在我們的項目中就是按照這個標準來實現的:

我們關聯這兩個類就能一目瞭然,Spring在啓動的時候,會加載這兩個配置類,在對 AbstractTransactionManagementConfiguration 的 setConfigurers 方法進行注入的時候,會從容器中找到對應類型的配置,並調用配置類的 annotationDrivenTransactionManager 方法,也就是我們實現的方法,獲取到我們創建的 DataSourceTransactionManager 類。這樣,我們的事務攔截器相關的類就完成了在Spring中的依賴關係。

但是,這個時候Spring中的事務運行還沒有搭建完成。比如什麼時候創建類的代理?根據什麼創建代理,因爲我們知道,Spring 中的事務就是使用AOP來完成的,必須使用動態代理或者 Cglib 代理來對目標方法進行攔截。

這就要複習我們之前的Spring IOC 的啓動過程了。Spring 在創建bean的時候,會對每個Bean 的所有方法進行遍歷,如果該方法匹配系統中任何一個攔截器的切點,就創建一個該Bean的代理對象。並且會將對應的通知器放入到代理類中。以便在執行代理方法的時候進行攔截。

具體代碼步驟樓主貼一下:

  1. 在對bean 進行初始化的時候會執行 AutowireCapableBeanFactory 接口的 applyBeanPostProcessorsAfterInitialization 的方法,其中會遍歷容器中所有的bean後置處理器,後置處理器會調用 postProcessAfterInitialization 方法對bean進行處理。

  1. 在處理過程中,對bean 進行包裝,也就是代理的創建,調用 getAdvicesAndAdvisorsForBean 方法,該方法會根據bean的信息獲取到對應的攔截器並創建代理,創建代理的過程我們之前已經分析過了,不再贅述。

  1. 尋找匹配攔截器過程:首先找到所有的攔截器,然後,根據bean的信息進行匹配。

  1. 匹配的過程就是,找到目標類的所有方法,遍歷,並調用攔截器的方法匹配器對每個方法進行匹配。方法匹配器就是事務攔截器中的 BeanFactoryTransactionAttributeSourceAdvisor 類,該類封裝了 AnnotationTransactionAttributeSource 用於匹配事務註解的匹配器。

  1. 最終調用方法匹配器中封裝的註解解析器解析方法,判斷方法是否含有事務註解從而決定是否生成代理:

到這裏,就完成了所有事務代理對象的創建。

項目中的每個Bean都有了代理對象,在執行目標方法的時候,代理類會查看目標方法是否匹配代理中攔截器的方法匹配器中定義的切點。如果匹配,則執行攔截器的攔截方法,否則,直接執行目標方法。這就是含有事務註解和不含有事務註解方法的執行區別。

到這裏,我們還剩下最後一個問題,我們知道,在分析mybatis 的時候,mybatis 也有自己的事務管理器,那麼他們融合之後,他們的事務管理權在誰的手上,又是根據什麼切換的呢?

8. mybatis 和 Spring 的事務管理權力之爭

我們之前說過,在Spring中,mybatis 有 SqlSessionTemplate 代理執行,其實現類動態代理的 InvocationHandler 方法,那麼最重要的方法就是 invoke 方法,其實這個方法我們已經看過了,今天再看一遍:

我們今天重點關注是否提交(報錯肯定回滾),其中紅框標出來的 if 判斷,就是判斷這個事務到底是Spring 來提交,還是 mybatis 來提交,那麼我們看看這個方法 isSqlSessionTransactional :

該方法從Spring 的容器中取出持有 SqlSession 的 持有類,判斷Spirng 持有的 SqlSession 和 Mybatis 持有的是否是同一個,如果是,則交給Spring,否則,Mybatis 自己處理。可以說很合理。

總結

今天的這篇文章可以說非常的長,我們分析了 SpringBoot 的事務運行過程,事務環境的搭建過程,mybatis 的事務和 Spring 事務如何協作。知道了整個事務其實是建立在AOP的基礎之上,其核心類就是 TransactionInterceptor,該類就是 invokeWithinTransaction 方法是就事務處理的核心方法,其中封裝了我們創建的 DataSourceTransactionManager 對象,該對象就是執行回滾或者提交的執行單位 其實,TransactionInterceptor 和我們平時標註 @Aspect 註解的類的作用相同,就是攔截指定的方法,而在
TransactionInterceptor 中是通過是否標有事務註解來決定的。如果一個類中任意方法含有事務註解,那麼這個方法就會被代理。而Mybatis 的事務和Spring 的事務協作則根據他們的SqlSession 是否是同一個SqlSession 來決定的,如果是同一個,則交給Spring,如果不是,Mybatis 則自己處理。

通過閱讀源碼,我們已經弄清楚了SpirngBoot 整個事務的運行過程。實際上,Spring 的其他版本也大同小異。底層都是 TransactionInterceptor ,只不過入口不一樣。我相信,在以後的工作中,如果遇到了Spring事務相關的問題,再也不會感到無助了,因爲知道了原理,可以深入到源碼中查看。

到這裏,樓主的 Spring ,mybatis ,Tomcat 的源碼閱讀之路暫時就告一段落了。源碼只要領會精華即可。還有其他的知識需要花費更多的時間學習。比如併發,JVM.

good luck!!!!

 

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