Spring筆記(4) - Spring的編程式事務和聲明式事務詳解

一.背景

  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事務管理的兩種方式

    • 編程式事務html

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

        • 編程式事務管理對基於 POJO 的應用來講是惟一選擇。咱們須要在代碼中調用beginTransaction()、commit()、rollback()等事務管理相關的方法;程序員

    • 聲明式事務:該事務是創建在AOP之上的,其本質是對方法先後進行攔截,而後在目標方法開始以前建立或加入一個事務,在執行完目標方法以後根據執行狀況提交或回滾事務。spring

      Spring配置文件中關於事務配置老是由三個組成部分,分別是DataSource、TransactionManager和代理機制這三部分,不管哪一種配置方式,通常變化的只是代理機制這部分。sql

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

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

      • 優勢:編程

        • 編程式事務每次實現都要單獨實現,但業務量大且功能複雜時,使用編程性事務無疑是痛苦的;而聲明式事務不一樣,聲明式事務屬於非侵入性,不會影響業務邏輯的實現,只需在配置文件中作相關的事務規則聲明(或經過基於@Transactional註解的方式),即可以將事務規則應用到業務邏輯中;後端

        • 非侵入式的開發方式,聲明式事務管理使業務代碼不受污染,一個普通的POJO對象,只要加上註解就能夠得到徹底的事務支持;spring-mvc

      • 缺點:最細粒度只能是做用到方法級別,沒法作到像編程事務那樣能夠做用到代碼塊級別;

      • 實現方式:

        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 指定事務規則便可實現事務管理,並且功能也沒必要其餘方式遜色。
相關文章
相關標籤/搜索