Spring筆記(4) - Spring的程式設計式事務和宣告式事務詳解

碼猿手發表於2020-10-20

一.背景

  1. 事務管理對於企業應用而言至關重要。它保證了使用者的每一次操作都是可靠的,即便出現了異常的訪問情況,也不至於破壞後臺資料的完整性。就像銀行的自助取款機,通常都能正常為客戶服務,但是也難免遇到操作過程中機器突然出故障的情況,此時,事務就必須確保出故障前對賬戶的操作不生效,就像使用者剛才完全沒有使用過取款機一樣,以保證使用者和銀行的利益都不受損失。
  2. 事務特性

    • 原子性(Atomicity):事務是一個原子操作,由一系列動作組成。事務的原子性確保動作要麼全部完成,要麼完全不起作用;
    • 一致性(Consistency):一旦事務完成(不管是成功還是失敗),系統必須確保它所建模的業務處於一致的狀態,而不會是部分完成部分失敗。在現實中的資料不應該被破壞;
    • 隔離性(Isolation):可能有許多事務會同時處理相同的資料,因此每個事務都應該與其他事務隔離開來,防止資料損壞;
    • 永續性(Durability):一旦事務完成,無論發生什麼系統錯誤,它的結果都不應該受到影響,這樣就能從任何系統崩潰中恢復過來。通常情況下,事務的結果被寫到持久化儲存器中;
  3. 事務型別

    1. 資料庫分為本地事務和全域性事務
      1. 本地事務:普通事務,獨立一個資料庫,能保證在該資料庫上操作的ACID;
      2. 分散式事務:涉及兩個或多個資料庫源的事務,即跨越多臺同類或異類資料庫的事務(由每臺資料庫的本地事務組成),分散式事務旨在保證這些本地事務的所有操作的ACID,使事務可以跨越多臺資料庫;
    2. Java事務型別分為JDBC事務和JTA事務
      1. JDBC事務:即為上面說的資料庫事務中的本地事務,通過connection物件控制管理;
      2. JTA事務:指Java事務API(Java Transaction API),是Java EE資料庫事務規範,JTA只提供了事務管理介面,由應用程式伺服器廠商(如WebSphere Application Server)提供實現,JTA事務比JDBC更強大,支援分散式事務;
    3. 按是否通過程式設計分為宣告式事務和程式設計式事務
      1. 程式設計式事務:通過程式設計程式碼在業務邏輯時需要時自行實現,粒度更小;
      2. 宣告式事務:通過註解或XML配置實現;
  4. Spring事務管理的兩種方式

    • 程式設計式事務

        • 是侵入性事務管理,直接使用底層的PlatformTransactionManager、使用TransactionTemplate(Spring推薦使用);

        • 程式設計式事務管理對基於 POJO 的應用來說是唯一選擇。我們需要在程式碼中呼叫beginTransaction()、commit()、rollback()等事務管理相關的方法;

    • 宣告式事務:該事務是建立在AOP之上的,其本質是對方法前後進行攔截,然後在目標方法開始之前建立或加入一個事務,在執行完目標方法之後根據執行情況提交或回滾事務。

      Spring配置檔案中關於事務配置總是由三個組成部分,分別是DataSource、TransactionManager和代理機制這三部分,無論哪種配置方式,一般變化的只是代理機制這部分。

      DataSource、TransactionManager這兩部分只是會根據資料訪問方式有所變化,比如使用Hibernate進行資料訪問時,DataSource實際為SessionFactory,TransactionManager的實現為HibernateTransactionManager。

      根據代理機制的不同,總結了五種Spring事務的配置方式,如下圖:

      • 優點:

        • 程式設計式事務每次實現都要單獨實現,但業務量大且功能複雜時,使用程式設計性事務無疑是痛苦的;而宣告式事務不同,宣告式事務屬於非侵入性,不會影響業務邏輯的實現,只需在配置檔案中做相關的事務規則宣告(或通過基於@Transactional註解的方式),便可以將事務規則應用到業務邏輯中;

        • 非侵入式的開發方式,宣告式事務管理使業務程式碼不受汙染,一個普通的POJO物件,只要加上註解就可以獲得完全的事務支援;

      • 缺點:最細粒度只能是作用到方法級別,無法做到像程式設計事務那樣可以作用到程式碼塊級別;

      • 實現方式:

        1. 使用攔截器:基於TransactionInterceptor 類來實施宣告式事務管理功能(Spring最初提供的實現方式);

        2. Bean和代理:基於 TransactionProxyFactoryBean的宣告式事務管理

        3. 使用tx標籤配置的攔截器:基於tx和aop名字空間的xml配置檔案(基於Aspectj AOP配置事務);
        4. 全註解:基於@Transactional註解;

      • 宣告式事務的約定流程:

          首先Spring通過事務管理器(PlatformTransactionManager的子類)建立事務,與此同時會把事務定義中的隔離級別、超時時間等屬性根據配置內容往事務上設定。而根據傳播行為配置採取一種特定的策略,後面會談到傳播行為的使用問題,這是Spring根據配置完成的內容,你只需要配置,無須編碼。然後,啟動開發者提供的業務程式碼,我們知道Spring會通過反射的方式排程開發者的業務程式碼,但是反射的結果可能是正常返回或者產生異常返回,那麼它給的約定是隻要發生異常,並且符合事務定義類回滾條件的,Spring就會將資料庫事務回滾,否則將資料庫事務提交,這也是Spring自己完成的。


  5. Spring事務特性

    • Spring 框架中,涉及到事務管理的 API 大約有100個左右,其中最重要的有三個:TransactionDefinition、PlatformTransactionManager、TransactionStatus。所謂事務管理,其實就是”按照給定的事務規則來執行提交或者回滾操作”。”給定的事務規則”就是用 TransactionDefinition 表示的,”按照……來執行提交或者回滾操作”便是用 PlatformTransactionManager 來表示,而 TransactionStatus 用於表示一個執行著的事務的狀態。打一個不恰當的比喻,TransactionDefinition 與 TransactionStatus 的關係就像程式和程式的關係。
      • Spring所有的事務管理策略類都繼承自org.springframework.transaction.PlatformTransactionManager介面,用於執行具體的事務操作。PlatformTransactionManager 介面中定義的主要方法如下:

        public interface PlatformTransactionManager{
           TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;//獲得當前事務狀態
           void commit(TransactionStatus status)throws TransactionException;//提交事務
           void rollback(TransactionStatus status)throws TransactionException;//回滾事務
        }

        根據底層所使用的不同的持久化 API 或框架,PlatformTransactionManager 的主要實現類大致如下:

        • DataSourceTransactionManager:適用於使用JDBC和iBatis進行資料持久化操作的情況。
        • HibernateTransactionManager:適用於使用Hibernate進行資料持久化操作的情況。
        • JpaTransactionManager:適用於使用JPA進行資料持久化操作的情況。
        • 另外還有JtaTransactionManager 、JdoTransactionManager、JmsTransactionManager等等。

                如果我們使用JTA進行事務管理,我們可以通過 JNDI 和 Spring 的 JtaTransactionManager 來獲取一個容器管理的 DataSource。JtaTransactionManager 不需要知道 DataSource 和其他特定的資源,因為它將使用容器提供的全域性事務管理。而對於其他事務管理器,比如DataSourceTransactionManager,在定義時需要提供底層的資料來源作為其屬性,也就是 DataSource。與 HibernateTransactionManager 對應的是 SessionFactory,與 JpaTransactionManager 對應的是 EntityManagerFactory 等等。

      • TransactionDefinition介面的主要方法如下:

        public interface TransactionDefinition{
            int getIsolationLevel();//返回事務的隔離級別,事務管理器依據它來控制另外一個事務能夠看到本事務內的哪些資料。
            int getPropagationBehavior();//返回事務的傳播行為,由是否有一個活動的事務來決定一個事務呼叫。
            int getTimeout();//它返回事務必須在多少秒內完畢。
            boolean isReadOnly();//事務是否僅僅讀,事務管理器可以依據這個返回值進行優化。確保事務是僅僅讀的。
        }

                也許你會奇怪,為什麼介面只提供了獲取屬性的方法,而沒有提供相關設定屬性的方法。其實道理很簡單,事務屬性的設定完全是程式設計師控制的,因此程式設計師可以自定義任何設定屬性的方法,而且儲存屬性的欄位也沒有任何要求。唯一的要求的是,Spring 進行事務操作的時候,通過呼叫以上介面提供的方法必須能夠返回事務相關的屬性取值。

      • TransactionStatus:PlatformTransactionManager.getTransaction(…) 方法返回一個 TransactionStatus 物件,表示一個事務的狀態。返回的TransactionStatus 物件可能代表一個新的或已經存在的事務(如果在當前呼叫堆疊有一個符合條件的事務)。TransactionStatus 介面提供了一個簡單的控制事務執行和查詢事務狀態的方法。比如當前呼叫棧中之前已經存在了一個事務,那麼就是通過該介面來判斷的,TransactionStatus介面可以讓事務管理器控制事務的執行,比如檢查事務是否為一個新事務,或者是否只讀,TransactionStatus還可以初始化回滾操作。

        TransactionStatus 介面中定義的主要方法如下:

        public interface TransactionStatus extends SavepointManager, Flushable {
        
            //是否是一個新的事務
            boolean isNewTransaction();
        
            //判斷是否有回滾點
            boolean hasSavepoint();
        
            //將一個事務標識為不可提交的。在呼叫完setRollbackOnly()後只能被回滾
            //在大多數情況下,事務管理器會檢測到這一點,在它發現事務要提交時會立刻結束事務。
            //呼叫完setRollbackOnly()後,數資料庫可以繼續執行select,但不允許執行update語句,因為事務只可以進行讀取操作,任何修改都不會被提交。
            void setRollbackOnly();
            boolean isRollbackOnly();
        
            @Override
            void flush();
            //判斷事務是否已經完成
            boolean isCompleted();
        } 
    • TransactionDefinition介面定義以下特性:

    1. 事務隔離級別:指若干個併發的事務之間的隔離程度,TransactionDefinition介面中定義了5個表示隔離級別的常量

      1. TransactionDefinition.ISOLATION_DEFAULT:預設值-1,表示使用底層資料庫的預設隔離級別,對大部分資料庫而言,通常這值就是TransactionDefinition.ISOLATION_READ_COMMITTED;

      2. TransactionDefinition.ISOLATION_READ_UNCOMMITTED:該隔離級別表示一個事務可以讀取另一個事務修改但還沒有提交的資料,該級別可能導致髒讀、不可重複讀和幻讀,因此很少使用該隔離級別,比如PostgreSQL實際上並沒有此級別;

      3. TransactionDefinition.ISOLATION_READ_COMMITTED:(Oracle預設級別)該隔離級別表示一個事務只能讀取另一個事務已經提交的資料,即允許從已經提交的併發事務讀取,該級別可以防止髒讀,但幻讀和不可重複讀仍可能會發生;

      4. TransactionDefinition.ISOLATION_REPEATABLE_READ:(MySQL預設級別)該隔離級別表示一個事務在整個過程中可以多次重複執行某個查詢,並且每次返回的記錄都相同,即對相同欄位的多次讀取的結果是一致的,除非資料被當前事務本事改變。該級別可以防止髒讀和不可重複讀,但幻讀仍可能發生;

      5. TransactionDefinition.ISOLATION_SERIALIZABLE:(完全服從ACID的隔離級別)所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀和幻讀,但嚴重影響程式的效能,因為它通常是通過完全鎖定當前事務所涉及的資料表來完成的;

      • 髒讀(Dirty read):發生在一個事務讀取了被另一個事務改寫但尚未提交的資料時。如果這些改變在稍後被回滾了,那麼第一個事務讀取的資料就會是無效的;

        不可重複讀(Nonrepeatable read):發生在一個事務執行相同的查詢兩次或兩次以上,但每次查詢結果都不相同時。這通常是由於另一個併發事務在兩次查詢之間更新了資料。(不可重複讀重點在修改)

        幻讀(Phantom reads):幻讀和不可重複讀相似。當一個事務(T1)讀取幾行記錄後,另一個併發事務(T2)插入了一些記錄時,幻讀就發生了。在後來的查詢中,第一個事務(T1)就會發現一些原來沒有的額外記錄。(幻讀重點在新增或刪除)

    2. 事務傳播機制:事務的傳播性一般用在事務巢狀的場景,比如一個事務方法裡面呼叫了另外一個事務方法,那麼兩個方法是各自作為獨立的方法提交還是內層的事務合併到外層的事務一起提交,這就需要事務傳播機制的配置來確定怎麼樣執行;在TransactionDefinition介面中定義了以下幾個表示傳播機制的常量,值為0~6:

      1. TransactionDefinition.PROPAGATION_REQUIRED:預設值,能滿足絕大部分業務需求,如果外層有事務,則當前事務加入到外層事務,一塊提交,一塊回滾。如果外層沒有事務,新建一個事務執行;

      2. TransactionDefinition.PROPAGATION_REQUIRES_NEW:該事務傳播機制是每次都會新開啟一個事務,同時把外層事務掛起,噹噹前事務執行完畢,恢復上層事務的執行。如果外層沒有事務,執行當前新開啟的事務即可; 

      3. TransactionDefinition.PROPAGATION_SUPPORTS:如果外層有事務,則加入外層事務;如果外層沒有事務,則直接以非事務的方式繼續執行。完全依賴外層的事務;

      4. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:該傳播機制不支援事務,如果外層存在事務則掛起,執行完當前程式碼,則恢復外層事務,無論是否異常都不會回滾當前的程式碼;

      5. TransactionDefinition.PROPAGATION_NEVER:該傳播機制不支援外層事務,即如果外層有事務就丟擲異常;

      6. TransactionDefinition.PROPAGATION_MANDATORY:與NEVER相反,如果外層有事務,則加入外層事務,如果外層沒有事務,則丟擲異常;

      7. TransactionDefinition.PROPAGATION_NESTED:該傳播機制的特點是可以儲存狀態儲存點,當前事務回滾到某一個點,從而避免所有的巢狀事務都回滾,即各自回滾各自的,如果子事務沒有把異常吃掉,基本還是會引起全部回滾的;

      • 傳播機制回答了這樣一個問題:一個新的事務應該被啟動還是被掛起,或者是一個方法是否應該在事務性上下文中執行。
      • 這裡需要指出的是,前面的六種事務傳播行為是 Spring 從 EJB 中引入的,他們共享相同的概念。而 PROPAGATION_NESTED是 Spring 所特有的。以 PROPAGATION_NESTED 啟動的事務內嵌於外部事務中(如果存在外部事務的話),此時,內嵌事務並不是一個獨立的事務,它依賴於外部事務的存在,只有通過外部的事務提交,才能引起內部事務的提交,巢狀的子事務不能單獨提交。如果熟悉 JDBC 中的儲存點(SavePoint)的概念,那巢狀事務就很容易理解了,其實巢狀的子事務就是儲存點的一個應用,一個事務中可以包括多個儲存點,每一個巢狀子事務。另外,外部事務的回滾也會導致巢狀子事務的回滾。
      • 掛起事務,指的是將當前事務的屬性如事務名稱,隔離級別等屬性儲存在一個變數中,同時將當前執行緒中所有和事務相關的ThreadLocal變數設定為從未開啟過執行緒一樣。Spring維護著一個當前執行緒的事務狀態,用來判斷當前執行緒是否在一個事務中以及在一個什麼樣的事務中,掛起事務後,當前執行緒的事務狀態就好像沒有事務。
    3. 只讀:如果一個事務只對資料庫執行讀操作,那麼該資料庫就可能利用那個事務的只讀特性,採取某些優化措施。通過把一個事務宣告為只讀,可以給後端資料庫一個機會來應用那些它認為合適的優化措施。由於只讀的優化措施是在一個事務啟動時由後端資料庫實施的,因此,只有對於那些具有可能啟動一個新事務的傳播行為(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法來說,將事務宣告為只讀才有意義,在 TransactionDefinition 中以 boolean 型別來表示該事務是否只讀。;
    4. 事務超時:指一個事務所允許執行的最長時間,如果超過該時間限制但事務還沒有完成,則自動回滾事務,在TransactionDefinition中以int的值來表示超時時間,其單位是秒;預設設定為底層事務系統的超時值,如果底層資料庫事務系統沒有設定超時值,那麼就是none,沒有超時限制;

    5. Spring事務回滾規則:預設配置下,Spring只有在丟擲的異常為執行時異常(runtime exception)時才回滾該事務,也就是丟擲的異常為RuntimeException的子類(Error也會導致事務回滾),而丟擲受檢查異常(checked exception)則不會導致事務回滾,不過可以宣告在丟擲哪些異常時回滾事務,包括checked異常,也可以宣告哪些異常丟擲時不回滾事務,即使異常是執行時異常,還可以程式設計性的通過setRollbackOnly()方法來指示一個事務必須回滾,在呼叫完setRollbackOnly()後你所能執行的唯一操作就是回滾;

      • 事務回滾異常只能為RuntimeException異常,而Checked Exception異常不回滾,捕獲異常不丟擲也不會回滾,但可以強制事務回滾:TransactionAspectSupport.currentTransactionStatus().isRollbackOnly();
      • 解決“自我呼叫”而導致的不能設定正確的事務屬性問題,可參考http://www.iteye.com/topic/1122740
  6. 程式設計式事務的實現

    1. PlatformTransactionManager程式碼實現步驟:獲取事務管理器;建立事務屬性物件;獲取事務狀態物件;建立JDBC模板物件;業務資料操作處理;
      public class test {
          @Resource
          private PlatformTransactionManager txManager;
          @Resource
          private  DataSource dataSource;
          private static JdbcTemplate jdbcTemplate;
          @Test
          public void testdelivery(){
              //定義事務隔離級別,傳播行為,
              DefaultTransactionDefinition def = new DefaultTransactionDefinition();  
              def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);  
              def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);  
              //事務狀態類,通過PlatformTransactionManager的getTransaction方法根據事務定義獲取;獲取事務狀態後,Spring根據傳播行為來決定如何開啟事務
              TransactionStatus status = txManager.getTransaction(def);  
              jdbcTemplate = new JdbcTemplate(dataSource);
              try {  
                  jdbcTemplate.update("insert into testtranstation(sd) values(?)", "1");  
                   //提交status中繫結的事務
                  txManager.commit(status); 
              } catch (RuntimeException e) {  
                  //回滾
                  txManager.rollback(status);  
              } 
          }
          
      }

      如上所示,我們在類中增加了兩個屬性:一個是 TransactionDefinition 型別的屬性,它用於定義一個事務;另一個是 PlatformTransactionManager 型別的屬性,用於執行事務管理操作。

      如果方法需要實施事務管理,我們首先需要在方法開始執行前啟動一個事務,呼叫PlatformTransactionManager.getTransaction(…) 方法便可啟動一個事務。建立並啟動了事務之後,便可以開始編寫業務邏輯程式碼,然後在適當的地方執行事務的提交或者回滾。

    2. 使用TransactionTemplate,該類繼承了DefaultTransactionDefinition,用於簡化事務管理,事務管理由模板定義,主要是通過TransactionCallback回撥介面或TransactionCallbackWithoutResult回撥介面指定,通過呼叫模板類的引數型別為TransactionCallback或TransactionCallbackWithoutResult的execute方法來自動享受事務管理。
      • TransactionCallback:通過實現該介面的“T doInTransaction(TransactionStatus status)”方法來定義需要事務管理的操作程式碼;
      • TransactionCallbackWithoutResult:繼承TransactionCallback介面,提供“void doInTransactionWithoutResult(TransactionStatus status)”便利介面用於方便哪些不需要返回值的事務操作程式碼;
      1. TransactionCallback的配置、程式碼實現步驟:獲取模板物件;選擇事務結果型別;業務資料操作處理;
        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:tx="http://www.springframework.org/schema/tx"
            xsi:schemaLocation="
                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
         
            <description>資料來源及事務配置</description>
         
            <!-- 資料來源配置 -->
            <!-- 代理datasource,使其能夠顯式獲取preparedStatement的引數值 -->
            <bean id="proxyDataSource" class="org.jdbcdslog.ConnectionPoolDataSourceProxy">
                <property name="targetDSDirect" ref="dataSource"/>
            </bean>
         
            <!-- 配置事務管理器 -->
            <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
                <property name="dataSource" ref="proxyDataSource" />
            </bean>
         
            <!--事務模板 -->  
            <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">  
                <property name="transactionManager" ref="transactionManager"/>  
                <!--ISOLATION_DEFAULT 表示由使用的資料庫決定  -->  
                <property name="isolationLevelName" value="ISOLATION_DEFAULT"/>  
                <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED" />  
                <!-- <property name="timeout" value="30"/> -->  
            </bean> 
         
            <!-- 註解方式配置事物 -->
            <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
        </beans>
        package com.hrh.initialize;
         
        import org.springframework.beans.factory.InitializingBean;
        import org.springframework.jdbc.core.JdbcTemplate;
        import org.springframework.transaction.TransactionStatus;
        import org.springframework.transaction.support.TransactionCallback;
        import org.springframework.transaction.support.TransactionTemplate;
         
        public class DataInitializer implements InitializingBean{
         
            private TransactionTemplate transactionTemplate;
         
            private JdbcTemplate jdbcTemplate;
            @Override
            public void afterPropertiesSet() throws Exception {
         
                transactionTemplate.execute(new TransactionCallback<Object>() {
                    @Override
                    public Object doInTransaction(TransactionStatus status) {
                        //建立儲存點
                        Object savepoint = status.createSavepoint();
                        // DML執行
                        try {
                            jdbcTemplate.execute("truncate table SET_RESOURCE");
                            jdbcTemplate.execute(String.format("INSERT INTO SET_RESOURCE VALUES ('%s','%s','%s','%s',%s,%s)", 
                                    "100", "cAuthc", "/sample/component.html", "1", "null", "null"));
                            jdbcTemplate.execute(String.format("INSERT INTO SET_RESOURCE VALUES ('%s','%s','%s','%s',%s,%s)", 
                                    "992", "cAuthc", "/mt/rgroup/rgroup_read.html", "1", "null", "null"));
                        } catch (Throwable e) {
                            LOG.error("Error occured, cause by: {}", e.getMessage());
                            //通過TransactionStatus的setRollbackOnly()或rollbackToSavepoint(savepoint) 控制事務
                            status.setRollbackOnly();
                            // status.rollbackToSavepoint(savepoint);
                        }
                        return null;
                    }
                });
            }
         
            public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
                this.transactionTemplate = transactionTemplate;
            }
         
            public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
                this.jdbcTemplate = jdbcTemplate;
            }  
         
        }
      2. TransactionCallbackWithoutResult程式碼實現:
            @Override
            public void afterPropertiesSet() throws Exception {
                transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                    @Override
                    protected void doInTransactionWithoutResult(TransactionStatus status) {
                        //欄位sd為int型,所以插入肯定失敗報異常,自動回滾,代表TransactionTemplate自動管理事務
                        jdbcTemplate.update("insert into testtranstation(sd) values(?)", "餓死");   
                    }}
                );
            }
      • 總結

        TransactionTemplate 的 execute() 方法有一個 TransactionCallback 型別的引數,該介面中定義了一個 doInTransaction() 方法,通常我們以匿名內部類的方式實現 TransactionCallback 介面,並在其 doInTransaction() 方法中書寫業務邏輯程式碼。這裡可以使用預設的事務提交和回滾規則,這樣在業務程式碼中就不需要顯式呼叫任何事務管理的 API。doInTransaction() 方法有一個TransactionStatus 型別的引數,我們可以在方法的任何位置呼叫該引數的 setRollbackOnly() 方法將事務標識為回滾的,以執行事務回滾。

        根據預設規則,如果在執行回撥方法的過程中丟擲了未檢查異常,或者顯式呼叫了TransacationStatus.setRollbackOnly() 方法,則回滾事務;如果事務執行完成或者丟擲了 checked 型別的異常,則提交事務。

        TransactionCallback 介面有一個子介面 TransactionCallbackWithoutResult,該介面中定義了一個 doInTransactionWithoutResult() 方法,TransactionCallbackWithoutResult 介面主要用於事務過程中不需要返回值的情況。當然,對於不需要返回值的情況,我們仍然可以使用 TransactionCallback 介面,並在方法中返回任意值即可。

  7. 宣告式事務的實現

    1. 基於TransactionInterceptor 類來實施宣告式事務管理功能:Spring最初提供的實現方式
      • 配置檔案:
        <beans...>
        ......
            <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
                <property name="transactionManager" ref="transactionManager"/>
                <property name="transactionAttributes">
                    <props>
                        <prop key="add*">PROPAGATION_REQUIRED</prop>
                    </props>
                </property>
            </bean>
            <bean id="buyStockServiceTarget" class="footmark.spring.core.tx.declare.origin.BuyStockServiceImpl">
                <property name="stockDao" ref="stockDao"/>
            </bean> 

        首先,我們配置了一個 TransactionInterceptor 來定義相關的事務規則,它有兩個主要的屬性:一個是 transactionManager,用來指定一個事務管理器,並將具體事務相關的操作委託給它;另一個是 Properties 型別的 transactionAttributes 屬性,它主要用來定義事務規則,該屬性的每一個鍵值對中,鍵指定的是方法名,方法名可以使用萬用字元,而值就表示相應方法的所應用的事務屬性。

        指定事務屬性的取值有較複雜的規則,這在 Spring 中算得上是一件讓人頭疼的事。具體的書寫規則如下:

         傳播行為 [,隔離級別] [,只讀屬性] [,超時屬性] [不影響提交的異常] [,導致回滾的異常]

        • 超時屬性的取值必須以”TIMEOUT_”開頭,後面跟一個int型別的值,表示超時時間,單位是秒。
        • 不影響提交的異常是指,即使事務中丟擲了這些型別的異常,事務任然正常提交。必須在每一個異常的名字前面加上”+”。異常的名字可以是類名的一部分。比如”+RuntimeException”、”+tion”等等。
        • 導致回滾的異常是指,當事務中丟擲這些型別的異常時,事務將回滾。必須在每一個異常的名字前面加上”-”。異常的名字可以是類名的全部或者部分,比如”-RuntimeException”、”-tion”等等。

         例項:

        <property name="*Service">
        PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED,TIMEOUT_20,
        +AbcException,+DefException,-HijException
        </property>
        以上表示式表示,針對所有方法名以 Service 結尾的方法,使用 PROPAGATION_REQUIRED 事務傳播行為,事務的隔離級別是 ISOLATION_READ_COMMITTED,超時時間為20秒,當事務丟擲 AbcException 或者 DefException 型別的異常,則仍然提交,當丟擲 HijException 型別的異常時必須回滾事務。這裡沒有指定”readOnly”,表示事務不是隻讀的。

        1)配置好了 TransactionInterceptor,我們還需要配置一個 ProxyFactoryBean 來組裝 target 和advice。這也是典型的 Spring AOP 的做法。通過 ProxyFactoryBean 生成的代理類就是織入了事務管理邏輯後的目標類。

        <bean id="buyStockService" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="target" ref="buyStockServiceTarget"/>
            <property name="interceptorNames">
                <list>
                    <idref bean="transactionInterceptor"/>
                </list>
            </property>
        </bean>

         2)除了使用上面的ProxyFactoryBean來組裝代理類表示哪些類需要使用到事務攔截器外,還可以使用BeanNameAutoProxyCreator告訴Spring哪些類要使用事務攔截器進行攔截:

        <!--指明事務攔截器攔截哪些類-->
        <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
            <property name="beanNames">
                <list>
                    <value>*ServiceImpl</value>
                </list>
            </property>
            <property name="interceptorNames">
                <list>
                    <value>transactionInterceptor</value>
                </list>
            </property>
        </bean>

        BeanName屬性告訴Spring如何攔截類。由於宣告為*ServiceImpl,所有關於Service是現實類都會被其攔截,然後interceptorNames則是定義事務攔截器,這樣對應的類和方法就會被事務管理器所攔截了。

        至此,宣告式事務管理就算是實現了。我們沒有對業務程式碼進行任何操作,所有設定均在配置檔案中完成,這就是宣告式事務的最大優點。

    2. 基於 TransactionProxyFactoryBean的宣告式事務管理:

      前面的宣告式事務雖然好,但是卻存在一個非常惱人的問題:配置檔案太多。我們必須針對每一個目標物件配置一個 ProxyFactoryBean;另外,雖然可以通過父子 Bean 的方式來複用 TransactionInterceptor 的配置,但是實際的複用機率也不高;這樣,加上目標物件本身,每一個業務類可能需要對應三個 <bean/> 配置,隨著業務類的增多,配置檔案將會變得越來越龐大,管理配置檔案又成了問題。

      為了緩解這個問題,Spring 為我們提供了 TransactionProxyFactoryBean,用於將TransactionInterceptor 和 ProxyFactoryBean 的配置合二為一。

      • 下面是每個Bean都有一個代理的實現:

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:context="http://www.springframework.org/schema/context"
            xmlns:mvc="http://www.springframework.org/schema/mvc"
            xmlns:aop="http://www.springframework.org/schema/aop"
            xmlns:tx="http://www.springframework.org/schema/tx"
            xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
                http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
                http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
                http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-aop-4.2.xsd
                ">
            
            <context:property-placeholder location="classpath:jdbc.properties"/>
            
            <!-- 註冊資料來源 C3P0 -->
            <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"  >
                 <property name="driverClass" value="${jdbc.driverClass}"></property>
                 <property name="jdbcUrl"  value="${jdbc.url}"></property>
                 <property name="user"  value="${jdbc.username}"></property>
                 <property name="password" value="${jdbc.password}"></property>
            </bean>
            
            <bean id="accountDao" class="com.hrh.dao.impl.AccountDaoImpl">
                <property name="dataSource" ref="dataSource"/>
            </bean>
            
            <bean id="stockDao" class="com.hrh.dao.impl.StockDaoImpl">
                <property name="dataSource" ref="dataSource"/>
            </bean>
            
            <bean id="buyStockService" class="com.hrh.service.impl.BuyStockServiceImpl">
                <property name="accountDao" ref="accountDao"></property>
                <property name="stockDao" ref="stockDao"></property>
            </bean>
            
            
            <!-- 事務管理器 -->
            <bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
                <property name="dataSource" ref="dataSource"></property>
            </bean>
            
            <!-- 事務代理工廠 -->
            <!-- 生成事務代理物件 -->
            <bean id="serviceProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
                <property name="transactionManager" ref="myTracnsactionManager"></property>
                <property name="target" ref="buyStockService"></property>
                <property name="transactionAttributes">
                    <props>
                        <!-- 主要 key 是方法   
                            ISOLATION_DEFAULT  事務的隔離級別
                            PROPAGATION_REQUIRED  傳播行為
                        -->
                        <prop key="add*">ISOLATION_DEFAULT,PROPAGATION_REQUIRED</prop>
                        <!-- -Exception 表示發生指定異常回滾,+Exception 表示發生指定異常提交 -->
                        <prop key="buyStock">ISOLATION_DEFAULT,PROPAGATION_REQUIRED,-BuyStockException</prop>
                    </props>
                </property>
                
            </bean>
            
            
        </beans>

        如此一來,配置檔案與先前相比簡化了很多。我們把這種配置方式稱為 Spring 經典的宣告式事務管理。

        但是,顯式為每一個業務類配置一個 TransactionProxyFactoryBean 的做法將使得程式碼顯得過於刻板,為此我們可以使用自動建立代理的方式來將其簡化(使用自動建立代理是純 AOP 知識)。

      • 所有Bean共享一個代理基類的實現:
        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:context="http://www.springframework.org/schema/context"
            xmlns:mvc="http://www.springframework.org/schema/mvc"
            xmlns:aop="http://www.springframework.org/schema/aop"
            xmlns:tx="http://www.springframework.org/schema/tx"
            xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
                http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
                http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
                http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-aop-4.2.xsd
                ">
            
            <context:property-placeholder location="classpath:jdbc.properties"/>
            
            <!-- 註冊資料來源 C3P0 -->
            <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"  >
                 <property name="driverClass" value="${jdbc.driverClass}"></property>
                 <property name="jdbcUrl"  value="${jdbc.url}"></property>
                 <property name="user"  value="${jdbc.username}"></property>
                 <property name="password" value="${jdbc.password}"></property>
            </bean>
            
            <bean id="accountDao" class="com.hrh.dao.impl.AccountDaoImpl">
                <property name="dataSource" ref="dataSource"/>
            </bean>
            
            <bean id="stockDao" class="com.hrh.dao.impl.StockDaoImpl">
                <property name="dataSource" ref="dataSource"/>
            </bean>
            
            <bean id="buyStockService" class="com.hrh.service.impl.BuyStockServiceImpl">
                <property name="accountDao" ref="accountDao"></property>
                <property name="stockDao" ref="stockDao"></property>
            </bean>
            
            
            <!-- 事務管理器 -->
            <bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
                <property name="dataSource" ref="dataSource"></property>
            </bean>
            
            <bean id="transactionBase"  
                    class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"  
                    lazy-init="true" abstract="true">  
                <!-- 配置事務管理器 -->  
                <property name="transactionManager" ref="myTracnsactionManager" />  
                <!-- 配置事務屬性 -->  
                <property name="transactionAttributes">  
                    <props>  
                        <prop key="*">PROPAGATION_REQUIRED</prop>  
                    </props>  
                </property>  
            </bean>    
           <!-- 共享基類 -->
            <bean id="serviceProxy" parent="transactionBase" >  
                <property name="target" ref="buyStockService" />   
            </bean>
        </beans>
    3. 以MyBatis為例,基於.xml檔案的宣告式事務配置(基於Aspectj AOP配置事務),通過使用Spring的<tx:advice>定義事務通知與AOP相關配置實現

      前面兩種宣告式事務配置方式奠定了 Spring 宣告式事務管理的基石。在此基礎上,Spring 2.x 引入了 <tx> 名稱空間,結合使用 <aop> 名稱空間,帶給開發人員配置宣告式事務的全新體驗,配置變得更加簡單和靈活。另外,得益於 <aop> 名稱空間的切點表示式支援,宣告式事務也變得更加強大。

      <!-- 
      <tx:advice>定義事務通知,用於指定事務屬性,其中“transaction-manager”屬性指定事務管理器,並通過<tx:attributes>指定具體需要攔截的方法
          <tx:method>攔截方法,其中引數有:
          name:方法名稱,將匹配的方法注入事務管理,可用萬用字元
          propagation:事務傳播行為,
          isolation:事務隔離級別定義;預設為“DEFAULT”
          timeout:事務超時時間設定,單位為秒,預設-1,表示事務超時將依賴於底層事務系統;
          read-only:事務只讀設定,預設為false,表示不是隻讀;
          rollback-for:需要觸發回滾的異常定義,可定義多個,以“,”分割,預設任何RuntimeException都將導致事務回滾,而任何Checked Exception將不導致事務回滾;
          no-rollback-for:不被觸發進行回滾的 Exception(s);可定義多個,以“,”分割;
       -->
      <tx:advice id="advice" transaction-manager="transactionManager">
          <tx:attributes>
              <!-- 攔截save開頭的方法,事務傳播行為為:REQUIRED:必須要有事務, 如果沒有就在上下文建立一個 -->
              <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED" timeout="" read-only="false" no-rollback-for="" rollback-for="java.lang.Exception"/>
              <!-- 支援,如果有就有,沒有就沒有 -->
              <tx:method name="*" propagation="SUPPORTS"/>
          </tx:attributes>
      </tx:advice>
      <!-- 定義切入點,expression為切人點表示式,如下是指定impl包下的所有方法,具體以自身實際要求自定義  -->
      <aop:config>
          <aop:pointcut expression="execution(* com.hrh.*.service.impl.*.*(..))" id="pointcut"/>
          <!--<aop:advisor>定義切入點,與通知,把tx與aop的配置關聯,才是完整的宣告事務配置 -->
          <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
      </aop:config>

      由於使用了切點表示式,我們就不需要針對每一個業務類建立一個代理物件了。另外,如果配置的事務管理器 Bean 的名字取值為 “transactionManager”,則我們可以省略 <tx:advice> 的 transaction-manager 屬性,因為該屬性的預設值即為 “transactionManager”。

    4. 以MyBatis為例,基於註解的宣告式事務配置,通過@Transactional實現事務管理

      除了基於名稱空間的事務配置方式,Spring 2.x 還引入了基於 Annotation 的方式,具體主要涉及@Transactional 標註。

      1. 新增tx名字空間
        xmlns:tx="http://www.springframework.org/schema/tx"
      2. 開啟事務的註解支援:Spring 使用 BeanPostProcessor 來處理 Bean 中的標註,因此我們需要在配置檔案中作如下宣告來啟用該後處理 Bean,
        <!-- 開啟事務控制的註解支援 -->  
        <tx:annotation-driven transaction-manager="transactionManager"/>
      3. MyBatis自動參與到Spring事務管理中,無需額外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的資料來源與DataSourceTransactionManager引用的資料來源一致即可
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> 
            <property name="dataSource" ref="dataSource" />  
            <property name="configLocation">  
                <value>classpath:mybatis-config.xml</value>  
            </property>  
        </bean> 
        
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
            <property name="dataSource" ref="dataSource" />  
        </bean>  
      4. 使用@Transactional註解                              

        @Transactional 可以作用於介面、介面方法、類以及類方法上。當作用於類上時,該類的所有 public 方法都將具有該型別的事務屬性,同時,我們也可以在方法級別使用該註解來覆蓋類級別的定義。

        雖然 @Transactional 註解可以作用於介面、介面方法、類以及類方法上,但是 Spring 建議不要在介面或介面方法上使用該註解,因為這隻有在使用基於介面的代理時它才會生效。另外,@Transactional 註解應該只被應用到 public 方法上,這是由 Spring AOP 的本質決定的。如果在 protected、private 或者預設可見性的方法上使用@Transactional註解,這將被忽略,也不會丟擲任何異常。

        • 注意:
          • 如果在介面、實現類或方法上都指定了@Transactional 註解,則優先順序順序為方法>實現類>介面;
          • 建議只在實現類或實現類的方法上使用@Transactional,而不要在介面上使用,這是因為如果使用JDK代理機制(基於介面的代理)是沒問題;而使用使用CGLIB代理(繼承)機制時就會遇到問題,因為其使用基於類的代理而不是介面,這是因為介面上的@Transactional註解是“不能繼承的”;
          • 預設情況下,只有來自外部的方法呼叫才會被AOP代理捕獲,也就是,類內部方法呼叫本類內部的其他方法並不會引起事務行為,即使被呼叫方法使用@Transactional註解進行修飾。
        • @Transactional(timeout = 60)

            如果用這個註解描述一個方法的話,執行緒已經跑到方法裡面,如果已經過去60秒了還沒跑完這個方法並且執行緒在這個方法中的後面還有涉及到對資料庫的增刪改查操作時會報事務超時錯誤(會回滾)。
            如果已經過去60秒了還沒跑完但是後面已經沒有涉及到對資料庫的增刪改查操作,那麼這時不會報事務超時錯誤(不會回滾)。
           
        • Spring管理事務預設回滾的異常是什麼? 
            答案是: RuntimeException或者Error。 
            注意:如果事務在try{}catch(Exception e){e.printStackTrace();}中跑,並且catch中只是列印e的話,那麼事務不會rollback。因為異常被catch掉了,框架不知道發生了異常。
            如果想要rollback,可以加上rollbackFor=Exception.class,然後:
              ①在方法上新增 throws  Exception,將方法中出現的異常丟擲給spring事務,
              ②去掉方法體中的try catch
              ③catch (Exception e) {  throw e;}繼續向上拋,目的是讓spring事務捕獲這個異常。
                rollbackFor=Exception.class,catch(){
                     throw new RunTimeException();
                }
            如果不加rollbackFor=Exception.class,丟擲new Exception() 是不會回滾的。Spring原始碼如下:
              public boolean rollbackOn(Throwable ex) { 
                   return (ex instanceof RuntimeException || ex instanceof Error);
              } 
            如果是RuntimeException或Error的話,就返回True,表示要回滾,否則返回False,表示不回滾。
            只有spring事務捕獲到Exception異常後,@Transactional(rollbackFor=Exception.class),才會起到應有的作用;catch (Exception e) {            e.printStackTrace();        }這句是捕獲try中出現的Exception然後將異常資訊列印出來,僅僅是列印出來,然後什麼也沒幹。

            @Transactional(timeout = 60,rollbackFor=Exception.class)與rollbackFor=Exception.class的作用是
              1. 讓checked例外也回滾:在整個方法前加上 @Transactional(rollbackFor=Exception.class)
              2. 讓unchecked例外不回滾: @Transactional(notRollbackFor=RunTimeException.class)
            checked Unchecked exception是執行時錯誤。
    5. 基於 <tx> 名稱空間和基於 @Transactional 的事務宣告方式各有優缺點。基於 <tx> 的方式,其優點是與切點表示式結合,功能強大。利用切點表示式,一個配置可以匹配多個方法,而基於 @Transactional 的方式必須在每一個需要使用事務的方法或者類上用 @Transactional 標註,儘管可能大多數事務的規則是一致的,但是對 @Transactional 而言,也無法重用,必須逐個指定。另一方面,基於 @Transactional 的方式使用起來非常簡單明瞭,沒有學習成本。開發人員可以根據需要,任選其中一種使用,甚至也可以根據需要混合使用這兩種方式。

      如果不是對遺留程式碼進行維護,則不建議再使用基於 TransactionInterceptor 以及基於TransactionProxyFactoryBean 的宣告式事務管理方式。

  8. 總結

    • 基於 TransactionDefinition、PlatformTransactionManager、TransactionStatus 程式設計式事務管理是 Spring 提供的最原始的方式,通常我們不會這麼寫,但是瞭解這種方式對理解 Spring 事務管理的本質有很大作用。
    • 基於 TransactionTemplate 的程式設計式事務管理是對上一種方式的封裝,使得編碼更簡單、清晰。
    • 基於 TransactionInterceptor 的宣告式事務是 Spring 宣告式事務的基礎,通常也不建議使用這種方式,但是與前面一樣,瞭解這種方式對理解 Spring 宣告式事務有很大作用。
    • 基於 TransactionProxyFactoryBean 的宣告式事務是上中方式的改進版本,簡化的配置檔案的書寫,這是 Spring 早期推薦的宣告式事務管理方式,但是在 Spring 2.0 中已經不推薦了。
    • 基於 <tx> 和 <aop> 名稱空間的宣告式事務管理是目前推薦的方式,其最大特點是與 Spring AOP 結合緊密,可以充分利用切點表示式的強大支援,使得管理事務更加靈活。
    • 基於 @Transactional 的方式將宣告式事務管理簡化到了極致。開發人員只需在配置檔案中加上一行啟用相關後處理 Bean 的配置,然後在需要實施事務管理的方法或者類上使用 @Transactional 指定事務規則即可實現事務管理,而且功能也不必其他方式遜色。

相關文章