理解 spring 事務傳播行為與數據隔離級別

事務,是為了保障邏輯處理的原子性、一致性、隔離性、永久性。

通過事務控制,可以避免因為邏輯處理失敗而導致產生臟數據等等一系列的問題。

事務有兩個重要特性:

  • 事務的傳播行為
  • 數據隔離級別

1、事務傳播行為(Transaction Behavior)

傳播行為級別,定義的是事務的控制范圍。通俗點說,執行到某段代碼時,對已存在事務的不同處理方式。

Spring 對 JDBC 的事務隔離級別進行了補充和擴展,并提出了 7 種事務傳播行為。

1)Spring 中提供的 7 種傳播行為

  1. PROPAGATION_REQUIRED,需要事務處理。有則使用,無則新建。這是 Spring 默認的事務傳播行為。該級別的特性是,如果 Context 中已經存在事務,那么就將當前需要使用事務的代碼加入到 Context 的事務中執行,如果當前 Context 中不存在事務,則新建一個事務執行代碼。這個級別通常能滿足大多數的業務場景。

  2. PROPAGATION_SUPPORTS,支持事務處理。該級別的特性是,如果 Context 存在事務,則將代碼加入到 Context 的事務中執行,如果 Context 中沒有事務,則使用 非事務 的方式執行。

  3. PROPAGATION_MANDATORY,強制性要求事務。該級別的特性是,當要以事務的方式執行代碼時,要求 Context 中必須已經存在事務,否則就會拋出異常!使用 MANDATORY 強制事務,可以有效地控制 “必須以事務執行的代碼,卻忘記給它加上事務控制” 這種情況的發生。舉個簡單的例子:有一個方法,對這個方法的要求是一旦被調用,該方法就必須包含在事務中才能正常執行,那么這個方法就適合設置為 PROPAGATION_MANDATORY 強制事務傳播行為,從而在代碼層面加以控制。

  4. PROPAGATION_REQUIRES_NEW,每次都新建一個事務。該級別的特點是,當執行到一段需要事務的代碼時,先判斷 Context 中是否已經有事務存在,如果不存在,就新建一個事務;如果已經存在,就 suspend 掛起當前事務,然后創建一個新事務去執行,直到新事務執行完畢,才會恢復先前掛起的 Context 事務。

  5. PROPAGATION_NOT_SUPPORTED,不支持事務。該級別的特點是,如果發現當前 Context 中有事務存在,則掛起該事務,然后執行邏輯代碼,執行完畢后,恢復先前掛起的 Context 事務。這個傳播行為的事務,可以縮小事務處理過程的范圍。舉個簡單例子,在一個事務中,需要調用一段非核心業務的邏輯操作 1000 次,如果將這段邏輯放在事務中,會導致該事務的范圍變大、生命周期變長,為了避免因事務范圍擴大、周期變長而引發一些的事先沒有考慮到的異常情況發生,可以將這段邏輯設置為 NOT_SUPPORTED 不支持事務傳播行為。

  6. PROPAGATION_NEVER,對事務要求更嚴格,不能出現事務!該級別的特點是,設置了該級別的代碼,在執行前一旦發現 Context 中有事務存在,就會拋出 Runtime 異常,強制停止執行,有我無他!

  7. PROPAGATION_NESTED,嵌套事務。該級別的特點是,如果 Context 中存在事務 A,就將當前代碼對應的事務 B 加入到 事務 A 內部,嵌套執行;如果 Context 中不存在事務,則新建事務執行代碼。換句話說,事務 A 與事務 B 之間是父子關系,A 是父,B 是子。理解嵌套事務的關鍵點是:save point。

    父、子事務嵌套、save point 的說明:

    • 父事務會在子事務進入之前創建一個 save point;
    • 子事務 rollback ,父事務只會回滾到 save point,而不會回滾整個父事務;
    • 父事務 commit 之前,必須先 commit 子事務。

2)代碼舉例說明

我在網上看到有一篇文章,采用代碼的方式來解釋事務傳播行為級別,代碼方式很清晰,一看就明白了。

首先準備如下兩個 Service:

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

class ServiceB {
     void methodB() {
     }
}    
  1. ServiceB.methodB() 的傳播行為定義為 PROPAGATION_REQUIRED , 那么在執行 ServiceA.methodA() 的時候,若 ServiceA.methodA() 已經開啟了事務,這時調用 ServiceB.methodB()ServiceB.methodB() 將會運行在 ServiceA.methodA() 的事務內部,而不再開啟新的事務。而假如 ServiceA.methodA() 運行的時候發現自己沒有在事務中,就會為它分配一個新事務。這樣,在 ServiceA.methodA() 或者在 ServiceB.methodB() 內的任何地方出現異常,事務都會被回滾。即使 ServiceB.methodB() 的事務已經被
    提交,但是 ServiceA.methodA() 在接下來的過程中 fail 要回滾,ServiceB.methodB() 也會跟著一起回滾。
  2. 假如 ServiceA.methodA() 的傳播行為設置為 PROPAGATION_REQUIREDServiceB.methodB() 的傳播行為為 PROPAGATION_REQUIRES_NEW,那么當執行到 ServiceB.methodB() 的時候,ServiceA.methodA() 所在的事務就會掛起,而 ServiceB.methodB() 會起一個新的事務,等待 ServiceB.methodB() 的事務完成以后,A的事務才會繼續執行。PROPAGATION_REQUIREDPROPAGATION_REQUIRES_NEW 的事務區別在于事務的回滾程度。因為 ServiceB.methodB 是新起一個事務,那么就是存在兩個不同的事務。如果 ServiceB.methodB 已經提交,那么 ServiceA.methodA 失敗回滾,ServiceB.methodB 是不會回滾的。如果 ServiceB.methodB 失敗回滾,如果它拋出的異常被 ServiceA.methodA 捕獲,ServiceA.methodA 事務仍然可能會提交。
  3. 假如 ServiceA.methodA 的事務傳播行為是 PROPAGATION_REQUIRED,而 ServiceB.methodB 的事務傳播行為是 PROPAGATION_NOT_SUPPORTED,那么當執行到 ServiceB.methodB 時,ServiceA.methodA 的事務掛起,而ServiceB.methodB 以非事務的狀態運行完之后,再繼續 ServiceA.methodA 的事務。
  4. 假如 ServiceA.methodA 的事務傳播行為是 PROPAGATION_REQUIRED, 而 ServiceB.methodB 的事務級別是 PROPAGATION_NEVER ,那么 ServiceB.methodB 執行時就會拋出異常。


2、數據隔離級別(Isolation Level)

在讀取數據庫的過程中,如果兩個事務并發執行,那么多個事務彼此之間,會對數據產生什么樣的影響呢?

這里就引出了事務的第二個特性:數據隔離級別。

數據隔離級別,定義的是事務在數據庫端讀寫方面的控制范圍。

數據隔離級別分為 4 種:

  1. Serializable:串行化。這是最嚴格的隔離級別,多個事務之間串行執行,資源消耗極大。
  2. Repeatable Read:可重復讀。該級別可以確保一個已經被事務讀取的數據,另一個事務不能修改這個數據,從而避免了 “臟讀” 和 “不可重復讀”。仍然有較大的性能損耗。
  3. Read Commited:這是大部分主流數據庫默認的數據隔離級別。該級別下,只允許讀已經提交的數據。例如:當一個事務修改了數據但未提交時,另一個并行事務只會讀到該數據修改之前的內容,從而避免了 “臟讀”。
  4. Read Uncommited:一個事務可以讀取另一個并行事務已修改但還未提交的數據。會產生 “臟讀”。

第 1 種數據準確性最高,但相應地性能最差。第 4 種性能高,但是相應地讀取數據的準確性低。


3、臟讀、幻讀、不可重復讀

臟讀、幻讀、不可重復讀都是并發事務的情況下,因為不同的數據隔離級別而讀取到不同的內容。

臟讀(Dirty Reads)

臟讀,即一個事務讀到了另一個事務還未提交的數據。如果臟讀讀取到的數據最終還是提交了倒還好,但如果這條數據最終回滾了,那么這條數據對于剛剛讀取到它的事務而言,就是一條臟數據。

不可重復讀(Non-repeatable Reads)

不可重復讀,不同的事務讀取同一條數據,讀取到的內容是不同的。也就是說,對某一條數據而言,不同的事務以同樣的重復操作讀取,卻產生了不同的結果。

幻讀(Phantom Reads)

幻讀,一個事務按照某種查詢條件,第一次讀取的數據量和第二次讀取的數據量不一樣,就像幻覺一樣,明明剛才查的是 N 條數據,再查一次就變成了 M 條(M <> N)。


4、如何縮小事務?

假設一個邏輯操作需要檢查的條件有 20 個,能否為了減小事務而將檢查性的內容放到事務之外呢?

很多系統都是在 DAO 的內部開始啟動事務,然后進行操作,最后提交或者回滾。這其中涉及到代碼設計的問題。

小一些的系統可以采用這種方式來做,但是在一些比較大的系統,邏輯較為復雜的系統中,勢必會將過多的業務邏輯嵌入到 DAO 中,導致 DAO 的復用性下降。所以這不是一個好的實踐。

來回答這個問題,能否為了縮小事務,而將一些業務邏輯檢查放到事務外面?

答案是:對于核心的業務檢查邏輯,不能放到事務之外,而且必須要作為分布式下的并發控制!一旦在事務之外做檢查,那么勢必會造成事務A已經檢查過的數據被事務B所修改,導致事務A徒勞無功而且出現并發問題,直接導致業務控制失敗。

所以,在分布式的高并發環境下,對于核心業務邏輯的檢查,要采用加鎖機制。

比如事務開啟需要讀取一條數據進行驗證,然后邏輯操作中需要對這條數據進行修改,最后提交。

這樣的一個過程,如果讀取并驗證的代碼放到事務之外,那么讀取的數據極有可能已經被其他的事務修改,當前事務一旦提交,又會重新覆蓋掉其他事務的數據,導致數據異常。

所以在進入當前事務的時候,必須要將這條數據鎖住,例如使用 Oracle 的 for update 就是一個在分布式環境下很有效的控制手段。

另一種好的實踐方式是使用編程式事務而非聲明式事務,尤其是在較大規模的項目中。對于大量事務的聲明配置,在代碼量非常大的情況下,將是一種折磨。

將 DAO 保持針對一張表的最基本操作,然后業務邏輯的處理放入 manager 或 service 中進行,同時使用編程式事務可以更精確地控制事務范圍。

特別注意的,對于事務內部一些可能拋出異常的情況,捕獲異常時要謹慎,不能隨便的 catch Exception,不然會導致事務的異常被吃掉而不能正常回滾。

5、spring 配置聲明式事務

Spring配置聲明式事務:

  • 配置SessionFactory
  • 配置事務管理器
  • 事務的傳播特性
  • 聲明哪些類,哪些方法需要使用事務

編寫業務邏輯方法:

  • 默認情況下運行期異常才會回滾(包括繼承了RuntimeException子類),普通異常是不會回滾的。
  • 編寫業務邏輯方法時,最好將異常一直向上拋出,在表示層(view)處理。
  • 關于事務邊界的設置,通常設置到業務層,不要添加到 Dao 上。

(1)使用 xml 配置方式:

<!-- 配置SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="configLocation">
     <value>classpath:hibernate.cfg.xml</value>
    </property>
</bean>

<!-- 配置 Hibernate 事務管理器 -->
<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="add*" propagation="REQUIRED"/>
        <tx:method name="del*" propagation="REQUIRED"/>
        <tx:method name="modify*" propagation="REQUIRED"/>
        <tx:method name="*" propagation="REQUIRED" read-only="true"/>
    </tx:attributes>
</tx:advice>

<!-- 聲明哪些類哪些方法需要使用事務 -->
<aop:config>
    <aop:pointcut id="transactionPC" expression="execution(* com.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPC"/>
</aop:config>

<!-- 普通 IOC 注入 -->
<bean id="userManager" class="com.service.UserManagerImpl">
    <property name="logManager" ref="logManager"/>
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>
<bean id="logManager" class="com.service.LogManagerImpl">
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>

關于 spring 配置中 read-only 的說明:

read-only 配置為 true,會告訴 spring 對應的事務應該被最優化為只讀事務。

這是一個最優化提示。在一些情況下,一些事務策略能夠起到顯著的最優化效果,例如在使用 Object/Relational 映射工具(如:Hibernate 或 TopLink)時避免 dirty checking(試圖“刷新”)。

(2)使用 JPA 注解方式

@Service
public class UserServiceImpl implements IUserService {
  @Resource
  IUserDAO userDAO;
  
  //啟動 REQUIRED 默認事務傳播行為的方法
  @Transactional
  public void funNone() throws Exception {
    save(new UserEntity("aaa"));
  }
  
  //啟動 REQUIRED 默認事務傳播行為的方法
  @Transactional(propagation = Propagation.REQUIRED)
  public void funRequire() throws Exception {
    save(new UserEntity("bbb"));
  }
  
  //啟動 Nested 嵌套事務的方法
  @Transactional(propagation = Propagation.NESTED)
  public void funNest() throws Exception {
    save(new UserEntity("ccc"));
  }
  
  //REQUIRES_NEW 事務的方法
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void funRequireNew() throws Exception {
    save(new UserEntity("ddd"));
  }

}

參考文章:

(Spring事務傳播性與隔離級別)[http://blog.csdn.net/edward0830ly/article/details/7569954]

http://blog.sina.com.cn/s/blog_4b5bc0110100z7jr.html

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

推薦閱讀更多精彩內容

  • 很多人喜歡這篇文章,特此同步過來 由淺入深談論spring事務 前言 這篇其實也要歸納到《常識》系列中,但這重點又...
    碼農戲碼閱讀 4,785評論 2 59
  • 一、事務的基本原理 Spring事務的本質其實就是數據庫對事務的支持,沒有數據庫的事務支持,spring是無法提供...
    芭蕾武閱讀 1,702評論 3 12
  • Spring事務機制主要包括聲明式事務和編程式事務,此處側重講解聲明式事務,編程式事務在實際開發中得不到廣泛使用,...
    EnigmaXXX閱讀 675評論 0 0
  • 網絡社交要用好才是真的好 作者:白峰 如今是信息時代,也是網絡時代,這個誰也知道,網絡時代,就是與網絡打交代的時代...
    讀寫人家閱讀 226評論 0 1
  • 愛麗絲來到了鏡之國 手持利劍斬妖除魔 在叢林深處,她見到了 那個女孩 一樣的面孔和衣著 笑著走到她身邊 低低的囈語...
    小舟清江里閱讀 625評論 2 2