由淺入深談論spring事務

很多人喜歡這篇文章,特此同步過來 由淺入深談論spring事務

前言

這篇其實也要歸納到《常識》系列中,但這重點又是spring的介紹,故歸檔在spring系列中。

工作很多年,除了學生時代學過,事務還真沒有用過。過去開發游戲時,完全不用事務;現在互聯網開發,也沒有使用事務的場景,不要見怪。

概念

對于事務(Transaction)的概念,網上有各種版本,大同小異,

事務就是是由一系列對系統中數據進行讀寫的操作組成的一個程序執行單元,狹義上的事務特指數據庫事務。

事務是一系列的動作,它們綜合在一起才是一個完整的工作單元,這些動作必須全部完成,如果有一個失敗的話,那么事務就會回滾到最開始的狀態,仿佛什么都沒發生過一樣。

在企業級應用程序開發中,事務管理必不可少的技術,用來確保數據的完整性和一致性。

比如你去ATM機取1000塊錢,大體有兩個步驟:首先輸入密碼金額,銀行卡扣掉1000元錢;然后ATM出1000元錢。這兩個步驟必須是要么都執行要么都不執行。如果銀行卡扣除了1000塊但是ATM出錢失敗的話,你將會損失1000元;如果銀行卡扣錢失敗但是ATM卻出了1000塊,那么銀行將損失1000元。所以,如果一個步驟成功另一個步驟失敗對雙方都不是好事,如果不管哪一個步驟失敗了以后,整個取錢過程都能回滾,也就是完全取消所有操作的話,這對雙方都是極好的。

事務的特性

大名鼎鼎的ACID

  1. 原子性(Atomicity),事務必須是一個原子的操作序列單元,一次事務只允許存在兩種狀態,全部成功或全部失敗,任何一個操作失敗都將導致整個事務失敗
  2. 一致性(Consistency),事務的執行不能破壞系統數據的完整性和一致性,如果未完成的事務對系統數據的修改有一部分已經寫入物理數據庫,這時系統數據就處于不一致狀態
  3. 隔離性(Isolation),在并發環境中,不同的事務操作相同的數據時,虛相互隔離不能相互干擾
  4. 持久性(Durability),事務一旦提交,對系統數據的變更就應該是永久的,必須被永久保存下來,即使服務器宕機了,只要數據庫能夠重新啟動,就一定能夠恢復到事務成功結束時的狀態

事務并發處理問題

如果沒有鎖定且多個用戶同時訪問一個數據庫,則當他們的事務同時使用相同的數據時可能會發生問題。由于并發操作帶來的數據不一致性包括:丟失數據修改、讀”臟”數據(臟讀)、不可重復讀、產生幽靈數據:

假設數據庫中有如下一張表:

image

第一類丟失更新(lost update)

回滾丟失

在完全未隔離事務的情況下,兩個事物更新同一條數據資源,某一事物異常終止,回滾造成第一個完成的更新也同時丟失。


image

在T1時刻開啟了事務1,T2時刻開啟了事務2,

在T3時刻事務1從數據庫中取出了id="402881e535194b8f0135194b91310001"的數據,

T4時刻事務2取出了同一條數據,

T5時刻事務1將age字段值更新為30,

T6時刻事務2更新age為35并提交了數據,

但是T7事務1回滾了事務age最后的值依然為20,事務2的更新丟失了,

這種情況就叫做"第一類丟失更新(lost update)"。

臟讀(dirty read)

事務沒提交,提前讀取

如果第二個事務查詢到第一個事務還未提交的更新數據,形成臟讀


image

在T1時刻開啟了事務1,T2時刻開啟了事務2,

在T3時刻事務1從數據庫中取出了id="402881e535194b8f0135194b91310001"的數據,

在T5時刻事務1將age的值更新為30,但是事務還未提交,

T6時刻事務2讀取同一條記錄,獲得age的值為30,但是事務1還未提交,

若在T7時刻事務1回滾了事務2的數據就是錯誤的數據(臟數據),

這種情況叫做" 臟讀(dirty read)"。

虛讀(phantom read)

一個事務執行兩次查詢,第二次結果集包含第一次中沒有或者某些行已被刪除,造成兩次結果不一致,只是另一個事務在這兩次查詢中間插入或者刪除了數據造成的

image

在T1時刻開啟了事務1,T2時刻開啟了事務2,

T3時刻事務1從數據庫中查詢所有記錄,記錄總共有一條,

T4時刻事務2向數據庫中插入一條記錄,T6時刻事務2提交事務。

T7事務1再次查詢數據數據時,記錄變成兩條了。

這種情況是"虛讀(phantom read)"。

不可重復讀(unrepeated read)

一個事務兩次讀取同一行數據,結果得到不同狀態結果,如中間正好另一個事務更新了該數據,兩次結果相異,不可信任


image

在T1時刻開啟了事務1,T2時刻開啟了事務2,

在T3時刻事務1從數據庫中取出了id="402881e535194b8f0135194b91310001"的數據,此時age=20,

T4時刻事務2查詢同一條數據,

T5事務2更新數據age=30,T6時刻事務2提交事務,

T7事務1查詢同一條數據,發現數據與第一次不一致。

這種情況就是"不可重復讀(unrepeated read)"

第二類丟失更新(second lost updates)

覆蓋丟失

不可重復讀的特殊情況,如果兩個事務都讀取同一行,然后兩個都進行寫操作,并提交,第一個事務所做的改變就會丟失。

image

在T1時刻開啟了事務1,T2時刻開啟了事務2,

T3時刻事務1更新數據age=25,

T5時刻事務2更新數據age=30,

T6時刻提交事務,

T7時刻事務2提交事務,把事務1的更新覆蓋了。

這種情況就是"第二類丟失更新(second lost updates)"。

并發問題總結

不可重復讀的重點是修改 :

同樣的條件 , 你讀取過的數據 , 再次讀取出來發現值不一樣了

幻讀的重點在于新增或者刪除

同樣的條件 , 第 1 次和第 2 次讀出來的記錄數不一樣

第一類更新丟失(回滾丟失)

第二類更新丟失(覆蓋丟失)

隔離級別

解決并發問題的途徑是什么?答案是:采取有效的隔離機制。怎樣實現事務的隔離呢?隔離機制的實現必須使用鎖

一般在編程的時候只需要設置隔離等級

數據庫系統提供四種事務隔離級別:

  1. 未提交讀(READ UNCOMMITTED )

最低隔離級別,一個事務能讀取到別的事務未提交的更新數據,很不安全,可能出現丟失更新、臟讀、不可重復讀、幻讀;

  1. 提交讀(READ COMMITTED)

一個事務能讀取到別的事務提交的更新數據,不能看到未提交的更新數據,不會出現丟失更新、臟讀,但可能出現不可重復讀、幻讀;

  1. 可重復讀(REPEATABLE READ)

保證同一事務中先后執行的多次查詢將返回同一結果,不受其他事務影響,不可能出現丟失更新、臟讀、不可重復讀,但可能出現幻讀;

  1. 序列化(SERIALIZABLE)

最高隔離級別,不允許事務并發執行,而必須串行化執行,最安全,不可能出現更新、臟讀、不可重復讀、幻讀,但是效率最低。

image

隔離級別越高,數據庫事務并發執行性能越差,能處理的操作越少。
所以一般地,推薦使用REPEATABLE READ級別保證數據的讀一致性。
對于幻讀的問題,可以通過加鎖來防止

image

MySQL支持這四種事務等級,默認事務隔離級別是REPEATABLE READ。

Oracle數據庫支持READ COMMITTED 和 SERIALIZABLE這兩種事務隔離級別,
所以Oracle數據庫不支持臟讀

Oracle數據庫默認的事務隔離級別是READ COMMITTED

不可重復讀和幻讀的區別是,不可重復讀對應的表的操作是更改(UPDATE),而幻讀對應的表的操作是插入(INSERT),兩種的應對策略不一樣。對于不可重復讀,只需要采用行級鎖防止該記錄被更新即可,而對于幻讀必須加個表級鎖,防止在表中插入數據

樂觀鎖(Optimistic Lock)和悲觀鎖(Pessimistic Lock)

最重要的分類就是樂觀鎖(Optimistic Lock)和悲觀鎖(Pessimistic Lock),這實際上是兩種鎖策略

樂觀鎖,顧名思義就是非常樂觀,非常相信真善美,每次去讀數據都認為其它事務沒有在寫數據,所以就不上鎖,快樂的讀取數據,而只在提交數據的時候判斷其它事務是否搞過這個數據了,如果搞過就rollback。樂觀鎖相當于一種檢測沖突的手段,可通過為記錄添加版本或添加時間戳來實現。

悲觀鎖,對其它事務抱有保守的態度,每次去讀數據都認為其它事務想要作祟,所以每次讀數據的時候都會上鎖,直到取出數據。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性,但隨之而來的是各種開銷。悲觀鎖相當于一種避免沖突的手段。

選擇標準:如果并發量不大,或數據沖突的后果不嚴重,則可以使用樂觀鎖;而如果并發量大或數據沖突后果比較嚴重(對用戶不友好),那么就使用悲觀鎖。

分共享鎖(S鎖,Shared Lock)和排他鎖(X鎖,Exclusive Lock)

從讀寫角度,分共享鎖(S鎖,Shared Lock)和排他鎖(X鎖,Exclusive Lock),也叫讀鎖(Read Lock)和寫鎖(Write Lock)。
理解:

持有S鎖的事務只讀不可寫。

如果事務A對數據D加上S鎖后,其它事務只能對D加上S鎖而不能加X鎖。

持有X鎖的事務可讀可寫。

如果事務A對數據D加上X鎖后,其它事務不能再對D加鎖,直到A對D的鎖解除。

表級鎖(Table Lock)和行級鎖(Row Lock)

從鎖的粒度角度,主要分為表級鎖(Table Lock)和行級鎖(Row Lock)。

表級鎖將整個表加鎖,性能開銷最小

用戶可以同時進行讀操作。當一個用戶對表進行寫操作時,用戶可以獲得一個寫鎖,寫鎖禁止其他的用戶讀寫操作。寫鎖比讀鎖的優先級更高,即使有讀操作已排在隊列中,一個被申請的寫鎖仍可以排在所隊列的前列。

行級鎖僅對指定的記錄進行加鎖

這樣其它進程可以對同一個表中的其它記錄進行讀寫操作。行級鎖粒度最小,開銷大,能夠支持高并發,可能會出現死鎖。

MySQL的MyISAM引擎使用表級鎖,而InnoDB支持表級鎖和行級鎖,默認是行級鎖。
還有BDB引擎使用頁級鎖,即一次鎖定一組記錄,并發性介于行級鎖和表級鎖之間。

三級鎖協議

三級加鎖協議是為了保證正確的事務并發操作,事務在讀、寫數據庫對象是需要遵循的加鎖規則。

一級封鎖協議:事務T在修改數據R之前必須對它加X鎖,直到事務結束方可釋放。而若事務T只是讀數據,不進行修改,則不需加鎖,因此一級加鎖協議下可能會出現臟讀和不可重復讀。

二級加鎖協議:在一級加鎖協議的基礎上,加上這樣一條規則——事務T在讀取數據R之前必須對它加S鎖,直到讀取完畢以后釋放。二級加鎖協議下可能會出現不可重復讀。

三級加鎖協議:在一級加鎖協議的基礎上,加上這樣一條規則——事務T在讀取數據R之前必須對它加S鎖,直到事務結束方可釋放。三級加鎖協議避免了臟讀和不可重復讀的問題

Spring事務

Spring事務管理的實現有許多細節,如果對整個接口框架有個大體了解會非常有利于我們理解事務

image
image

Spring事務管理器

Spring事務管理涉及的接口的聯系如下:


image

Spring并不直接管理事務,而是提供了多種事務管理器,他們將事務管理的職責委托給Hibernate或者JTA等持久化機制所提供的相關平臺框架的事務來實現。

Spring事務管理器的接口是org.springframework.transaction.PlatformTransactionManager,
通過這個接口,Spring為各個平臺如JDBC、Hibernate等都提供了對應的事務管理器,

但是具體的實現就是各個平臺自己的事情了

/**
 * This is the central interface in Spring's transaction infrastructure.
 * Applications can use this directly, but it is not primarily meant as API:
 * Typically, applications will work with either TransactionTemplate or
 * declarative transaction demarcation through AOP.
 */
public interface PlatformTransactionManager {
    TransactionStatus getTransaction(TransactionDefinition definition)
        throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}

標準的jdbc處理事務代碼

Connection conn = DataSourceUtils.getConnection();
 //開啟事務
conn.setAutoCommit(false);
try {
    Object retVal = callback.doInConnection(conn);
    conn.commit(); //提交事務
    return retVal;
}catch (Exception e) {
    conn.rollback();//回滾事務
    throw e;
}finally {
    conn.close();
}

spring對應的TranstactionTemplate處理

public class TransactionTemplate extends DefaultTransactionDefinition
        implements TransactionOperations, InitializingBean {
        
    @Override
    public <T> T execute(TransactionCallback<T> action) throws TransactionException {
        if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
            return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
        }
        else {
            TransactionStatus status = this.transactionManager.getTransaction(this);
            T result;
            try {
                result = action.doInTransaction(status);
            }
            catch (RuntimeException ex) {
                // Transactional code threw application exception -> rollback
                rollbackOnException(status, ex);
                throw ex;
            }
            catch (Error err) {
                // Transactional code threw error -> rollback
                rollbackOnException(status, err);
                throw err;
            }
            catch (Exception ex) {
                // Transactional code threw unexpected exception -> rollback
                rollbackOnException(status, ex);
                throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
            }
            this.transactionManager.commit(status);
            return result;
        }
    }
    
}

具體的事務管理機制對Spring來說是透明的,它并不關心那些,那些是對應各個平臺需要關心的,所以Spring事務管理的一個優點就是為不同的事務API提供一致的編程模型,如JTA、JDBC、Hibernate、JPA。下面分別介紹各個平臺框架實現事務管理的機制。

image

JDBC事務

如果應用程序中直接使用JDBC來進行持久化,DataSourceTransactionManager會為你處理事務邊界。為了使用DataSourceTransactionManager,你需要使用如下的XML將其裝配到應用程序的上下文定義中:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

實際上,DataSourceTransactionManager是通過調用java.sql.Connection來管理事務,而后者是通過DataSource獲取到的。通過調用連接的commit()方法來提交事務,同樣,事務失敗則通過調用rollback()方法進行回滾。

Hibernate事務

如果應用程序的持久化是通過Hibernate實習的,那么你需要使用HibernateTransactionManager。對于Hibernate3,需要在Spring上下文定義中添加如下的<bean>聲明:

<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

sessionFactory屬性需要裝配一個Hibernate的session工廠,HibernateTransactionManager的實現細節是它將事務管理的職責委托給org.hibernate.Transaction對象,而后者是從Hibernate Session中獲取到的。當事務成功完成時,HibernateTransactionManager將會調用Transaction對象的commit()方法,反之,將會調用rollback()方法。

事務屬性

事務管理器接口PlatformTransactionManager通過getTransaction(TransactionDefinition definition)方法來得到事務,這個方法里面的參數是TransactionDefinition類,這個類就定義了一些基本的事務屬性。

public interface TransactionDefinition {
    int getPropagationBehavior(); // 返回事務的傳播行為
    int getIsolationLevel(); // 返回事務的隔離級別,事務管理器根據它來控制另外一個事務可以看到本事務內的哪些數據
    int getTimeout();  // 返回事務必須在多少秒內完成
    boolean isReadOnly(); // 事務是否只讀,事務管理器能夠根據這個返回值進行優化,確保事務是只讀的
} 

那么什么是事務屬性呢?事務屬性可以理解成事務的一些基本配置,描述了事務策略如何應用到方法上。事務屬性包含了5個方面

image

傳播行為

事務的第一個方面是傳播行為(propagation behavior)。
當事務方法被另一個事務方法調用時,必須指定事務應該如何傳播。

為什么需要定義傳播?

在我們用SSH開發項目的時候,我們一般都是將事務設置在Service層 那么當我們調用Service層的一個方法的時候它能夠保證我們的這個方法中執行的所有的對數據庫的更新操作保持在一個事務中,在事務層里面調用的這些方法要么全部成功,要么全部失敗。那么事務的傳播特性也是從這里說起的。
如果你在你的Service層的這個方法中,除了調用了Dao層的方法之外,還調用了本類的其他的Service方法,那么在調用其他的Service方法的時候,這個事務是怎么規定的呢,我必須保證我在我方法里掉用的這個方法與我本身的方法處在同一個事務中,否則如果保證事物的一致性。事務的傳播特性就是解決這個問題的,“事務是會傳播的”在Spring中有針對傳播特性的多種配置我們大多數情況下只用其中的一種:PROPGATION_REQUIRED:這個配置項的意思是說當我調用service層的方法的時候開啟一個事務(具體調用那一層的方法開始創建事務,要看你的aop的配置),那么在調用這個service層里面的其他的方法的時候,如果當前方法產生了事務就用當前方法產生的事務,否則就創建一個新的事務。這個工作使由Spring來幫助我們完成的。
以前沒有Spring幫助我們完成事務的時候我們必須自己手動的控制事務,例如當我們項目中僅僅使用hibernate,而沒有集成進spring的時候,我們在一個service層中調用其他的業務邏輯方法,為了保證事物必須也要把當前的hibernate session傳遞到下一個方法中,或者采用ThreadLocal的方法,將session傳遞給下一個方法,其實都是一個目的。現在這個工作由spring來幫助我們完成,就可以讓我們更加的專注于我們的業務邏輯。而不用去關心事務的問題。

Spring定義了七種傳播行為:

PROPAGATION_REQUIRED

如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。

PROPAGATION_SUPPORTS

支持當前事務,如果當前沒有事務,就以非事務方式執行。

PROPAGATION_MANDATORY

使用當前的事務,如果當前沒有事務,就拋出異常。

PROPAGATION_REQUIRES_NEW

新建事務,如果當前存在事務,把當前事務掛起。

PROPAGATION_NOT_SUPPORTED

以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。

PROPAGATION_NEVER

以非事務方式執行,如果當前存在事務,則拋出異常。

PROPAGATION_NESTED

如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。

傳播行為詳細

通過實例嘗試一下各個傳播屬性

ServiceA {
       
     void methodA() {
         ServiceB.methodB();
     }
}
  
ServiceB {
       
     void methodB() {
     }
}
PROPAGATION_REQUIRED

如果存在一個事務,則支持當前事務。如果沒有事務則開啟一個新的事務

//事務屬性 PROPAGATION_REQUIRED
methodA{
    ……
    methodB();
    ……
}
//事務屬性 PROPAGATION_REQUIRED
methodB{
   ……
}

單獨調用methodB方法:

main{ 
    metodB(); 
}

相當于

Main{ 
    Connection con=null; 
    try{ 
        con = getConnection(); 
        con.setAutoCommit(false); 

        //方法調用
        methodB(); 

        //提交事務
        con.commit(); 
    } Catch(RuntimeException ex) { 
        //回滾事務
        con.rollback();   
    } finally { 
        //釋放資源
        closeCon(); 
    } 
} 

Spring保證在methodB方法中所有的調用都獲得到一個相同的連接。在調用methodB時,沒有一個存在的事務,所以獲得一個新的連接,開啟了一個新的事務。

只是在ServiceB.methodB內的任何地方出現異常,ServiceB.methodB將會被回滾,不會引起ServiceA.methodA的回滾

單獨調用MethodA時,在MethodA內又會調用MethodB.

執行效果相當于:

main{ 
    Connection con = null; 
    try{ 
        con = getConnection(); 
        methodA(); 
        con.commit(); 
    } catch(RuntimeException ex) { 
        con.rollback(); 
    } finally {    
        closeCon(); 
    }  
} 

調用MethodA時,環境中沒有事務,所以開啟一個新的事務.

當在MethodA中調用MethodB時,環境中已經有了一個事務,所以methodB就加入當前事務

ServiceA.methodA或者ServiceB.methodB無論哪個發生異常methodA和methodB作為一個整體都將一起回滾

PROPAGATION_SUPPORTS

如果存在一個事務,支持當前事務。如果沒有事務,則非事務的執行。但是對于事務同步的事務管理器,PROPAGATION_SUPPORTS與不使用事務有少許不同

//事務屬性 PROPAGATION_REQUIRED
methodA(){
  methodB();
}

//事務屬性 PROPAGATION_SUPPORTS
methodB(){
  ……
}

單純的調用methodB時,methodB方法是非事務的執行的。當調用methdA時,methodB則加入了methodA的事務中,事務地執行

PROPAGATION_MANDATORY

如果已經存在一個事務,支持當前事務。如果沒有一個活動的事務,則拋出異常

//事務屬性 PROPAGATION_REQUIRED
methodA(){
    methodB();
}

//事務屬性 PROPAGATION_MANDATORY
    methodB(){
    ……
}

當單獨調用methodB時,因為當前沒有一個活動的事務,則會拋出異常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);

當調用methodA時,methodB則加入到methodA的事務中,事務地執行

PROPAGATION_REQUIRES_NEW

總是開啟一個新的事務。如果一個事務已經存在,則將這個存在的事務掛起。

//事務屬性 PROPAGATION_REQUIRED
methodA(){
    doSomeThingA();
    methodB();
    doSomeThingB();
}

//事務屬性 PROPAGATION_REQUIRES_NEW
methodB(){
    ……
}

調用A方法:

main(){
    methodA();
}

相當于

main(){
    TransactionManager tm = null;
    try{
        //獲得一個JTA事務管理器
        tm = getTransactionManager();
        tm.begin();//開啟一個新的事務
        Transaction ts1 = tm.getTransaction();
        doSomeThing();
        tm.suspend();//掛起當前事務
        try{
            tm.begin();//重新開啟第二個事務
            Transaction ts2 = tm.getTransaction();
            methodB();
            ts2.commit();//提交第二個事務
        } Catch(RunTimeException ex) {
            ts2.rollback();//回滾第二個事務
        } finally {
            //釋放資源
        }
        //methodB執行完后,恢復第一個事務
        tm.resume(ts1);
        doSomeThingB();
        ts1.commit();//提交第一個事務
    } catch(RunTimeException ex) {
        ts1.rollback();//回滾第一個事務
    } finally {
        //釋放資源
    }
}

在這里,我把ts1稱為外層事務,ts2稱為內層事務。從上面的代碼可以看出,ts2與ts1是兩個獨立的事務,互不相干。

Ts2是否成功并不依賴于ts1

如果methodA方法在調用methodB方法后的doSomeThingB方法失敗了,而methodB方法所做的結果依然被提交。

而除了 methodB之外的其它代碼導致的結果卻被回滾了

PROPAGATION_NOT_SUPPORTED

以非事務方式執行操作,如果當前存在事務,就把當前事務掛起,

//事務屬性 PROPAGATION_REQUIRED
methodA(){
    doSomeThingA();
    methodB();
    doSomeThingB();
}

//事務屬性 PROPAGATION_NOT_SUPPORTED
methodB(){
    ……
}

當前不支持事務。比如ServiceA.methodA的事務級別是PROPAGATION_REQUIRED ,
而ServiceB.methodB的事務級別是PROPAGATION_NOT_SUPPORTED ,
那么當執行到ServiceB.methodB時,ServiceA.methodA的事務掛起,而他以非事務的狀態運行完,再繼續ServiceA.methodA的事務

PROPAGATION_NEVER

不能在事務中運行。假設ServiceA.methodA的事務級別是PROPAGATION_REQUIRED,
而ServiceB.methodB的事務級別是PROPAGATION_NEVER ,
那么ServiceB.methodB就要拋出異常了。

PROPAGATION_NESTED

開始一個 "嵌套的" 事務, 它是已經存在事務的一個真正的子事務. 潛套事務開始執行時, 它將取得一個 savepoint. 如果這個嵌套事務失敗, 我們將回滾到此 savepoint. 潛套事務是外部事務的一部分, 只有外部事務結束后它才會被提交.

比如我們設計ServiceA.methodA的事務級別為PROPAGATION_REQUIRED,ServiceB.methodB的事務級別為PROPAGATION_NESTED,那么當執行到ServiceB.methodB的時候,ServiceA.methodA所在的事務就會掛起,ServiceB.methodB會起一個新的子事務并設置savepoint,等待ServiceB.methodB的事務完成以后,他才繼續執行

因為ServiceB.methodB是外部事務的子事務,那么

  1. 如果ServiceB.methodB已經提交,那么ServiceA.methodA失敗回滾,ServiceB.methodB也將回滾。
  2. 如果ServiceB.methodB失敗回滾,如果他拋出的異常被ServiceA.methodA的try..catch捕獲并處理,ServiceA.methodA事務仍然可能提交;如果他拋出的異常未被ServiceA.methodA捕獲處理,ServiceA.methodA事務將回滾。

理解Nested的關鍵是savepoint。

與PROPAGATION_REQUIRES_NEW的區別

  1. RequiresNew每次都創建新的獨立的物理事務,而Nested只有一個物理事務;
  2. Nested嵌套事務回滾或提交不會導致外部事務回滾或提交,但外部事務回滾將導致嵌套事務回滾,而 RequiresNew由于都是全新的事務,所以之間是無關聯的;
  3. Nested使用JDBC 3的保存點實現,即如果使用低版本驅動將導致不支持嵌套事務。
    使用嵌套事務,必須確保具體事務管理器實現的nestedTransactionAllowed屬性為true,否則不支持嵌套事務,如DataSourceTransactionManager默認支持,而HibernateTransactionManager默認不支持,需要我們來開啟。

在 spring 中使用 PROPAGATION_NESTED的前提:

  1. 我們要設置 transactionManager 的 nestedTransactionAllowed 屬性為 true, 注意, 此屬性默認為 false!!!
  2. java.sql.Savepoint 必須存在, 即 jdk 版本要 1.4+
  3. Connection.getMetaData().supportsSavepoints() 必須為 true, 即 jdbc drive 必須支持 JDBC 3.0
image

隔離規則

用來解決并發事務時出現的問題,其使用TransactionDefinition中的靜態變量來指定

  1. ISOLATION_DEFAULT 使用后端數據庫默認的隔離級別
  2. ISOLATION_READ_UNCOMMITTED 最低的隔離級別,允許讀取尚未提交的數據變更,可能會導致臟讀、幻讀或不可重復讀
  3. ISOLATION_READ_COMMITTED 允許讀取并發事務已經提交的數據,可以阻止臟讀,但是幻讀或不可重復讀仍有可能發生
  4. ISOLATION_REPEATABLE_READ 對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止臟讀和不可重復讀,但幻讀仍有可能發生
  5. ISOLATION_SERIALIZABLE 最高的隔離級別,完全服從ACID的隔離級別,確保阻止臟讀、不可重復讀以及幻讀,也是最慢的事務隔離級別,因為它通常是通過完全鎖定事務相關的數據庫表來實現的

可以使用DefaultTransactionDefinition類的setIsolationLevel(TransactionDefinition. ISOLATION_READ_COMMITTED)來指定隔離級別,其中此處表示隔離級別為提交讀

也可以使用或setIsolationLevelName(“ISOLATION_READ_COMMITTED”)方式指定,其中參數就是隔離級別靜態變量的名字,但不推薦這種方式

事務只讀

將事務標識為只讀,只讀事務不修改任何數據;

對于JDBC只是簡單的將連接設置為只讀模式,對于更新將拋出異常;

對于一些其他ORM框架有一些優化作用,如在Hibernate中,Spring事務管理器將執行“session.setFlushMode(FlushMode.MANUAL)”
即指定Hibernate會話在只讀事務模式下不用嘗試檢測和同步持久對象的狀態的更新。

如果使用設置具體事務管理的validateExistingTransaction屬性為true(默認false),將確保整個事務傳播鏈都是只讀或都不是只讀


image

第二個addressService.save()不能設置成false

對于錯誤的事務只讀設置將拋出IllegalTransactionStateException異常,并伴隨“Participating transaction with definition [……] is not marked as read-only……”信息,表示參與的事務只讀屬性設置錯誤

事務超時

設置事務的超時時間,單位為秒,默認為-1表示使用底層事務的超時時間

使用如setTimeout(100)來設置超時時間,如果事務超時將拋出org.springframework.transaction.TransactionTimedOutException異常并將當前事務標記為應該回滾,即超時后事務被自動回滾

可以使用具體事務管理器實現的defaultTimeout屬性設置默認的事務超時時間,如DataSourceTransactionManager. setDefaultTimeout(10)

回滾規則

spring事務管理器會捕捉任何未處理的異常,然后依據規則決定是否回滾拋出異常的事務

默認配置下,Spring只有在拋出的異常為運行時unchecked異常時才回滾該事務,也就是拋出的異常為RuntimeException的子類(Errors也會導致事務回滾),而拋出checked異常則不會導致事務回滾。可以明確的配置在拋出那些異常時回滾事務,包括checked異常。也可以明確定義那些異常拋出時不回滾事務

如何改變默認規則

  1. 讓checked例外也回滾:在整個方法前加上 @Transactional(rollbackFor=Exception.class)
  2. 讓unchecked例外不回滾: @Transactional(notRollbackFor=RunTimeException.class)
  3. 不需要事務管理的(只查詢的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)

事務狀態

上面講到的調用PlatformTransactionManager接口的getTransaction()的方法得到的是TransactionStatus接口的一個實現,這個接口的內容如下:

public interface TransactionStatus{
    boolean isNewTransaction(); // 是否是新的事物
    boolean hasSavepoint(); // 是否有恢復點
    void setRollbackOnly();  // 設置為只回滾
    boolean isRollbackOnly(); // 是否為只回滾
    boolean isCompleted; // 是否已完成
} 

可以發現這個接口描述的是一些處理事務提供簡單的控制事務執行和查詢事務狀態的方法,在回滾或提交的時候需要應用對應的事務狀態

編程式和聲明式事務

Spring提供了對編程式事務和聲明式事務的支持,編程式事務允許用戶在代碼中精確定義事務的邊界

而聲明式事務(基于AOP)有助于用戶將操作與事務規則進行解耦。

簡單地說,編程式事務侵入到了業務代碼里面,但是提供了更加詳細的事務管理;而聲明式事務由于基于AOP,所以既能起到事務管理的作用,又可以不影響業務代碼的具體實現。

編程式

Spring提供兩種方式的編程式事務管理,分別是:使用TransactionTemplate和直接使用PlatformTransactionManager

使用TransactionTemplate

采用TransactionTemplate和采用其他Spring模板,如JdbcTempalte和HibernateTemplate是一樣的方法。它使用回調方法,把應用程序從處理取得和釋放資源中解脫出來。如同其他模板,TransactionTemplate是線程安全的。代碼片段:

    TransactionTemplate tt = new TransactionTemplate(); // 新建一個TransactionTemplate
    Object result = tt.execute(
        new TransactionCallback(){  
            public Object doTransaction(TransactionStatus status){  
                updateOperation();  
                return resultOfUpdateOperation();  
            }  
    }); // 執行execute方法進行事務管理

使用TransactionCallback()可以返回一個值。如果使用TransactionCallbackWithoutResult則沒有返回值

使用PlatformTransactionManager

示例代碼如下:

DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
//定義一個某個框架平臺的TransactionManager,如JDBC、Hibernate
dataSourceTransactionManager.setDataSource(this.getJdbcTemplate().getDataSource()); // 設置數據源
    DefaultTransactionDefinition transDef = new DefaultTransactionDefinition(); // 定義事務屬性
    transDef.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED); // 設置傳播行為屬性
    TransactionStatus status = dataSourceTransactionManager.getTransaction(transDef); // 獲得事務狀態
    try {
        // 數據庫操作
        dataSourceTransactionManager.commit(status);// 提交
    } catch (Exception e) {
        dataSourceTransactionManager.rollback(status);// 回滾
    }

聲明式

有幾種實現方式,不一一羅列了

使用tx攔截器

<!-- 定義事務管理器(聲明式的事務) --> 
    <bean id="transactionManager"
        class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>

    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED" />
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id="interceptorPointCuts"
            expression="execution(* com.bluesky.spring.dao.*.*(..))" />
        <aop:advisor advice-ref="txAdvice"
            pointcut-ref="interceptorPointCuts" />       
    </aop:config> 

全注解

<tx:annotation-driven transaction-manager="transactionManager" />
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager ">
        <property name="dataSource">
            <ref bean="basicDataSource" />
        </property>
</bean>

Spring源碼片段

在《BeanPostProcessor學習》中提到了AOP的實現方式,聲明式事務實現是基于AOP

首先得解析xml配置,TxNamespaceHandler

@Override
    public void init() {
        registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
        registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser());
        registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser());
    }

主要是TransactionInterceptor類

public Object invoke(final MethodInvocation invocation) throws Throwable {
        // Work out the target class: may be {@code null}.
        // The TransactionAttributeSource should be passed the target class
        // as well as the method, which may be from an interface.
        Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

        // Adapt to TransactionAspectSupport's invokeWithinTransaction...
        //主要邏輯在父類
        return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
            @Override
            public Object proceedWithInvocation() throws Throwable {
                return invocation.proceed();
            }
        });
    }

核心邏輯,還得看父類TransactionAspectSupport#invokeWithinTransaction

邏輯主干很清晰

if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
            // Standard transaction demarcation with getTransaction and commit/rollback calls.
            // 判斷創建Transaction
            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;
        }

創建事務createTransactionIfNecessary

主要邏輯在PlatformTransactionManager#getTransaction()

public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
        //得到各個不同數據源的事務對象,spring盡然沒有把transaction對象抽象出來,很是奇怪
        Object transaction = doGetTransaction();

        // Cache debug flag to avoid repeated checks.
        boolean debugEnabled = logger.isDebugEnabled();

        if (definition == null) {
            // Use defaults if no transaction definition given.
            definition = new DefaultTransactionDefinition();
        }

        //此事務是否已經存在
        if (isExistingTransaction(transaction)) {
            // Existing transaction found -> check propagation behavior to find out how to behave.
            return handleExistingTransaction(definition, transaction, debugEnabled);
        }

        // Check definition settings for new transaction.
        if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
            throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
        }

        // No existing transaction found -> check propagation behavior to find out how to proceed.
        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
            throw new IllegalTransactionStateException(
                    "No existing transaction found for transaction marked with propagation 'mandatory'");
        }
        //這三種都是新建事務
        else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            SuspendedResourcesHolder suspendedResources = suspend(null);
            if (debugEnabled) {
                logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
            }
            try {
                boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                DefaultTransactionStatus status = newTransactionStatus(
                        definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                //開始獲取鏈接,開啟事務,綁定資源到當前線程
                doBegin(transaction, definition);
                prepareSynchronization(status, definition);
                return status;
            }
            catch (RuntimeException | Error ex) {
                resume(null, suspendedResources);
                throw ex;
            }
        }
        else {
            // Create "empty" transaction: no actual transaction, but potentially synchronization.
            if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
                logger.warn("Custom isolation level specified but no actual transaction initiated; " +
                        "isolation level will effectively be ignored: " + definition);
            }
            boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
            return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
        }
    }

TransactionStatus

這兒返回的是TransactionStatus

public interface TransactionStatus extends SavepointManager, Flushable {

    boolean isNewTransaction();

    boolean hasSavepoint();

    void setRollbackOnly();

    boolean isRollbackOnly();

    void flush();

    boolean isCompleted();

TransactionInfo

事務信息

protected final class TransactionInfo {

        private final PlatformTransactionManager transactionManager;

        private final TransactionAttribute transactionAttribute;

        private final String joinpointIdentification;

        private TransactionStatus transactionStatus;

        private TransactionInfo oldTransactionInfo;
    }
        

commitTransactionAfterReturning提交事務

邏輯到了AbstractPlatformTransactionManager#processRollback

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
        try {
            boolean unexpectedRollback = unexpected;

            try {
                triggerBeforeCompletion(status);
                //有savepoint,
                if (status.hasSavepoint()) {
                    if (status.isDebug()) {
                        logger.debug("Rolling back transaction to savepoint");
                    }
                    status.rollbackToHeldSavepoint();
                }
                else if (status.isNewTransaction()) {
                    if (status.isDebug()) {
                        logger.debug("Initiating transaction rollback");
                    }
                    //回滾事務
                    doRollback(status);
                }
                else {
                    // Participating in larger transaction
                    //在一個事務中,就先設置回滾標識,等父事務一起回滾
                    if (status.hasTransaction()) {
                        if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
                            if (status.isDebug()) {
                                logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                            }
                            doSetRollbackOnly(status);
                        }
                        else {
                            if (status.isDebug()) {
                                logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
                            }
                        }
                    }
                    else {
                        logger.debug("Should roll back transaction but cannot - no transaction available");
                    }
                    // Unexpected rollback only matters here if we're asked to fail early
                    if (!isFailEarlyOnGlobalRollbackOnly()) {
                        unexpectedRollback = false;
                    }
                }
            }
            catch (RuntimeException | Error ex) {
                triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
                throw ex;
            }

            triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

            // Raise UnexpectedRollbackException if we had a global rollback-only marker
            if (unexpectedRollback) {
                throw new UnexpectedRollbackException(
                        "Transaction rolled back because it has been marked as rollback-only");
            }
        }
        finally {
            cleanupAfterCompletion(status);
        }

completeTransactionAfterThrowing回滾事務

protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
        //有事務才能回滾
        if (txInfo != null && txInfo.hasTransaction()) {
            if (logger.isTraceEnabled()) {
                logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
                        "] after exception: " + ex);
            }
            //回滾在 (ex instanceof RuntimeException || ex instanceof Error)
            if (txInfo.transactionAttribute.rollbackOn(ex)) {
                try {
                    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
                }
                catch (TransactionSystemException ex2) {
                    logger.error("Application exception overridden by rollback exception", ex);
                    ex2.initApplicationException(ex);
                    throw ex2;
                }
                catch (RuntimeException ex2) {
                    logger.error("Application exception overridden by rollback exception", ex);
                    throw ex2;
                }
                catch (Error err) {
                    logger.error("Application exception overridden by rollback error", ex);
                    throw err;
                }
            }
            else {
                // We don't roll back on this exception.
                // Will still roll back if TransactionStatus.isRollbackOnly() is true.
                try {
                    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
                }
                catch (TransactionSystemException ex2) {
                    logger.error("Application exception overridden by commit exception", ex);
                    ex2.initApplicationException(ex);
                    throw ex2;
                }
                catch (RuntimeException ex2) {
                    logger.error("Application exception overridden by commit exception", ex);
                    throw ex2;
                }
                catch (Error err) {
                    logger.error("Application exception overridden by commit error", ex);
                    throw err;
                }
            }
        }
    }

總結

一個完整的事務介紹結束了。框架就是封裝一切,透明一切,簡化一切。本質的流程不會變

歡迎關注【碼農戲碼】
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring事務機制主要包括聲明式事務和編程式事務,此處側重講解聲明式事務,編程式事務在實際開發中得不到廣泛使用,...
    EnigmaXXX閱讀 675評論 0 0
  • 1. 事務基礎 1.1 什么是事務 所謂事務就是用戶定義的一個數據庫操作序列,這些操作要么全做,要么全不做,是一個...
    執筆弄風月閱讀 539評論 0 0
  • 事務有四個特性:ACID 原子性(Atomicity):事務是一個原子操作,由一系列動作組成。事務的原子性確保動作...
    jiangmo閱讀 1,252評論 0 7
  • 一、事務的基本原理 Spring事務的本質其實就是數據庫對事務的支持,沒有數據庫的事務支持,spring是無法提供...
    芭蕾武閱讀 1,702評論 3 12
  • 希望每個紀念日都是快樂的,但今天不快樂。
    AmNobody閱讀 329評論 0 0