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

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

經過事務控制,能夠避免由於邏輯處理失敗而致使產生髒數據等等一系列的問題。java

事務有兩個重要特性:spring

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

一、事務傳播行爲(Transaction Behavior)

傳播行爲級別,定義的是事務的控制範圍。通俗點說,執行到某段代碼時,對已存在事務的不一樣處理方式。數據庫

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

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

  1. PROPAGATION_REQUIRED,須要事務處理。有則使用,無則新建。這是 Spring 默認的事務傳播行爲。該級別的特性是,若是 Context 中已經存在事務,那麼就將當前須要使用事務的代碼加入到 Context 的事務中執行,若是當前 Context 中不存在事務,則新建一個事務執行代碼。這個級別一般能知足大多數的業務場景。編程

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

  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 執行時就會拋出異常。


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

在讀取數據庫的過程當中,若是兩個事務併發執行,那麼多個事務彼此之間,會對數據產生什麼樣的影響呢?

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

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

數據隔離級別分爲 4 種:

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

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


三、髒讀、幻讀、不可重複讀

髒讀、幻讀、不可重複讀都是併發事務的狀況下,由於不一樣的數據隔離級別而讀取到不一樣的內容。

髒讀(Dirty Reads)

髒讀,即一個事務讀到了另外一個事務還未提交的數據。若是髒讀讀取到的數據最終仍是提交了倒還好,但若是這條數據最終回滾了,那麼這條數據對於剛剛讀取到它的事務而言,就是一條髒數據。

不可重複讀(Non-repeatable Reads)

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

幻讀(Phantom Reads)

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


四、如何縮小事務?

假設一個邏輯操做須要檢查的條件有 20 個,可否爲了減少事務而將檢查性的內容放到事務以外呢?

不少系統都是在 DAO 的內部開始啓動事務,而後進行操做,最後提交或者回滾。這其中涉及到代碼設計的問題。

小一些的系統能夠採用這種方式來作,可是在一些比較大的系統,邏輯較爲複雜的系統中,勢必會將過多的業務邏輯嵌入到 DAO 中,致使 DAO 的複用性降低。因此這不是一個好的實踐。

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

答案是:對於核心的業務檢查邏輯,不能放到事務以外,並且必需要做爲分佈式下的併發控制!一旦在事務以外作檢查,那麼勢必會形成事務A已經檢查過的數據被事務B所修改,致使事務A徒勞無功並且出現併發問題,直接致使業務控制失敗。

因此,在分佈式的高併發環境下,對於核心業務邏輯的檢查,要採用加鎖機制。

好比事務開啓須要讀取一條數據進行驗證,而後邏輯操做中須要對這條數據進行修改,最後提交。

這樣的一個過程,若是讀取並驗證的代碼放到事務以外,那麼讀取的數據極有可能已經被其餘的事務修改,當前事務一旦提交,又會從新覆蓋掉其餘事務的數據,致使數據異常。

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

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

將 DAO 保持針對一張表的最基本操做,而後業務邏輯的處理放入 manager 或 service 中進行,同時使用編程式事務能夠更精確地控制事務範圍。

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

五、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

相關文章
相關標籤/搜索