1 初步理解
理解事務之前,先講一個你日常生活中最常干的事:取錢。
比如你去ATM機取1000塊錢,大體有兩個步驟:首先輸入密碼金額,銀行卡扣掉1000元錢;然后ATM出1000元錢。這兩個步驟必須是要么都執行要么都不執行。如果銀行卡扣除了1000塊但是ATM出錢失敗的話,你將會損失1000元;如果銀行卡扣錢失敗但是ATM卻出了1000塊,那么銀行將損失1000元。所以,如果一個步驟成功另一個步驟失敗對雙方都不是好事,如果不管哪一個步驟失敗了以后,整個取錢過程都能回滾,也就是完全取消所有操作的話,這對雙方都是極好的。
事務就是用來解決類似問題的。事務是一系列的動作,它們綜合在一起才是一個完整的工作單元,這些動作必須全部完成,如果有一個失敗的話,那么事務就會回滾到最開始的狀態,仿佛什么都沒發生過一樣。
在企業級應用程序開發中,事務管理必不可少的技術,用來確保數據的完整性和一致性。
事務有四個特性:ACID
- 原子性(Atomicity):事務是一個原子操作,由一系列動作組成。事務的原子性確保動作要么全部完成,要么完全不起作用。
- 一致性(Consistency):一旦事務完成(不管成功還是失敗),系統必須確保它所建模的業務處於一致的狀態,而不會是部分完成部分失敗。在現實中的數據不應該被破壞。
- 隔離性(Isolation):可能有許多事務會同時處理相同的數據,因此每個事務都應該與其他事務隔離開來,防止數據損壞。
- 持久性(Durability):一旦事務完成,無論發生什么系統錯誤,它的結果都不應該受到影響,這樣就能從任何系統崩潰中恢復過來。通常情況下,事務的結果被寫到持久化存儲器中。
2 核心接口
Spring事務管理的實現有許多細節,如果對整個接口框架有個大體了解會非常有利於我們理解事務,下面通過講解Spring的事務接口來了解Spring實現事務的具體策略。 Spring事務管理涉及的接口的聯系如下:
2.1 事務管理器
Spring並不直接管理事務,而是提供了多種事務管理器,他們將事務管理的職責委托給Hibernate或者JTA等持久化機制所提供的相關平台框架的事務來實現。 Spring事務管理器的接口是org.springframework.transaction.PlatformTransactionManager,通過這個接口,Spring為各個平台如JDBC、Hibernate等都提供了對應的事務管理器,但是具體的實現就是各個平台自己的事情了。此接口的內容如下:
-
Public interface PlatformTransactionManager() {
-
// 由TransactionDefinition得到TransactionStatus對象
-
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
-
// 提交
-
Void commit(TransactionStatus status) throws TransactionException;
-
// 回滾
-
Void rollback(TransactionStatus status) throws TransactionException;
-
}
從這里可知具體的具體的事務管理機制對Spring來說是透明的,它並不關心那些,那些是對應各個平台需要關心的,所以Spring事務管理的一個優點就是為不同的事務API提供一致的編程模型,如JTA、JDBC、Hibernate、JPA。下面分別介紹各個平台框架實現事務管理的機制。
2.1.1 JDBC事務
如果應用程序中直接使用JDBC來進行持久化,DataSourceTransactionManager會為你處理事務邊界。為了使DataSourceTransactionManager,你需要使用如下的XML將其裝配到應用程序的上下文定義中:
-
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
-
<property name="dataSource" ref="dataSource" />
-
</bean>
實際上,DataSourceTransactionManager是通過調用java.sql.Connection來管理事務,而后者是通過DataSource獲取到的。通過調用連接的commit()方法來提交事務,同樣,事務失敗則通過調用rollback()方法進行回滾。
2.1.2 Hibernate事務
如果應用程序的持久化是通過Hibernate實習的,那么你需要使用HibernateTransactionManager。對於Hibernate3,需要在Spring上下文定義中添加如下的<bean>
聲明:
-
<bean id=
"transactionManager"
class=
"org.springframework.orm.hibernate3.HibernateTransactionManager">
-
<property name=
"sessionFactory" ref=
"sessionFactory" />
-
</bean>
sessionFactory屬性需要裝配一個Hibernate的session工廠,HibernateTransactionManager的實現細節是它將事務管理的職責委托org.hibernate.Transaction對象,而后者是從Hibernate Session中獲取到的。當事務成功完成時,HibernateTransactionManager將會調用Transaction對象的commit()方法,反之,將會調用rollback()方法。
2.2 基本事務屬性的定義(PROPAGATION_REQUIRED應該是我們首先的事務傳播行為。它能夠滿足我們大多數的事務需求。)
上面講到的事務管理器接口PlatformTransactionManager通過getTransaction(TransactionDefinition definition)方法來得到事務,這個方法里面的參數是TransactionDefinition類,這個類就定義了一些基本的事務屬性。 那么什么是事務屬性呢?事務屬性可以理解成事務的一些基本配置,描述了事務策略如何應用到方法上。事務屬性包含了5個方面,如圖所示:
而TransactionDefinition接口內容如下:
-
public interface TransactionDefinition {
-
int getPropagationBehavior();
// 返回事務的傳播行為
-
int getIsolationLevel();
// 返回事務的隔離級別,事務管理器根據它來控制另外一個事務可以看到本事務內的哪些數據
-
int getTimeout();
// 返回事務必須在多少秒內完成
-
boolean isReadOnly();
// 事務是否只讀,事務管理器能夠根據這個返回值進行優化,確保事務是只讀的
-
}
2.2.1 傳播行為
事務的第一個方面是傳播行為(propagation behavior)。當事務方法被另一個事務方法調用時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中運行,也可能開啟一個新事務,並在自己的事務中運行。Spring定義了七種傳播行為:
傳播行為 | 含義 |
---|---|
PROPAGATION_REQUIRED | 表示當前方法必須運行在事務中。如果當前事務存在,方法將會在該事務中運行。否則,會啟動一個新的事務 |
PROPAGATION_SUPPORTS | 表示當前方法不需要事務上下文,但是如果存在當前事務的話,那么該方法會在這個事務中運行 |
PROPAGATION_MANDATORY | 表示該方法必須在事務中運行,如果當前事務不存在,則會拋出一個異常 |
PROPAGATION_REQUIRED_NEW | 表示當前方法必須運行在它自己的事務中。一個新的事務將被啟動。如果存在當前事務,在該方法執行期間,當前事務會被掛起。如果使用JTATransactionManager的話,則需要訪問TransactionManager |
PROPAGATION_NOT_SUPPORTED | 表示該方法不應該運行在事務中。如果存在當前事務,在該方法運行期間,當前事務將被掛起。如果使用JTATransactionManager的話,則需要訪問TransactionManager |
PROPAGATION_NEVER | 表示當前方法不應該運行在事務上下文中。如果當前正有一個事務在運行,則會拋出異常 |
PROPAGATION_NESTED | 表示如果當前已經存在一個事務,那么該方法將會在嵌套事務中運行。嵌套的事務可以獨立於當前事務進行單獨地提交或回滾。如果當前事務不存在,那么其行為與PROPAGATION_REQUIRED一樣。注意各廠商對這種傳播行為的支持是有所差異的。可以參考資源管理器的文檔來確認它們是否支持嵌套事務 |
-
//事務屬性 PROPAGATION_REQUIRED
-
methodA{
-
……
-
methodB();
-
……
-
}
-
//事務屬性 PROPAGATION_REQUIRED
-
methodA{
-
……
-
methodB();
-
……
-
}
使用spring聲明式事務,spring使用AOP來支持聲明式事務,會根據事務屬性,自動在方法調用之前決定是否開啟一個事務,並在方法執行之后決定事務提交或回滾事務。
單獨調用methodB方法:
-
main{
-
metodB();
-
}
相當於
-
Main{
-
Connection con=
null;
-
try{
-
con = getConnection();
-
con.setAutoCommit(
false);
-
-
//方法調用
-
methodB();
-
-
//提交事務
-
con.commit();
-
} Catch(RuntimeException ex) {
-
//回滾事務
-
con.rollback();
-
}
finally {
-
//釋放資源
-
closeCon();
-
}
-
}
Spring保證在methodB方法中所有的調用都獲得到一個相同的連接。在調用methodB時,沒有一個存在的事務,所以獲得一個新的連接,開啟了一個新的事務。 單獨調用MethodA時,在MethodA內又會調用MethodB.執行效果相當於:
-
main{
-
Connection con =
null;
-
try{
-
con = getConnection();
-
methodA();
-
con.commit();
-
}
catch(RuntimeException ex) {
-
con.rollback();
-
}
finally {
-
closeCon();
-
}
-
}
調用MethodA時,環境中沒有事務,所以開啟一個新的事務.當在MethodA中調用MethodB時,環境中已經有了一個事務,所以methodB就加入當前事務。
(2)PROPAGATION_SUPPORTS 如果存在一個事務,支持當前事務。如果沒有事務,則非事務的執行。但是對於事務同步的事務管理器,PROPAGATION_SUPPORTS與不使用事務有少許不同。
-
//事務屬性 PROPAGATION_REQUIRED
-
methodA(){
-
methodB();
-
}
-
-
//事務屬性 PROPAGATION_SUPPORTS
-
methodB(){
-
……
-
}
單純的調用methodB時,methodB方法是非事務的執行的。當調用methdA時,methodB則加入了methodA的事務中,事務地執行。
(3)PROPAGATION_MANDATORY 如果已經存在一個事務,支持當前事務。如果沒有一個活動的事務,則拋出異常。
-
//事務屬性 PROPAGATION_REQUIRED
-
methodA(){
-
methodB();
-
}
-
-
//事務屬性 PROPAGATION_MANDATORY
-
methodB(){
-
……
-
}
當單獨調用methodB時,因為當前沒有一個活動的事務,則會拋出異常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);當調用methodA時,methodB則加入到methodA的事務中,事務地執行。
(4)PROPAGATION_REQUIRES_NEW 總是開啟一個新的事務。如果一個事務已經存在,則將這個存在的事務掛起。
-
//事務屬性 PROPAGATION_REQUIRED
-
methodA(){
-
doSomeThingA();
-
methodB();
-
doSomeThingB();
-
}
-
-
//事務屬性 PROPAGATION_REQUIRES_NEW
-
methodB(){
-
……
-
}
調用A方法:
-
main(){
-
methodA();
-
}
相當於
-
main(){
-
TransactionManager tm =
null;
-
try{
-
//獲得一個JTA事務管理器
-
tm = getTransactionManager();
-
tm.begin();
//開啟一個新的事務
-
Transaction ts1 = tm.getTransaction();
-
doSomeThing();
-
tm.suspend();
//掛起當前事務
-
try{
-
tm.begin();
//重新開啟第二個事務
-
Transaction ts2 = tm.getTransaction();
-
methodB();
-
ts2.commit();
//提交第二個事務
-
} Catch(RunTimeException ex) {
-
ts2.rollback();
//回滾第二個事務
-
}
finally {
-
//釋放資源
-
}
-
//methodB執行完后,恢復第一個事務
-
tm.resume(ts1);
-
doSomeThingB();
-
ts1.commit();
//提交第一個事務
-
}
catch(RunTimeException ex) {
-
ts1.rollback();
//回滾第一個事務
-
}
finally {
-
//釋放資源
-
}
-
}
在這里,我把ts1稱為外層事務,ts2稱為內層事務。從上面的代碼可以看出,ts2與ts1是兩個獨立的事務,互不相干。Ts2是否成功並不依賴於 ts1。如果methodA方法在調用methodB方法后的doSomeThingB方法失敗了,而methodB方法所做的結果依然被提交。而除了 methodB之外的其它代碼導致的結果卻被回滾了。使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作為事務管理器。
(5)PROPAGATION_NOT_SUPPORTED 總是非事務地執行,並掛起任何存在的事務。使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作為事務管理器。(代碼示例同上,可同理推出)
(6)PROPAGATION_NEVER 總是非事務地執行,如果存在一個活動事務,則拋出異常。
(7)PROPAGATION_NESTED如果一個活動的事務存在,則運行在一個嵌套的事務中. 如果沒有活動事務, 則按TransactionDefinition.PROPAGATION_REQUIRED 屬性執行。這是一個嵌套事務,使用JDBC 3.0驅動時,僅僅支持DataSourceTransactionManager作為事務管理器。需要JDBC 驅動的java.sql.Savepoint類。有一些JTA的事務管理器實現可能也提供了同樣的功能。使用PROPAGATION_NESTED,還需要把PlatformTransactionManager的nestedTransactionAllowed屬性設為true;而 nestedTransactionAllowed屬性值默認為false。
-
//事務屬性 PROPAGATION_REQUIRED
-
methodA(){
-
doSomeThingA();
-
methodB();
-
doSomeThingB();
-
}
-
-
//事務屬性 PROPAGATION_NESTED
-
methodB(){
-
……
-
}
如果單獨調用methodB方法,則按REQUIRED屬性執行。如果調用methodA方法,相當於下面的效果:
-
main(){
-
Connection con =
null;
-
Savepoint savepoint =
null;
-
try{
-
con = getConnection();
-
con.setAutoCommit(
false);
-
doSomeThingA();
-
savepoint = con2.setSavepoint();
-
try{
-
methodB();
-
}
catch(RuntimeException ex) {
-
con.rollback(savepoint);
-
}
finally {
-
//釋放資源
-
}
-
doSomeThingB();
-
con.commit();
-
}
catch(RuntimeException ex) {
-
con.rollback();
-
}
finally {
-
//釋放資源
-
}
-
}
當methodB方法調用之前,調用setSavepoint方法,保存當前的狀態到savepoint。如果methodB方法調用失敗,則恢復到之前保存的狀態。但是需要注意的是,這時的事務並沒有進行提交,如果后續的代碼(doSomeThingB()方法)調用失敗,則回滾包括methodB方法的所有操作。
嵌套事務一個非常重要的概念就是內層事務依賴於外層事務。外層事務失敗時,會回滾內層事務所做的動作。而內層事務操作失敗並不會引起外層事務的回滾。PROPAGATION_NESTED 與PROPAGATION_REQUIRES_NEW的區別:它們非常類似,都像一個嵌套事務,如果不存在一個活動的事務,都會開啟一個新的事務。使用 PROPAGATION_REQUIRES_NEW時,內層事務與外層事務就像兩個獨立的事務一樣,一旦內層事務進行了提交后,外層事務不能對其進行回滾。兩個事務互不影響。兩個事務不是一個真正的嵌套事務。同時它需要JTA事務管理器的支持。使用PROPAGATION_NESTED時,外層事務的回滾可以引起內層事務的回滾。而內層事務的異常並不會導致外層事務的回滾,它是一個真正的嵌套事務。DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED時,需要JDBC 3.0以上驅動及1.4以上的JDK版本支持。其它的JTA TrasactionManager實現可能有不同的支持方式。PROPAGATION_REQUIRES_NEW 啟動一個新的, 不依賴於環境的 “內部” 事務. 這個事務將被完全 commited 或 rolled back 而不依賴於外部事務, 它擁有自己的隔離范圍, 自己的鎖, 等等.當內部事務開始執行時, 外部事務將被掛起, 內務事務結束時, 外部事務將繼續執行。另一方面, PROPAGATION_NESTED 開始一個 “嵌套的” 事務, 它是已經存在事務的一個真正的子事務. 潛套事務開始執行時, 它將取得一個 savepoint. 如果這個嵌套事務失敗, 我們將回滾到此 savepoint. 潛套事務是外部事務的一部分, 只有外部事務結束后它才會被提交。由此可見, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大區別在於, PROPAGATION_REQUIRES_NEW 完全是一個新的事務, 而 PROPAGATION_NESTED 則是外部事務的子事務, 如果外部事務 commit, 嵌套事務也會被 commit, 這個規則同樣適用於 roll back.
PROPAGATION_REQUIRED應該是我們首先的事務傳播行為。它能夠滿足我們大多數的事務需求。
2.2.2 隔離級別
事務的第二個維度就是隔離級別(isolation level)。隔離級別定義了一個事務可能受其他並發事務影響的程度。
(1)並發事務引起的問題
在典型的應用程序中,多個事務並發運行,經常會操作相同的數據來完成各自的任務。並發雖然是必須的,但可能會導致一下的問題。
- 臟讀(Dirty reads)——臟讀發生在一個事務讀取了另一個事務改寫但尚未提交的數據時。如果改寫在稍后被回滾了,那么第一個事務獲取的數據就是無效的。
- 不可重復讀(Nonrepeatable read)——不可重復讀發生在一個事務執行相同的查詢兩次或兩次以上,但是每次都得到不同的數據時。這通常是因為另一個並發事務在兩次查詢期間進行了更新。
- 幻讀(Phantom read)——幻讀與不可重復讀類似。它發生在一個事務(T1)讀取了幾行數據,接着另一個並發事務(T2)插入了一些數據時。在隨后的查詢中,第一個事務(T1)就會發現多了一些原本不存在的記錄。
不可重復讀與幻讀的區別在於:
不可重復讀的重點是修改,同樣的條件, 你讀取過的數據, 再次讀取出來發現值不一樣了 .;在幻讀中,在一個事務中不同時間段查詢,記錄數不同。與不可重復讀的主要區別是:在幻讀中,已經讀取的數據不會改變,只是與以前相比,會有更多的數據滿足查詢條件。
例如:在事務1中,Mary 讀取了自己的工資為1000,操作並沒有完成
-
con1 = getConnection();
-
select salary
from employee empId =
"Mary";
在事務2中,這時財務人員修改了Mary的工資為2000,並提交了事務.
-
con2 = getConnection();
-
update employee set salary =
2000;
-
con2.commit();
在事務1中,Mary 再次讀取自己的工資時,工資變為了2000
-
//con1
-
select salary
from employee empId =
"Mary";
在一個事務中前后兩次讀取的結果並不一致,導致了不可重復讀。
幻讀的重點在於新增或者刪除:
同樣的條件, 第1次和第2次讀出來的記錄數不一樣
例如:目前工資為1000的員工有10人。事務1,讀取所有工資為1000的員工。
-
con1 = getConnection();
-
select salary
from employee empId =
"Mary";
共讀取10條記錄
這時另一個事務向employee表插入了一條員工記錄,工資也為1000
-
con2 = getConnection();
-
update employee set salary =
2000;
-
con2.commit();
事務1再次讀取所有工資為1000的員工
-
//con1
-
select salary
from employee empId =
"Mary";
共讀取到了11條記錄,這就產生了幻像讀。
從總的結果來看, 似乎不可重復讀和幻讀都表現為兩次讀取的結果不一致。但如果你從控制的角度來看, 兩者的區別就比較大。
對於前者, 只需要鎖住滿足條件的記錄。 對於后者, 要鎖住滿足條件及其相近的記錄。
(2)隔離級別
隔離級別 | 含義 |
---|---|
ISOLATION_DEFAULT | 使用后端數據庫默認的隔離級別 |
ISOLATION_READ_UNCOMMITTED | 最低的隔離級別,允許讀取尚未提交的數據變更,可能會導致臟讀、幻讀或不可重復讀 |
ISOLATION_READ_COMMITTED | 允許讀取並發事務已經提交的數據,可以阻止臟讀,但是幻讀或不可重復讀仍有可能發生 |
ISOLATION_REPEATABLE_READ | 對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止臟讀和不可重復讀,但幻讀仍有可能發生 |
ISOLATION_SERIALIZABLE | 最高的隔離級別,完全服從ACID的隔離級別,確保阻止臟讀、不可重復讀以及幻讀,也是最慢的事務隔離級別,因為它通常是通過完全鎖定事務相關的數據庫表來實現的 |
2.2.3 只讀
事務的第三個特性是它是否為只讀事務。如果事務只對后端的數據庫進行該操作,數據庫可以利用事務的只讀特性來進行一些特定的優化。通過將事務設置為只讀,你就可以給數據庫一個機會,讓它應用它認為合適的優化措施。
2.2.4 事務超時
為了使應用程序很好地運行,事務不能運行太長的時間。因為事務可能涉及對后端數據庫的鎖定,所以長時間的事務會不必要的占用數據庫資源。事務超時就是事務的一個定時器,在特定時間內事務如果沒有執行完畢,那么就會自動回滾,而不是一直等待其結束。
2.2.5 回滾規則
事務五邊形的最后一個方面是一組規則,這些規則定義了哪些異常會導致事務回滾而哪些不會。默認情況下,事務只有遇到運行期異常時才會回滾,而在遇到檢查型異常時不會回滾(這一行為與EJB的回滾行為是一致的) 。但是你可以聲明事務在遇到特定的檢查型異常時像遇到運行期異常那樣回滾。同樣,你還可以聲明事務遇到特定的異常不回滾,即使這些異常是運行期異常。
2.3 事務狀態
上面講到的調用PlatformTransactionManager接口的getTransaction()的方法得到的是TransactionStatus接口的一個實現,這個接口的內容如下:
-
public
interface TransactionStatus{
-
boolean isNewTransaction();
// 是否是新的事物
-
boolean hasSavepoint();
// 是否有恢復點
-
void setRollbackOnly();
// 設置為只回滾
-
boolean isRollbackOnly();
// 是否為只回滾
-
boolean isCompleted;
// 是否已完成
-
}
可以發現這個接口描述的是一些處理事務提供簡單的控制事務執行和查詢事務狀態的方法,在回滾或提交的時候需要應用對應的事務狀態。
原文博客地址:
http://www.mamicode.com/info-detail-1248286.html