1. 程式人生 > 實用技巧 >第八章、宣告式事務管理

第八章、宣告式事務管理

一、事務概述

  1. 事務就是一組由於邏輯上緊密關聯而合併成一個整體(工作單元)的多個數據庫操作,這些操作要麼都執行,要麼都不執行,為了保證資料的完整性和一致性。

  2. 事務的四個關鍵屬性(ACID)

    1. 原子性(atomicity):“原子”的本意是“不可再分”,事務的原子性表現為一個事務中涉及到的多個操作在邏輯上缺一不可。事務的原子性要求事務中的所有操作要麼都執行,要麼都不執行。

    2. 一致性(consistency):“一致”指的是資料的一致,具體是指:所有資料都處於滿足業務規則的一致性狀態。一致性原則要求:一個事務中不管涉及到多少個操作,都必須保證事務執行之前資料是正確的,事務執行之後資料仍然是正確的。如果一個事務在執行的過程中,其中某一個或某幾個操作失敗了,則必須將其他所有操作撤銷,將資料恢復到事務執行之前的狀態,這就是回滾。

    3. 隔離性(isolation):在應用程式實際執行過程中,事務之間是併發執行的,所以很有可能有許多事務同時處理相同的資料,因此每個事務都應該與其他事務隔離開來,防止資料損壞。隔離性原則要求多個事務在併發執行過程中不會互相干擾。

    4. 永續性(durability):永續性原則要求事務執行完成後,對資料的修改永久的儲存下來,不會因各種系統錯誤或其他意外情況而受到影響。通常情況下,事務對資料的修改應該被寫入到持久化儲存器中。

二、Spring事務管理

2.1、程式設計式事務管理

  1. 使用原生的JDBC API進行事務管理

    1. 獲取資料庫連線Connection物件

    2. 取消事務的自動提交

    3. 執行操作

    4. 正常完成操作時手動提交事務

    5. 執行失敗時回滾事務

    6. 關閉相關資源

使用原生的JDBC API實現事務管理是所有事務管理方式的基石,同時也是最典型的程式設計式事務管理。程式設計式事務管理需要將事務管理程式碼嵌入到業務方法中來控制事務的提交和回滾。在使用程式設計的方式管理事務時,必須在每個事務操作中包含額外的事務管理程式碼。相對於核心業務而言,事務管理的程式碼顯然屬於非核心業務,如果多個模組都使用同樣模式的程式碼進行事務管理,顯然會造成較大程度的程式碼冗餘。

2.2、宣告式事務管理

通過配置的形式,基於AOP的方式,動態的把事務管理的程式碼作用的目標方法上面。

大多數情況下宣告式事務比程式設計式事務管理更好:它將事務管理程式碼從業務方法中分離出來,以宣告的方式來實現事務管理

事務管理程式碼的固定模式作為一種橫切關注點,可以通過AOP方法模組化,進而藉助Spring AOP框架實現宣告式事務管理。

Spring在不同的事務管理API之上定義了一個抽象層,通過配置的方式使其生效,從而讓應用程式開發人員不必瞭解事務管理API的底層實現細節,就可以使用Spring的事務管理機制。 Spring既支援程式設計式事務管理,也支援宣告式的事務管理。

2.3、Spring提供的事務管理器

Spring從不同的事務管理API中抽象出了一整套事務管理機制,讓事務管理程式碼從特定的事務技術中獨立出來。開發人員通過配置的方式進行事務管理,而不必瞭解其底層是如何實現的。 Spring的核心事務管理抽象是PlatformTransactionManager。它為事務管理封裝了一組獨立於技術的方法。無論使用Spring的哪種事務管理策略(程式設計式或宣告式),事務管理器都是必須的。 事務管理器可以以普通的bean的形式宣告在Spring IOC容器中。

2.4、事務管理器的主要實現

  1. DataSourceTransactionManager:在應用程式中只需要處理一個數據源,而且通過JDBC存取。

  2. JtaTransactionManager:在JavaEE應用伺服器上用JTA(Java Transaction API)進行事務管理。

  3. HibernateTransactionManager:用Hibernate框架存取資料庫。

三、測試資料準備

3.1、需求

public interface BookShopDao {

    Book findBookPriceByIsbn(String isbn);

    int updateBookStock(String isbn,String stock);

    int updateAccount(String username,String balance);
}
@Repository
public class BookShopDaoImpl implements BookShopDao {

    @Autowired
    private JdbcTemplate template;

    @Override
    public Book findBookPriceByIsbn(String isbn) {
        String sql = "SELECT * FROM book WHERE isbn=?";
        RowMapper<Book> rowMapper = new BeanPropertyRowMapper<>(Book.class);
        return template.queryForObject(sql, rowMapper, isbn);
    }

    @Override
    public int updateBookStock(String isbn, String stock) {
        String sql = "UPDATE book_stock SET stock = ? WHERE isbn = ?";
        return template.update(sql, stock, isbn);

    }

    @Override
    public int updateAccount(String username, String balance) {
        String sql = "UPDATE account  SET balance  = ? WHERE username = ?";
        return template.update(sql, balance, username);

    }
}
public interface BookShopService {

   Book purchase(String isbn, String username, String stock, String balance);

}

在需要進行事務控制的方法上加註解 @Transactional

@Service
public class BookShopServiceImpl implements BookShopService{


    @Autowired
    private BookShopDao bookShopDao;

    @Transactional
    @Override
    public Book purchase(String isbn, String username ,String stock,String balance) {
        Book book  = bookShopDao.findBookPriceByIsbn(isbn);
        bookShopDao.updateBookStock(isbn,stock);
         //bookShopDao.updateAccount(username,balance);
        return book;
    }

}

<context:component-scan base-package="com.jdy.spring2020.scan" />

    <context:property-placeholder location="classpath:jdbc.properties"/>

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="${user}"/>
        <property name="password" value="${password}"/>
        <property name="jdbcUrl" value="${jdbcUrl}"/>
        <property name="driverClass" value="${driverClass}"/>
        <property name="initialPoolSize" value="${initialPoolSize}"/>
        <property name="minPoolSize" value="${minPoolSize}"/>
        <property name="maxPoolSize" value="${maxPoolSize}"/>
        <property name="acquireIncrement" value="${acquireIncrement}"/>
        <property name="maxStatements" value="${maxStatements}"/>
        <property name="maxStatementsPerConnection" value="${maxStatementsPerConnection}"/>
    </bean>


    <bean id="template"  class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>


    <!-- 配置事務管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 啟用事務註解 -->
    <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
     ApplicationContext context = new ClassPathXmlApplicationContext("application_tx.xml");
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        for (String name : beanDefinitionNames) {
            System.out.println("name = " + name);
        }

        BookShopServiceImpl shopService= context.getBean("bookShopServiceImpl",BookShopServiceImpl.class);

        Book purchase = shopService.purchase("ISBN-004", "", "1", "");
        System.out.println("purchase = " + purchase);

四、事務的傳播行為

  • 當事務方法被另一個事務方法呼叫時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中執行,也可能開啟一個新事務,並在自己的事務中執行。
  • 事務的傳播行為可以由傳播屬性指定。Spring定義了7種類傳播行為。
  • 事務傳播屬性可以在@Transactional註解的propagation屬性中定義。
傳播屬性 描述
Required 如果有事務在執行,當前的方法就在這個事務內執行,否則,就啟動一個新的事務,並在自己的事務內執行。
Required_new 當前的方法必須啟動新事物,並在自己的事務內部執行,如果有事務在執行,應該將他掛起
Support 如果有事務,就在事務內執行,如果沒有,可以不再事務內執行
Not_ support 當前方法不應該執行在事務中,如果有事務,將事務掛起
Mandatory 當前方法必須在事務內部執行,如果沒有實物,就丟擲異常
Never 當前事務不應該在事務中執行,如果有就丟擲異常
Nested 如果有事務在執行,當前的方法就應該在這個事務的巢狀事務內執行,否則,就啟動一個新的事務,並在它自己的事務內執行。

  • REQUIRED傳播行為

當bookService的purchase()方法被另一個事務方法checkout()呼叫時,它預設會在現有的事務內執行。這個預設的傳播行為就是REQUIRED。因此在checkout()方法的開始和終止邊界內只有一個事務。這個事務只在checkout()方法結束的時候被提交,結果使用者一本書都買不了。

  • REQUIRES_NEW傳播行為

表示該方法必須啟動一個新事務,並在自己的事務內執行。如果有事務在執行,就應該先掛起它。

在Spring 2.x事務通知中,可以像下面這樣在tx:method元素中設定傳播事務屬性。

五、事務的隔離級別

5.1、資料庫事務併發問題

假設現在有兩個事務:Transaction01和Transaction02併發執行。

  1. 髒讀

    1. Transaction01將某條記錄的AGE值從20修改為30。

    2. Transaction02讀取了Transaction01更新後的值:30。

    3. Transaction01回滾,AGE值恢復到了20。

    4. Transaction02讀取到的30就是一個無效的值。

  2. 不可重複讀

    1. Transaction01讀取了AGE值為20。

    2. Transaction02將AGE值修改為30。

    3. Transaction01再次讀取AGE值為30,和第一次讀取不一致。

  3. 幻讀

    1. Transaction01讀取了STUDENT表中的一部分資料。

    2. Transaction02向STUDENT表中插入了新的行。

    3. Transaction01讀取了STUDENT表時,多出了一些行。

5.2、隔離級別

資料庫系統必須具有隔離併發執行各個事務的能力,使它們不會相互影響,避免各種併發問題。一個事務與其他事務隔離的程度稱為隔離級別。SQL標準中規定了多種事務隔離級別,不同隔離級別對應不同的干擾程度,隔離級別越高,資料一致性就越好,但併發性越弱。

  1. 讀未提交:READ UNCOMMITTED 允許Transaction01讀取Transaction02未提交的修改。(問題:髒讀

  2. 讀已提交:READ COMMITTED

    要求Transaction01只能讀取Transaction02已提交的修改。(問題:不可重複讀(修改資料問題))

  3. 可重複讀(預設):REPEATABLE READ

    確保Transaction01可以多次從一個欄位中讀取到相同的值,即Transaction01執行期間禁止其它事務對這個欄位進行更新。(問題:幻讀(增加資料問題))

  4. 序列(xing)化:SERIALIZABLE

    確保Transaction01可以多次從一個表中讀取到相同的行,在Transaction01執行期間,禁止其它事務對這個表進行新增、更新、刪除操作。可以避免任何併發問題,但效能十分低下

  5. 各個隔離級別解決併發問題的能力見下表

髒讀 不可重複讀 幻讀
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

5.3、在Spring中指定事務隔離級別

  • 註解

    用@Transactional註解宣告式地管理事務時可以在@Transactional的isolation屬性中設定隔離級別

  • XML

    在Spring 2.x事務通知中,可以在tx:method元素中指定隔離級別

六、觸發事務回滾的異常

預設情況,捕獲到RuntimeException或Error時回滾,而捕獲到編譯時異常不回滾。

6.1、設定途經

    1、註解@Transactional

    • rollbackFor屬性:指定遇到時必須進行回滾的異常型別,可以為多個.

    • noRollbackFor屬性:指定遇到時不回滾的異常型別,可以為多個

    2、XML

      在Spring 2.x事務通知中,可以在<tx:method>元素中指定回滾規則。如果有不止一種異常則用逗號分隔。

      

七、事務的超時和只讀屬性

由於事務可以在行和表上獲得鎖,因此長事務會佔用資源,並對整體效能產生影響。 如果一個事務只讀取資料但不做修改,資料庫引擎可以對這個事務進行優化。 超時事務屬性:事務在強制回滾之前可以保持多久。這樣可以防止長期執行的事務佔用資源。 只讀事務屬性: 表示這個事務只讀取資料但不更新資料, 這樣可以幫助資料庫引擎優化事務。

7.1、設定

 1、註解@Transactional

  2、XML

      在Spring 2.x事務通知中,超時和只讀屬性可以在tx:method元素中進行指定

八、 基於XML文件的宣告式事務配置

<!-- 配置事務切面 -->
    <aop:config>
        <aop:pointcut 
            expression="execution(* com.atguigu.tx.component.service.BookShopServiceImpl.purchase(..))" 
            id="txPointCut"/>
        <!-- 將切入點表示式和事務屬性配置關聯到一起 -->
        <aop:advisor advice-ref="myTx" pointcut-ref="txPointCut"/>
    </aop:config>
    
    <!-- 配置基於XML的宣告式事務  -->
    <tx:advice id="myTx" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- 設定具體方法的事務屬性 -->
            <tx:method name="find*" read-only="true"/>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="purchase" 
                isolation="READ_COMMITTED" 
    no-rollback-for="java.lang.ArithmeticException,java.lang.NullPointerException"
                propagation="REQUIRES_NEW"
                read-only="false"
                timeout="10"/>
        </tx:attributes>
    </tx:advice>