1. 程式人生 > >Spring事務管理詳解

Spring事務管理詳解

事務的基本原理

Spring事務的本質其實就是資料庫對事務的支援,使用JDBC的事務管理機制,就是利用java.sql.Connection物件完成對事務的提交,那在沒有Spring幫我們管理事務之前,我們要怎麼做。

Connection conn = DriverManager.getConnection();
try {  
    conn.setAutoCommit(false);  //將自動提交設定為false                         
    執行CRUD操作 
    conn.commit();      //當兩個操作成功後手動提交  
} catch
(Exception e) { conn.rollback(); //一旦其中一個操作出錯都將回滾,所有操作都不成功 e.printStackTrace(); } finally { conn.colse(); }

事務是一系列的動作,一旦其中有一個動作出現錯誤,必須全部回滾,系統將事務中對資料庫的所有已完成的操作全部撤消,滾回到事務開始的狀態,避免出現由於資料不一致而導致的接下來一系列的錯誤。事務的出現是為了確保資料的完整性和一致性,在目前企業級應用開發中,事務管理是必不可少的。

與事務相關的理論知識

眾所周知,事務有四大特性(ACID)

1.原子性(Atomicity)事務是一個原子操作,由一系列動作組成。事務的原子性確保動作要麼全部完成,要麼完全不起作用。

2.一致性(Consistency)事務在完成時,必須是所有的資料都保持一致狀態。

3.隔離性(Isolation)併發事務執行之間無影響,在一個事務內部的操作對其他事務是不產生影響,這需要事務隔離級別來指定隔離性。

4.永續性(Durability)一旦事務完成,資料庫的改變必須是持久化的。

在企業級應用中,多使用者訪問資料庫是常見的場景,這就是所謂的事務的併發。事務併發所可能存在的問題:
1.髒讀:一個事務讀到另一個事務未提交的更新資料。
2.不可重複讀:一個事務兩次讀同一行資料,可是這兩次讀到的資料不一樣。
3.幻讀:一個事務執行兩次查詢,但第二次查詢比第一次查詢多出了一些資料行。
4.丟失更新:撤消一個事務時,把其它事務已提交的更新的資料覆蓋了。

我們可以在java.sql.Connection中看到JDBC定義了五種事務隔離級別來解決這些併發導致的問題:

/**
 * A constant indicating that transactions are not supported. 
 */
int TRANSACTION_NONE         = 0;

/**
 * A constant indicating that
 * dirty reads, non-repeatable reads and phantom reads can occur.
 * This level allows a row changed by one transaction to be read
 * by another transaction before any changes in that row have been
 * committed (a "dirty read").  If any of the changes are rolled back, 
 * the second transaction will have retrieved an invalid row.
 */
int TRANSACTION_READ_UNCOMMITTED = 1;

/**
 * A constant indicating that
 * dirty reads are prevented; non-repeatable reads and phantom
 * reads can occur.  This level only prohibits a transaction
 * from reading a row with uncommitted changes in it.
 */
int TRANSACTION_READ_COMMITTED   = 2;

/**
 * A constant indicating that
 * dirty reads and non-repeatable reads are prevented; phantom
 * reads can occur.  This level prohibits a transaction from
 * reading a row with uncommitted changes in it, and it also
 * prohibits the situation where one transaction reads a row,
 * a second transaction alters the row, and the first transaction
 * rereads the row, getting different values the second time
 * (a "non-repeatable read").
 */
int TRANSACTION_REPEATABLE_READ  = 4;

/**
 * A constant indicating that
 * dirty reads, non-repeatable reads and phantom reads are prevented.
 * This level includes the prohibitions in
 * <code>TRANSACTION_REPEATABLE_READ</code> and further prohibits the 
 * situation where one transaction reads all rows that satisfy
 * a <code>WHERE</code> condition, a second transaction inserts a row that
 * satisfies that <code>WHERE</code> condition, and the first transaction
 * rereads for the same condition, retrieving the additional
 * "phantom" row in the second read.
 */
int TRANSACTION_SERIALIZABLE     = 8;

翻譯過來這幾個常量就是
TRANSACTION_NONE JDBC 驅動不支援事務
TRANSACTION_READ_UNCOMMITTED 允許髒讀、不可重複讀和幻讀。
TRANSACTION_READ_COMMITTED 禁止髒讀,但允許不可重複讀和幻讀。
TRANSACTION_REPEATABLE_READ 禁止髒讀和不可重複讀,單執行幻讀。
TRANSACTION_SERIALIZABLE 禁止髒讀、不可重複讀和幻讀。

隔離級別越高,意味著資料庫事務併發執行效能越差,能處理的操作就越少。你可以通過conn.setTransactionLevel去設定你需要的隔離級別。
JDBC規範雖然定義了事務的以上支援行為,但是各個JDBC驅動,資料庫廠商對事務的支援程度可能各不相同。
出於效能的考慮我們一般設定TRANSACTION_READ_COMMITTED就差不多了,剩下的通過使用資料庫的鎖來幫我們處理別的,關於資料庫的鎖這個之後再說。

瞭解了基本的JDBC事務,那有了Spring,在事務管理上會有什麼新的改變呢?
有了Spring,我們再也無需要去處理獲得連線、關閉連線、事務提交和回滾等這些操作,使得我們把更多的精力放在處理業務上。事實上Spring並不直接管理事務,而是提供了多種事務管理器。他們將事務管理的職責委託給Hibernate或者JTA等持久化機制所提供的相關平臺框架的事務來實現。

Spring事務管理

Spring事務管理的核心介面是PlatformTransactionManager
這裡寫圖片描述
事務管理器介面通過getTransaction(TransactionDefinition definition)方法根據指定的傳播行為返回當前活動的事務或建立一個新的事務,這個方法裡面的引數是TransactionDefinition類,這個類就定義了一些基本的事務屬性。
在TransactionDefinition介面中定義了它自己的傳播行為和隔離級別
這裡寫圖片描述
除去常量,主要的方法有:

int getIsolationLevel();// 返回事務的隔離級別
String getName();// 返回事務的名稱
int getPropagationBehavior();// 返回事務的傳播行為
int getTimeout();  // 返回事務必須在多少秒內完成
boolean isReadOnly(); // 事務是否只讀,事務管理器能夠根據這個返回值進行優化,確保事務是隻讀的

Spring事務的傳播屬性

由上圖可知,Spring定義了7個以PROPAGATION_開頭的常量表示它的傳播屬性。

名稱 解釋
PROPAGATION_REQUIRED 0 支援當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇,也是Spring預設的事務的傳播。
PROPAGATION_SUPPORTS 1 支援當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY 2 支援當前事務,如果當前沒有事務,就丟擲異常。
PROPAGATION_REQUIRES_NEW 3 新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED 4 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER 5 以非事務方式執行,如果當前存在事務,則丟擲異常。
PROPAGATION_NESTED 6 如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務,則進行與PROPAGATION_REQUIRED類似的操作。

Spring事務的隔離級別

名稱 解釋
ISOLATION_DEFAULT -1 這是一個PlatfromTransactionManager預設的隔離級別,使用資料庫預設的事務隔離級別。另外四個與JDBC的隔離級別相對應
ISOLATION_READ_UNCOMMITTED 1 這是事務最低的隔離級別,它充許另外一個事務可以看到這個事務未提交的資料。這種隔離級別會產生髒讀,不可重複讀和幻讀。
ISOLATION_READ_COMMITTED 2 保證一個事務修改的資料提交後才能被另外一個事務讀取。另外一個事務不能讀取該事務未提交的資料。
ISOLATION_REPEATABLE_READ 4 這種事務隔離級別可以防止髒讀,不可重複讀。但是可能出現幻讀。
ISOLATION_SERIALIZABLE 8 這是花費最高代價但是最可靠的事務隔離級別。事務被處理為順序執行。除了防止髒讀,不可重複讀外,還避免了幻讀。

呼叫PlatformTransactionManager介面的getTransaction()的方法得到的是TransactionStatus介面的一個實現
TransactionStatus介面
這裡寫圖片描述
主要的方法有:

void flush();//如果適用的話,這個方法用於重新整理底層會話中的修改到資料庫,例如,所有受影響的Hibernate/JPA會話。
boolean hasSavepoint(); // 是否有恢復點
boolean isCompleted();// 是否已完成
boolean isNewTransaction(); // 是否是新的事務
boolean isRollbackOnly(); // 是否為只回滾
void setRollbackOnly();  // 設定為只回滾

可以看出返回的結果是一些事務的狀態,可用來檢索事務的狀態資訊。

配置事務管理器

介紹完Spring事務的管理的流程大概是怎麼走的。接下來可以動手試試Spring是如何配置事務管理器的
例如我在spring-mybatis中配置的:

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

這配置不是唯一的,可以根據自己專案選擇的資料訪問框架靈活配置事務管理器

配置了事務管理器後,事務當然還是得我們自己去操作,Spring提供了兩種事務管理的方式:程式設計式事務管理和宣告式事務管理,讓我們分別看看它們是怎麼做的吧。

程式設計式事務管理

程式設計式事務管理我們可以通過PlatformTransactionManager實現來進行事務管理,同樣的Spring也為我們提供了模板類TransactionTemplate進行事務管理,下面主要介紹模板類,我們需要在配置檔案中配置

    <!--配置事務管理的模板-->
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="transactionManager"></property>
        <!--定義事務隔離級別,-1表示使用資料庫預設級別-->
        <property name="isolationLevelName" value="ISOLATION_DEFAULT"></property>
        <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"></property>
    </bean>

TransactionTemplate幫我們封裝了許多程式碼,節省了我們的工作。下面我們寫個單元測試來測測。
為了測試事務回滾,專門建了一張tbl_accont表,用於模擬存錢的一個場景。service層主要程式碼如下,後面會給出全部程式碼的github地址,有需要的朋友請移步檢視。
BaseSeviceImpl

    //方便測試直接寫的sql
    @Override
    public void insert(String sql, boolean flag) throws Exception {
        dao.insertSql(sql);
        // 如果flag 為 true ,丟擲異常
        if (flag){
            throw new Exception("has exception!!!");
        }
    }
    //獲取總金額
    @Override
    public Integer sum(){
        return dao.sum();
    }

dao對應的sum方法

    <select id="sum" resultType="java.lang.Integer">
        SELECT SUM(money) FROM tbl_account;
    </select>

下面看看測試程式碼

package com.gray;

import com.gray.service.BaseSevice;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

import javax.annotation.Resource;

/**
 * Created by gray on 2017/4/8.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring-test.xml"})
public class TransactionTest{
    @Resource
    private TransactionTemplate transactionTemplate;
    @Autowired
    private BaseSevice baseSevice;

    @Test
    public void transTest() {
        System.out.println("before transaction");
        Integer sum1 = baseSevice.sum();
        System.out.println("before transaction sum: "+sum1);
        System.out.println("transaction....");
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try{
                    baseSevice.insert("INSERT INTO tbl_account VALUES (100);",false);
                    baseSevice.insert("INSERT INTO tbl_account VALUES (100);",false);
                } catch (Exception e){
                    //對於丟擲Exception型別的異常且需要回滾時,需要捕獲異常並通過呼叫status物件的setRollbackOnly()方法告知事務管理器當前事務需要回滾
                    status.setRollbackOnly();
                    e.printStackTrace();
                }
           }
        });
        System.out.println("after transaction");
        Integer sum2 = baseSevice.sum();
        System.out.println("after transaction sum: "+sum2);
    }
}

當baseSevice.insert的第二個引數為false時,我們假設插入資料沒有出現任何問題,測試結果如圖所示:
這裡寫圖片描述
當第二個引數為true時,insert會丟擲一個異常,這是事務就應該回滾,資料前後不應該有變化,如圖所示:
這裡寫圖片描述

宣告式事務管理

宣告式事務管理有兩種常用的方式,一種是基於tx和aop名稱空間的xml配置檔案,一種是基於@Transactional註解,隨著Spring和Java的版本越來越高,大家越趨向於使用註解的方式,下面我們兩個都說。
1.基於tx和aop名稱空間的xml配置檔案
配置檔案

    <tx:advice id="advice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="insert" propagation="REQUIRED" read-only="false"  rollback-for="Exception"/>
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id="pointCut" expression="execution (* com.gray.service.*.*(..))"/>
        <aop:advisor advice-ref="advice" pointcut-ref="pointCut"/>
    </aop:config>

測試程式碼

    @Test
    public void transTest() {
        System.out.println("before transaction");
        Integer sum1 = baseSevice.sum();
        System.out.println("before transaction sum: "+sum1);
        System.out.println("transaction....");
        try{
            baseSevice.insert("INSERT INTO tbl_account VALUES (100);",true);
        } catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("after transaction");
        Integer sum2 = baseSevice.sum();
        System.out.println("after transaction sum: "+sum2);
    }

事務正常執行結果截圖
這裡寫圖片描述
事務出現異常結果截圖
這裡寫圖片描述
2.基於@Transactional註解
這種方式最簡單,也是最為常用的,只需要在配置檔案中開啟對註解事務管理的支援。

    <!-- 宣告式事務管理 配置事物的註解方式注入-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

然後在需要事務管理的地方加上@Transactional註解,如:

    @Transactional(rollbackFor=Exception.class)
    public void insert(String sql, boolean flag) throws Exception {
        dao.insertSql(sql);
        // 如果flag 為 true ,丟擲異常
        if (flag){
            throw new Exception("has exception!!!");
        }
    }

rollbackFor屬性指定出現Exception異常的時候回滾,遇到檢查性的異常需要回滾,預設情況下非檢查性異常,包括error也會自動回滾。
測試程式碼和上面那個一樣
事務正常執行結果截圖
這裡寫圖片描述
事務出現異常結果截圖
這裡寫圖片描述
以上就是對Spring事務進行了詳細的分析和程式碼示例。
最後是測試程式碼Spring事務測試