脫離 Spring 實現複雜嵌套事務,之一(必要的概念)

    寫這篇博文的目的首先是與你們分享一下如何用更輕量化的辦法去實現 Spring 那種完善的事務控制。 java

爲何須要嵌套事務? sql

    咱們知道,數據庫事務是爲了保證數據庫操做原子性而設計的一種解決辦法。例如執行兩條 update 當第二條執行失敗時候順便將前面執行的那條一塊兒回滾。 數據庫

    這種應用場景比較常見,例如銀行轉賬。A帳戶減小的錢要加到B帳戶上。這兩個SQL操做只要有一個失敗,必須一塊兒撤銷。 多線程

    可是一般銀行轉賬業務不管是否操做成功都會忘數據庫里加入系統日誌。若是日誌輸出與帳戶金額調整在一個事務裏,一旦事務回滾日誌也會跟着一塊兒消失。這時候就須要嵌套事務。 測試

時間 事務
T1 開始事務
T2 記錄日誌...
T3 轉帳500元
T4 記錄日誌...
T5 遞交事務

爲何有了嵌套事務還須要獨立事務? spa

    假設如今銀行須要知道當前正在進行轉帳的實時交易數。 .net

    咱們知道一個完整的轉帳業務會記錄兩第二天志,第一次用以記錄是什麼業務,第二次會記錄這個業務總共耗時。所以完成這個功能時咱們只須要查詢還未進行第二次記錄的那些交易日誌便可得出結果。 線程

時間 事務1 事務2
T1 開始事務
T2 記錄日誌...
T3
開始子事務
T4 轉帳500元
T5
遞交子事務
T6 記錄日誌...
T7 遞交事務

    分析一下上面這種嵌套事務就知道不會得出正確的結果,首先第一條日誌會被錄入數據庫的先決條件是轉帳操做成功以後的遞交事務。 設計

    若是事務遞交了,交易也就完成了。這樣得出的查詢結果根本不是實時數據。所以嵌套事務解決方案不能知足需求。假若日誌輸出操做使用的是一個全新的事務,就會保證能夠查詢到正確的數據。(以下)。 日誌

時間 事務1 事務2
T1 開始事務 開始事務
T2 記錄日誌...
T3 遞交事務
T4 轉帳500元
T5 開始事務
T6 記錄日誌...
T7 遞交事務 遞交事務

Spring 提供的幾種事務控制

1.PROPAGATION_REQUIRED(加入已有事務)
    嘗試加入已經存在的事務中,若是沒有則開啓一個新的事務。

2.RROPAGATION_REQUIRES_NEW(獨立事務)
    掛起當前存在的事務,並開啓一個全新的事務,新事務與已存在的事務之間彼此沒有關係。

3.PROPAGATION_NESTED(嵌套事務)
    在當前事務上開啓一個子事務(Savepoint),若是遞交主事務。那麼連同子事務一同遞交。若是遞交子事務則保存點以前的全部事務都會被遞交。

4.PROPAGATION_SUPPORTS(跟隨環境)
    是指 Spring 容器中若是當前沒有事務存在,就以非事務方式執行;若是有,就使用當前事務。

5.PROPAGATION_NOT_SUPPORTED(非事務方式)
    是指若是存在事務則將這個事務掛起,並使用新的數據庫鏈接。新的數據庫鏈接不使用事務。

6.PROPAGATION_NEVER(排除事務)
    當存在事務時拋出異常,不然就已非事務方式運行。

7.PROPAGATION_MANDATORY(須要事務)
    若是不存在事務就拋出異常,不然就已事務方式運行。

事務管理器API接口

    對於開發者而言,對事務管理器的操做只會涉及到「get」、「commit」、「rollback」三個基本操做。所以數據庫事務管理器的接口相對簡單。以下:

/**
 * 數據源的事務管理器。
 * @version : 2013-10-30
 * @author 趙永春(zyc@hasor.net)
 */
public interface TransactionManager {
    //開啓事務,使用不一樣的傳播屬性來建立事務。
    public TransactionStatus getTransaction(TransactionBehavior behavior);
    //遞交事務
    public void commit(TransactionStatus status) throws SQLException;
    //回滾事務
    public void rollBack(TransactionStatus status) throws SQLException;
}

取得的事務狀態使用下面這個接口進行封裝:

/**
 * 表示一個事務狀態
 * @version : 2013-10-30
 * @author 趙永春(zyc@hasor.net)
 */
public interface TransactionStatus {
    //獲取事務使用的傳播行爲
    public TransactionBehavior getTransactionBehavior();
    //獲取事務的隔離級別
    public TransactionLevel getIsolationLevel();
    //
    //事務是否已經完成,當事務已經遞交或者被回滾就標誌着已完成
    public boolean isCompleted();
    //是否已被標記爲回滾,若是返回值爲 true 則在commit 時會回滾該事務
    public boolean isRollbackOnly();
    //是否爲只讀模式。
    public boolean isReadOnly();
    //是否使用了一個全新的數據庫鏈接開啓事務
    public boolean isNewConnection();
    //測試該事務是否被掛起
    public boolean isSuspend();
    //表示事務是否攜帶了一個保存點,嵌套事務一般會建立一個保存點做爲嵌套事務與上一層事務的分界點。
    //注意:若是事務中包含保存點,則在遞交事務時只處理這個保存點。
    public boolean hasSavepoint();
    //
    //設置事務狀態爲回滾,做爲替代拋出異常進而觸發回滾操做。
    public void setRollbackOnly();
    //設置事務狀態爲只讀。
    public void setReadOnly();
}

    除此以外還須要聲明一個枚舉用以肯定事務傳播屬性:

/**
 * 事務傳播屬性
 * @version : 2013-10-30
 * @author 趙永春(zyc@hasor.net)
 */
public enum TransactionBehavior {
    //
    //加入已有事務,嘗試加入已經存在的事務中,若是沒有則開啓一個新的事務。
    PROPAGATION_REQUIRED,
    //
    //獨立事務,掛起當前存在的事務,並開啓一個全新的事務,新事務與已存在的事務之間彼此沒有關係。
    RROPAGATION_REQUIRES_NEW,
    //
    //嵌套事務,在當前事務中開啓一個子事務。若是事務遞交將連同上一級事務一同遞交。
    PROPAGATION_NESTED,
    //
    //跟隨環境,若是當前沒有事務存在,就以非事務方式執行;若是有,就使用當前事務。
    PROPAGATION_SUPPORTS,
    //
    //非事務方式,若是當前沒有事務存在,就以非事務方式執行;若是有,就將當前事務掛起。
    PROPAGATION_NOT_SUPPORTED,
    //
    //排除事務,若是當前沒有事務存在,就以非事務方式執行;若是有,就拋出異常。
    PROPAGATION_NEVER,
    //
    //強制要求事務,若是當前沒有事務存在,就拋出異常;若是有,就使用當前事務。
    PROPAGATION_MANDATORY,
}

約定條件

    在實現相似 Spring 那樣的事務控制以前須要作幾個約定:

  • 一、每條線程只能夠擁有一個活動的數據庫鏈接,稱之爲「當前鏈接」。
  • 二、程序在執行期間如持有數據庫鏈接,須要使用「引用計數」標記。
  • 三、一個事務狀態中最多隻能存在一個子事務(Savepoint)。
  • 四、當前的數據庫鏈接是能夠被隨時更換的,即便它的「引用計數不爲0」。
  • 五、數據庫鏈接具有「事務狀態」。

下面就講講爲何要先有這些約定:

1、爲何要有當前鏈接?

    通常數據庫事務操做遵循(開啓事務 -> 操做 -> 關閉事務)三個步驟,這三個步驟能夠看做是固定的。你不能隨意調換它們的順序。在多線程下若是數據庫鏈接共享,將會打破這個順序。由於極有可能線程 A 將線程 B 的事務一塊兒遞交了。

    因此爲了減小沒必要要的麻煩咱們使用「當前鏈接」來存放數據庫鏈接,而且約定當前鏈接是與當前線程綁定的。也就是說您在線程A下啓動的數據庫事務,是不會影響到線程B下的數據庫事務。它們之間使用的數據庫鏈接彼此互不干預。

2、爲何須要引用計數?

    引用計數是被用來肯定當前數據庫鏈接是否能夠被 close。當引用計數器收到「減法」操做時候若是計數器爲零或者小於零,則認爲應用程序已經不在使用這個鏈接,能夠放心 close。

3、爲何一個事務狀態中只能存在一個子事務?

    答:子事務與父事務會被封裝到不一樣的兩個事務狀態中。所以事務管理器從設計上就不容許一個事務狀態持有兩個事務特徵,這樣會讓系統設計變得複雜。

4、當前的數據庫鏈接是能夠被隨時更換的,即便它的「引用計數不爲0」

    咱們知道,隨意更換當前鏈接有可能會引起數據庫鏈接釋放錯誤。可是依然須要這個風險的操做是因爲「獨立事務」的要求。

    在獨立事務中若是當前鏈接已經存在事務,則會新建一個數據庫鏈接做爲當前鏈接並開啓它的事務。

    獨立事務的設計是爲了保證,處於事務控制中的應用程序對數據庫操做是不會有其它代碼影響到它。而且它也不會影響到別人,故此稱之爲「獨立」。

    此外在前面提到的場景「爲何有了嵌套事務還須要獨立事務?」也已經解釋獨立事務存在的必要性。

5、數據庫鏈接具有「事務狀態」

    事務管理器在建立事務對象時,須要知道當前數據鏈接是否已經具備事務狀態。

    若是還沒有開啓事務,事務管理器能夠認爲這個鏈接是一個新的(new狀態),此時在事務管理器收到 commit 請求時,具備new狀態時能夠放心大膽的去處理事務遞交操做。

    假若存在事務,則頗有可能在事務管理器建立事務對象以前已經對數據庫進行了操做。基於這種狀況下事務管理器就不能冒昧的進行 commit 或者 rollback。

    所以事務狀態是能夠用來決定事務管理器是否真實的去執行 commit 和 rollback 方法。有時候這個狀態也被稱之爲「new」狀態。

數據庫鏈接可能存在的狀況

    不管是否存在事務管理器,當前數據庫鏈接都會具備一些固定的狀態。那麼下面就先分析一下當前數據庫鏈接可能存在的狀況有哪些?

  • 當前鏈接已經有程序使用(引用計數 !=0)
  • 當前鏈接還沒有有程序使用(引用計數 ==0)
  • 當前鏈接已經開啓了事務(autoCommit 值爲 false)
  • 當前鏈接還沒有開啓事務(autoCommit 值爲 true)

    上面雖然列出了四種狀況,可是實際上能夠看做兩個狀態值。

  • 1. 引用計數是否爲0,表示是否能夠關閉鏈接
  • 2. autoCommit是否爲false(表示當前鏈接是否具備事務狀態)

    引用計數爲0,表示的是沒有任何程序在執行時須要或者正在使用這個鏈接。也就是說這個數據庫鏈接的存在與否根本不重要。

    autoCommit這個狀態是來自於 Connection 接口,它表示的含義是數據庫鏈接是否支持自動遞交。若是爲 true 表示Connection 在每次執行一條 sql 語句時都會跟隨一個 commit 遞交操做。若是執行失敗,天然就至關於 rollback。所以能夠看出這個值的狀況反映出當前數據庫鏈接的事務狀態。

  • 1.有事務,引用大於0
  • 2.有事務,引用等於0
  • 3.沒事務,引用大於0
  • 4.沒事務,引用等於0

理解「new」狀態

    new狀態是用來標記當事務管理器建立新的事務狀態時,當前鏈接的事務狀態是如何的。而且輔助事務管理器決定究竟如何處理事務遞交&回滾操做。

    上面這條定義準確的定義了 new 狀態的做用,以及如何獲取。那麼咱們要看看它究竟會決定哪些事情?

    根據定義,new 狀態是用來輔助事務遞交與回滾操做。咱們先假設下面這個場景:

public static void main(){
  DataSource ds= ......;
  Connection conn = DataSourceUtil.getConnection(ds);//取得數據庫鏈接,會致使引用計數+1
  conn.setAutoCommit(false);//開啓事務
  conn.execute("update ...");//預先執行的 update 語句

  TransactionStatus status = tm.getTransaction(PROPAGATION_REQUIRED);//加入到已有事務,引用計數+1
  insertData();//執行數據庫插入
  tm.commit(status);//引用計數-1

  conn.commit();//遞交事務
  DataSourceUtil.releaseConnection(conn,ds);//釋放鏈接,引用計數-1
}
public static void insertData(){
  jdbc.execute("insert into ...");//執行插入語句,在執行過程當中引用計數會 +1,而後在-1
}

    在上面這個場景中,在調用 insertData 方法以前使用 REQUIRED(加入已有事務) 行爲建立了一個事務。

    從邏輯上來說 insertData 方法雖然在完成以後會進行事務遞交操做,可是因爲它的事務已經加入到了更外層的事務中。所以這個事務遞交應該是被忽略的,最終的遞交應當是由 conn.commit() 代碼進行。

    咱們分析一下在這個場景下 new 狀態是怎樣的。

    咱們不難發如今 getTransaction 方法以前,應用程序實際上已經持有了數據庫鏈接(引用計數+1),而隨後它又關閉了自動遞交,開啓了事務。這樣一來,就不知足 new 狀態的特徵。

   最後在 tm.commit(status) 時候,事務管理器會參照 new 狀態。若是爲 false 則不觸發遞交事務的操做。這偏偏保護了上面這個代碼邏輯的正常運行。

    如今咱們修改上面的代碼以下:

public static void main(){
  DataSource ds= ......;
  Connection conn = DataSourceUtil.getConnection(ds);//取得數據庫鏈接,會致使引用計數+1
  conn.execute("update ...");//預先執行的 update 語句

  TransactionStatus status = tm.getTransaction(PROPAGATION_REQUIRED);//加入到已有事務,引用計數+1
  insertData();//執行數據庫插入
  tm.commit(status);//引用計數-1

  DataSourceUtil.releaseConnection(conn,ds);//釋放鏈接,引用計數-1
}
public static void insertData(){
  jdbc.execute("insert into ...");//執行插入語句,在執行過程當中引用計數會 +1,而後在-1
}

   咱們發現,本來在申請鏈接以後的開啓事務代碼和釋放鏈接以前的事務遞交代碼被刪除了。也就是說在 getTransaction 時候數據庫鏈接是知足 new 狀態的特徵的。

   程序中雖然在第四行有一條 SQL 執行語句,可是因爲 Connection 在執行這個 SQL語句的時候使用的是自動遞交事務。所以在 insertData 以後即便出現 rollback 也不會影響到它。

   最後在 tm.commit(status) 時候,事務管理器參照 new 狀態。爲 true 觸發了交事務的操做。這也偏偏知足了上面這個代碼邏輯的正常運行。

@黃勇 這裏也有一篇文章簡介事務控制 http://my.oschina.net/huangyong/blog/160012 他在文章中詳細說述說了,事務隔離級別。這篇文章正好是本文做爲基礎部分的一個重要補充。在這裏很是感謝 勇哥的貢獻。


相關博文:

相關文章
相關標籤/搜索