1. 程式人生 > >深入淺出Spring系列 ---- 之 SpringBoot 事務原理

深入淺出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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

該方法主要邏輯:

  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!!!!