1. 程式人生 > 程式設計 >Spring宣告式事務

Spring宣告式事務

Spring宣告式資料庫事務約定

為了省去令人厭煩的try···catch···finally語句,減少那些資料庫連線開閉和事務回滾提交的程式碼,Spring利用其AOP為我們提供了一個資料庫事務的約定流程,通過這個約定流程就可以減少大量冗餘程式碼和一些沒必要的try···catch···finally語句,讓開發者能夠更加集中於業務的開發,而不是資料庫連線資源和事務的功能開發,這樣開發的程式碼可讀性更高,也更好維護。

對於事務,需要通過標註告訴Spring在什麼地方啟用資料庫事務功能。對於宣告式事務,是使用@Transactional進行標註的。這個註解可以標註在類上或者方法上,當標註在類上時,代表這個類所有公共非靜態的方法都將啟用事務功能。在@Transactional中,還允許配置許多的屬性, 事務的隔離級別和傳播行為:又如異常型別 ,從而確定方法發生什麼異常 下回滾事務或者發生什麼異常下不回滾事務等。這些配置內容,是在 Spring IoC 容器在載入時就會將這些配置資訊解析出來,然後把這些資訊存到事務定義器( TransactionDefinition 介面的實現類) 並且記錄哪些類或者方法需要啟動事務功能,採取什麼策略去執行事務。這個過程中,我們所需要做的只是給需要事務的類或者方法標註@Transactional 和配置其屬性而己,並不是很複雜。

隔離級別

因為網際網路應用時刻面對著高併發的環境 ,如商品庫存,時刻都是多個執行緒共 享的資料,這樣就會在多執行緒的環境中扣減商品庫存。對於資料庫而言 就會出現多個事務同時訪問同一記錄的情況, 這樣引起資料出現不一致的情況,便是資料庫的丟失更新( Lost Update)問 題。應該說,隔離級別是資料庫 的概念,有些難度 ,所以在使用它之前應該先了解資料庫的相關知識。

資料庫事務的知識

資料庫事務具有以下4個基本特徵 也就是著名的 ACID 。

  • Atomic (原子性):事務中包含的操作被看作一個整體的業務單元, 這個業務單元中的操作 要麼全部成功,要麼全部失敗,不會出現部分失敗、部分成功的場景。
  • Consistency (一致性):事務在完成時,必須使所有的資料都保持一致狀態,在資料庫中所有的修改都基於事務,保證了資料的完整性。
  • Isolation (隔離性) 這是我們討論的核心內容,正如上述,可能多個應用程式執行緒同時訪問同一資料,這樣資料庫同樣的資料就會在各個不同的事務中被訪問,這樣會產生丟失更新。 為了壓制丟失更新的產生,資料庫定義了隔離級別的概念,通過它的選擇,可以在不同程度 上壓制丟失更新的發生。因為網際網路的應用常常面對高併發的場景,所以隔離性是需要掌握 的重點內容。
  • Durability (永續性):事務結束後,所有的資料會固化到一個地方,如儲存到磁碟當中,即 使斷電重啟後也可以提供給應用程式訪問。

這4個特性,除了隔離性,都還是比較好理解的,所以這裡會更為深入地討論隔離性。在多個事務同時操作資料的情況下,會引發丟失更新的場景,例如,電商有一種商品,在瘋狂搶購中,會出現多個事務同時訪問商品庫存的場景,這樣就會產生丟失更新。下面假設一種商品的庫存數量還有 100, 每次搶購都只能搶購1件商品, 那麼在搶購中就可能出現下面這種場景。

時刻 事務1 事務2
T1 初始庫存100 初始庫存100
T2 扣減庫存,餘99
T3 扣減庫存,餘99
T4 提交事務,庫存變為99
T5 提交事務,庫存變為99

注意T5時刻提交的事務。因為在事務1中,無法感知事務2的操作,這樣它就不知道事務2已經修改過了資料 ,因為它依舊認為只是發生了一筆業務,所以庫存變為了 99 ,而這個結果又是一個 錯誤的結果。這樣, T5時刻事務1提交的事務,就會引發事務2提交結果的丟失 ,為了克服這些問題,資料庫提出了事務之間的隔離級別概念。

為了壓制丟失問題,資料庫提出了4類隔離級別,在不同程度上壓制丟失更新,這4類隔離級別是未提交讀、讀寫提交、可重複讀和序列化,它們會在不通程度上壓制丟失更新的情景。

也許你會有一個疑問,都全部消除丟失更新不就好了嗎,為什麼只是在不同的程度上壓制丟失更新呢?其實這個問題是從兩個角度去看的,一個是資料的一致性,另一個是效能。資料庫現有的技術完全可以避免丟失更新,但是這樣做的代價,就是付出鎖的代價,在網際網路中,系統不單單要考慮資料的一致性,還要考慮系統的效能。試想,在網際網路中使用過多的鎖,一旦出現商品搶購這樣的場景必然會導致大量的執行緒被掛起和恢復,因為使用了鎖之後,一個時刻只能有一個執行緒訪問資料,這樣整個系統就會十分緩慢,當系統被數千甚至數萬使用者同時訪問時,過多的鎖就會引發宕機,大部分使用者執行緒被掛起,等待持有鎖事務的完成,這樣使用者體驗就會十分糟糕。因為使用者等待的時間會十分漫長,一般而言,網際網路系統響應超過5秒,就會讓使用者覺得很不友好,進而引發使用者忠誠度下降的問題。所以選擇隔離級別的時候,既需要考慮資料的一致性避免髒資料,又要考慮系統效能的問題。因此資料庫的規範就提出了4種隔離級別來在不同的程度上壓制丟失更新。下面我們通過商品搶購的場景來講述這4種隔離級別的區別。

  1. 未提交讀

    未提交讀(read uncommitted)是最低的隔離級別,其含義是允許一個事務讀取另外一個事務沒 有提交的資料。未提交讀是一種危險的隔離級別,所以一般在我們實際的開發中應用不廣,但是它 的優點在於併發能力高,適合那些對資料一致性沒有要求而追求高併發的場景,它的最大壞處是出現髒讀。讓我們看看可能發生的髒讀場景。

    時刻 事務1 事務2 備註
    T0 ······ ······ 商品庫存初始化為2
    T1 讀取庫存為2
    T2 扣減庫存 庫存為1
    T3 扣減庫存 庫存為0,讀取事務1未提交的庫存資料
    T4 提交事務 庫儲存存為0
    T5 回滾事務 庫存為0,結果錯誤

    髒讀一般是比較危險的隔離級別,在我們實際應用中採用的不多,為了克服髒讀問題,資料庫隔離級別還提供了讀寫提交(read commited)的級別。

2.讀寫提交

讀寫提交(read commited)隔離級別,是指一個事務只能讀取另外一個事務已經提交的資料,不能讀取未提交的資料。髒讀的場景限制為讀寫提交之後,就變成下面這種場景。

時刻 事務1 事務2 備註
T0 ······ ······ 商品庫存初始化為2
T1 讀取庫存為2
T2 扣減庫存 庫存為1
T3 扣減庫存 庫存為1,讀取不到事務1未提交的庫存資料
T4 提交事務 庫儲存存為1
T5 回滾事務 庫存為1,結果正確

在T3時刻,由於採用讀寫提交的隔離級別,因此事務2不能讀取事務1中未提交的庫存1,所以扣減庫存的結果依舊為1,然後它提交事務,則庫存在T4時刻變為了1。T5時刻,事務1回滾,結果庫存仍為1,這是一個正確結果。但是讀寫提交會產生不可重複讀場景。

時刻 事務1 事務2 備註
T0 ······ ······ 商品庫存初始化為1
T1 讀取庫存為1
T2 扣減庫存 事務為提交
T3 讀取庫存為1 認為可扣減
T4 提交事務 庫存變為0
T5 扣減失敗 失敗,因為此時庫存為0,無法扣減

在T3時刻事務2讀取庫存的時候,因為事務1未提交事務,所以讀出的庫存為1,於是事務2 認為當前可扣減庫存;在T4時刻,事務1己經提交事務,所以在T5時刻,它扣減庫存的時候就發 現庫存為0,於是就無法扣減庫存了。這裡的問題在於事務2之前認為可以扣減,而到扣減那一步卻 發現己經不可以扣減,於是庫存對於事務2而言是一個可變化的值,這樣的現象我們稱為不可重複讀,這就是讀寫提交的一個不足。為了克服這個不足,資料庫的隔離級別還提出了可重複讀的隔離級別,它能夠消除不可重讀的問題。

3.可重複讀

可重複讀的目標是克服讀寫提交中出現的不可重複讀的現象,因為在讀寫提交的時候,可能出現一些值的變化,影響當前事務的執行,如上述的庫存是個變化的值,這個時候資料庫提出了可重 復讀的隔離級別 。這樣就能夠克服不可重複讀的現象。

時刻 事務1 事務2 備註
T0 ······ ······ 商品庫存初始化為1
T1 讀取庫存為1
T2 扣減庫存 事務未提交
T3 嘗試讀取庫存 不允許讀取,等待事務1提交
T4 提交事務 庫存變為0
T5 讀取庫存 庫存為0,無法扣減

可以看到,事務 2在T3 時刻嘗試讀取庫存,但是此時這個庫存己經被事務1事先讀取,所以這 個時候資料庫就阻塞它的讀取, 直至事務1提交,事務2才能讀取庫存的值 此時己經 是T5 時刻, 而讀取到的值為0,這時就已經無法扣減了,顯然在讀寫提交中出現的不可重複讀的場景被消除了,但是這樣也會引發新的問題的出現,這就是幻讀。假設現在商品交易正在進行中,而後臺有人也在 進行查詢分析和列印的業務,讓我們看看幻讀的場景。

時刻 事務1 事務2 備註
T1 讀取庫存50件 商品庫存初始化為100,現在已經銷售50筆,庫存50件
T2 查詢交易記錄,50筆
T3 扣減庫存
T4 插入一筆交易記錄
T5 提交事務 庫存49件,交易記錄51筆
T6 列印交易記錄,51筆 這裡與查詢的不一致,在事務2看來有一筆是幻讀的,與之前查詢的不一致。

這便是幻讀現象。可重複讀和幻讀,是比較難以理解的內容,這裡稍微論述一下。首先這裡的筆數不是資料庫儲存的值,而是一個統計值,商品庫存則是資料庫儲存的值,這一點是要注意的。也就是幻讀不是針對一條資料庫記錄而言,而是多條記錄,例如,這51 筆交易筆數就是多條資料庫記錄統計出來的,而可重複讀是針對資料庫的單一條記錄,例如,商品的庫存是以資料庫裡面的一條記錄儲存的,它可以產生可重複讀,而不能產生幻讀

4.序列化

序列化( Serializable)是資料庫最高的隔離級別,它會要求所有的 SQL 都會按照順序執行,這 樣就可以克服上述隔離級別出現的各種問題,所以它能夠完全保證資料的一致性。

  1. 使用合理的隔離級別

    隔離級別和可能發生的現象

    隔離級別 髒讀 不可重複讀 幻讀
    未提交讀
    讀寫提交 ×
    可重複讀 × ×
    序列化 × × ×

    在現實中一般而言,選擇隔離級別會以讀寫提交為主,它能夠防止髒讀,而不能避免不可 重複讀和幻讀。為了克服資料不一致和效能問題,程式開發者還設計了樂觀鎖,甚至不再使用資料 庫而使用其他的手段。例如,使用 Redis作為資料載體。對於隔離 級別,不同的資料庫的支援也是不一樣的。例如, Oracle 只能支援讀寫提交和序列化,而 MySQL則能夠支援4種,對於 Oracle 預設的隔離級別為讀寫提交, MySQL 則是可重複讀,這些需要根據具體 資料庫來決定。

    只要掌握了隔離級別的含義,使用隔離級別就很簡單,只需要在@Transactiona 配置對應即可

    @Transactional(isolation = Isolation.SERIALIZABLE)
    複製程式碼

    上面的程式碼中我們使用了序列化的隔離級別來保證資料的一致性,這使它將阻塞其他的事務進 行併發,所以它只能運用在那些低井發而又需要保證資料一致性的場景下。對於高井發下又要保證 資料一致性的場景,則需要另行處理了。 當然,有時候一個個地指定隔離級別會很不方便,因此 Spring Boot 可以通過配置檔案指定預設的隔離級別。例如,當我們需要把隔離級別設定為讀寫提交時,可以在 application.properties 檔案加 入預設的配置。

    #隔離級別數字配置的含義
    #-1 資料庫預設隔離級別
    #1 未提交讀
    #2 讀寫提交
    #4 可重複讀
    #8 序列化
    #tomcat資料來源預設隔離級別
    spring.datasource.tomcat.default-transaction-isolation=2
    #dbcp2資料庫連線池預設隔離級別
    #spring.datasource.dbcp2.default-transaction-isolation=2
    複製程式碼

    程式碼中配置了 tomcat 資料來源的預設隔離級別,而註釋的程式碼則是配置了 DBCP2 資料來源的隔離 級別,註釋中己經說明瞭數字所代表的隔離級別,相信讀者也有了比較清晰的認識,這裡配置為 2,說明將資料庫的隔離級別預設為讀寫提交。

傳播行為

package org.springframework.transaction.annotation;

public enum Propagation {
    /**
     * 需要事務,它是預設傳播行為,如果當前存在事務,就沿用當前事務,
     * 否則新建一個事務執行子方法
     */
    REQUIRED(0),/**
     * 支援事務,如果當前存在事務,就沿用當前事務,
     * 如果不存在,則繼續採用無事務的方式執行子方法
     */
    SUPPORTS(1),/**
     * 必須使用事務,如果當前沒有事務,則會拋異常,
     * 如果存在當前事務,就沿用當前事務
     */
    MANDATORY(2),/**
     * 無論當前事務是否存在,都會建立新的事務執行方法,
     * 這樣新事務就可以擁有新的鎖和隔離級別等特性,與當前事務相互獨立
     */
    REQUIRES_NEW(3),/**
     * 不支援事務,當前存在事務時,將掛起事務,執行方法
     */
    NOT_SUPPORTED(4),/**
     * 不支援事務,如果當前方法存在事務,則丟擲異常,否則繼續使用無事務機制執行
     */
    NEVER(5),/**
     * 在當前方法呼叫子方法時,如果子方法發生異常,
     * 只回滾子方法執行過的SQL,而不回滾當前方法的事務
     */
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

複製程式碼