事務,是為了保障邏輯處理的原子性、一致性、隔離性、永久性。
通過事務控制,可以避免因為邏輯處理失敗而導致產生臟數據等等一系列的問題。
事務有兩個重要特性:
- 事務的傳播行為
- 數據隔離級別
1、事務傳播行為(Transaction Behavior)
傳播行為級別,定義的是事務的控制范圍。通俗點說,執行到某段代碼時,對已存在事務的不同處理方式。
Spring 對 JDBC 的事務隔離級別進行了補充和擴展,并提出了 7 種事務傳播行為。
1)Spring 中提供的 7 種傳播行為
PROPAGATION_REQUIRED,需要事務處理。有則使用,無則新建。這是 Spring 默認的事務傳播行為。該級別的特性是,如果 Context 中已經存在事務,那么就將當前需要使用事務的代碼加入到 Context 的事務中執行,如果當前 Context 中不存在事務,則新建一個事務執行代碼。這個級別通常能滿足大多數的業務場景。
PROPAGATION_SUPPORTS,支持事務處理。該級別的特性是,如果 Context 存在事務,則將代碼加入到 Context 的事務中執行,如果 Context 中沒有事務,則使用 非事務 的方式執行。
PROPAGATION_MANDATORY,強制性要求事務。該級別的特性是,當要以事務的方式執行代碼時,要求 Context 中必須已經存在事務,否則就會拋出異常!使用 MANDATORY 強制事務,可以有效地控制 “必須以事務執行的代碼,卻忘記給它加上事務控制” 這種情況的發生。舉個簡單的例子:有一個方法,對這個方法的要求是一旦被調用,該方法就必須包含在事務中才能正常執行,那么這個方法就適合設置為 PROPAGATION_MANDATORY 強制事務傳播行為,從而在代碼層面加以控制。
PROPAGATION_REQUIRES_NEW,每次都新建一個事務。該級別的特點是,當執行到一段需要事務的代碼時,先判斷 Context 中是否已經有事務存在,如果不存在,就新建一個事務;如果已經存在,就 suspend 掛起當前事務,然后創建一個新事務去執行,直到新事務執行完畢,才會恢復先前掛起的 Context 事務。
PROPAGATION_NOT_SUPPORTED,不支持事務。該級別的特點是,如果發現當前 Context 中有事務存在,則掛起該事務,然后執行邏輯代碼,執行完畢后,恢復先前掛起的 Context 事務。這個傳播行為的事務,可以縮小事務處理過程的范圍。舉個簡單例子,在一個事務中,需要調用一段非核心業務的邏輯操作 1000 次,如果將這段邏輯放在事務中,會導致該事務的范圍變大、生命周期變長,為了避免因事務范圍擴大、周期變長而引發一些的事先沒有考慮到的異常情況發生,可以將這段邏輯設置為 NOT_SUPPORTED 不支持事務傳播行為。
PROPAGATION_NEVER,對事務要求更嚴格,不能出現事務!該級別的特點是,設置了該級別的代碼,在執行前一旦發現 Context 中有事務存在,就會拋出 Runtime 異常,強制停止執行,有我無他!
-
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() {
}
}
- 若
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()
也會跟著一起回滾。 - 假如
ServiceA.methodA()
的傳播行為設置為PROPAGATION_REQUIRED
,ServiceB.methodB()
的傳播行為為PROPAGATION_REQUIRES_NEW
,那么當執行到ServiceB.methodB()
的時候,ServiceA.methodA()
所在的事務就會掛起,而ServiceB.methodB()
會起一個新的事務,等待ServiceB.methodB()
的事務完成以后,A的事務才會繼續執行。PROPAGATION_REQUIRED
與PROPAGATION_REQUIRES_NEW
的事務區別在于事務的回滾程度。因為ServiceB.methodB
是新起一個事務,那么就是存在兩個不同的事務。如果ServiceB.methodB
已經提交,那么ServiceA.methodA
失敗回滾,ServiceB.methodB
是不會回滾的。如果ServiceB.methodB
失敗回滾,如果它拋出的異常被ServiceA.methodA
捕獲,ServiceA.methodA
事務仍然可能會提交。 - 假如
ServiceA.methodA
的事務傳播行為是PROPAGATION_REQUIRED
,而ServiceB.methodB
的事務傳播行為是PROPAGATION_NOT_SUPPORTED
,那么當執行到ServiceB.methodB
時,ServiceA.methodA
的事務掛起,而ServiceB.methodB
以非事務的狀態運行完之后,再繼續ServiceA.methodA
的事務。 - 假如
ServiceA.methodA
的事務傳播行為是PROPAGATION_REQUIRED
, 而ServiceB.methodB
的事務級別是PROPAGATION_NEVER
,那么ServiceB.methodB
執行時就會拋出異常。
2、數據隔離級別(Isolation Level)
在讀取數據庫的過程中,如果兩個事務并發執行,那么多個事務彼此之間,會對數據產生什么樣的影響呢?
這里就引出了事務的第二個特性:數據隔離級別。
數據隔離級別,定義的是事務在數據庫端讀寫方面的控制范圍。
數據隔離級別分為 4 種:
- Serializable:串行化。這是最嚴格的隔離級別,多個事務之間串行執行,資源消耗極大。
- Repeatable Read:可重復讀。該級別可以確保一個已經被事務讀取的數據,另一個事務不能修改這個數據,從而避免了 “臟讀” 和 “不可重復讀”。仍然有較大的性能損耗。
- Read Commited:這是大部分主流數據庫默認的數據隔離級別。該級別下,只允許讀已經提交的數據。例如:當一個事務修改了數據但未提交時,另一個并行事務只會讀到該數據修改之前的內容,從而避免了 “臟讀”。
- 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]