spring事務-說說Propagation及其實現原理
前言
spring目前已是java開發的一個事實標準,這得益於它的便利、功能齊全、容易上手等特性。在開發過程當中,操作DB是非常常見的操作,而涉及到db,就會涉及到事務。事務在平時的開發過程當中,就算沒有注意到,程式正常執行不會有副作用,但如果出現了異常,而又沒有處理好事務的話,可能就會出現意想不到的結果。spring在事務方面進行了各種操作的封裝,特別是宣告式事務的出現,讓開發變得更加的舒心。spring對事務進行了擴充套件,支援定義多種傳播屬性,這也是本篇要說明的重點。
事務是什麼
非嚴格的講,一個事務是多個操作的簡稱,這些操作要麼全部生效,要麼一個都不生效(相當於沒有執行過),一個通用的操作流程簡化如下:
try{
Connection conn = getConnection();
// 執行一些資料庫操作
}catch(Exception e){
conn.rollback();
}finally{
conn.close();
}
從以上程式碼可以看出一些問題:
- 太多無用的固定程式碼
- 如果一個請求需要呼叫多個服務介面,難以更精細的控制事務
- 跨多種底層資料層,如jdbc,mybatis,hibernate,jta,難以統一編碼方式。
spring提供了宣告式事務,使得我們不用關注底層的具體實現,遮蔽了多種不同的底層實現細節,為了支援多種複雜業務對事務的精細控制,spring提供了事務的傳播屬性,結合宣告式事務,成就了一大事務利器。
spring事務傳播屬性示例分析
在TransactionDefinition類中,spring提供了6種傳播屬性,接下來分別用簡單示例來說明。
溫馨提醒:下文提到的加入當前事務,指的是底層使用同一個Connection,但是事務狀態物件是可以重新建立的,並不影響。文章提到的當前只存在一個事務,表示的是共用底層的一個Connection,而不在乎建立了多少個事務狀態物件(TransactionStatus)。
1、PROPAGATION_REQUIRED
說明: 如果當前已經存在事務,那麼加入該事務,如果不存在事務,建立一個事務,這是預設的傳播屬性值。
看一個小例子,程式碼如下:
@Transactional
public void service(){
serviceA();
serviceB();
}
@Transactional
serviceA();
@Transactional
serviceB();
serviceA 和 serviceB 都聲明瞭事務,預設情況下,propagation=PROPAGATION_REQUIRED,整個service呼叫過程中,只存在一個共享的事務,當有任何異常發生的時候,所有操作回滾。
2、PROPAGATION_SUPPORTS
說明:如果當前已經存在事務,那麼加入該事務,否則建立一個所謂的空事務(可以認為無事務執行)。
看一個小例子,程式碼如下:
public void service(){
serviceA();
throw new RunTimeException();
}
@Transactional(propagation=Propagation.SUPPORTS)
serviceA();
serviceA執行時當前沒有事務,所以service中丟擲的異常不會導致 serviceA回滾。
再看一個小例子,程式碼如下:
public void service(){
serviceA();
}
@Transactional(propagation=Propagation.SUPPORTS)
serviceA(){
do sql 1
1/0;
do sql 2
}
由於serviceA執行時沒有事務,這時候,如果底層資料來源defaultAutoCommit=true,那麼sql1是生效的,如果defaultAutoCommit=false,那麼sql1無效,如果service有@Transactional標籤,serviceA共用service的事務(不再依賴defaultAutoCommit),此時,serviceA全部被回滾。
3、 PROPAGATION_MANDATORY
說明:當前必須存在一個事務,否則丟擲異常。
看一個小例子,程式碼如下:
public void service(){
serviceB();
serviceA();
}
serviceB(){
do sql
}
@Transactional(propagation=Propagation.MANDATORY)
serviceA(){
do sql
}
這種情況執行 service會丟擲異常,如果defaultAutoCommit=true,則serviceB是不會回滾的,defaultAutoCommit=false,則serviceB執行無效。
4、PROPAGATN_REQUIRES_NEW
說明:如果當前存在事務,先把當前事務相關內容封裝到一個實體,然後重新建立一個新事務,接受這個實體為引數,用於事務的恢復。更直白的說法就是暫停當前事務(當前無事務則不需要),建立一個新事務。 針對這種情況,兩個事務沒有依賴關係,可以實現新事務回滾了,但外部事務繼續執行。
看一個小例子,程式碼如下:
@Transactional
public void service(){
serviceB();
try{
serviceA();
}catch(Exception e){
}
}
serviceB(){
do sql
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
serviceA(){
do sql 1
1/0;
do sql 2
}
當呼叫service介面時,由於serviceA使用的是REQUIRES_NEW,它會建立一個新的事務,但由於serviceA丟擲了執行時異常,導致serviceA整個被回滾了,而在service方法中,捕獲了異常,所以serviceB是正常提交的。 注意,service中的try … catch 程式碼是必須的,否則service也會丟擲異常,導致serviceB也被回滾。
5、Propagation.NOT_SUPPORTED
說明:如果當前存在事務,掛起當前事務,然後新的方法在沒有事務的環境中執行,沒有spring事務的環境下,sql的提交完全依賴於 defaultAutoCommit屬性值 。
看一個小例子,程式碼如下:
@Transactional
public void service(){
serviceB();
serviceA();
}
serviceB(){
do sql
}
@Transactional(propagation=Propagation.NOT_SUPPORTED)
serviceA(){
do sql 1
1/0;
do sql 2
}
當呼叫service方法的時候,執行到serviceA方法中的1/0程式碼時,丟擲了異常,由於serviceA處於無事務環境下,所以 sql1是否生效取決於defaultAutoCommit的值,當defaultAutoCommit=true時,sql1是生效的,但是service由於丟擲了異常,所以serviceB會被回滾。
6、 PROPAGATION_NEVER
說明: 如果當前存在事務,則丟擲異常,否則在無事務環境上執行程式碼。
看一個小例子,程式碼如下:
public void service(){
serviceB();
serviceA();
}
serviceB(){
do sql
}
@Transactional(propagation=Propagation.NEVER)
serviceA(){
do sql 1
1/0;
do sql 2
}
上面的示例呼叫service後,若defaultAutoCommit=true,則serviceB方法及serviceA中的sql1都會生效。
7、 PROPAGATION_NESTED
說明: 如果當前存在事務,則使用 SavePoint 技術把當前事務狀態進行儲存,然後底層共用一個連線,當NESTED內部出錯的時候,自行回滾到 SavePoint這個狀態,只要外部捕獲到了異常,就可以繼續進行外部的事務提交,而不會受到內嵌業務的干擾,但是,如果外部事務丟擲了異常,整個大事務都會回滾。
注意: spring配置事務管理器要主動指定 nestedTransactionAllowed=true,如下所示:
<bean id="dataTransactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataDataSource" />
<property name="nestedTransactionAllowed" value="true" />
</bean>
看一個小例子,程式碼如下:
@Transactional
public void service(){
serviceA();
try{
serviceB();
}catch(Exception e){
}
}
serviceA(){
do sql
}
@Transactional(propagation=Propagation.NESTED)
serviceB(){
do sql1
1/0;
do sql2
}
serviceB是一個內嵌的業務,內部丟擲了執行時異常,所以serviceB整個被回滾了,由於service捕獲了異常,所以serviceA是可以正常提交的。
再來看一個例子,程式碼如下:
@Transactional
public void service(){
serviceA();
serviceB();
1/0;
}
@Transactional(propagation=Propagation.NESTED)
serviceA(){
do sql
}
serviceB(){
do sql
}
由於service丟擲了異常,所以會導致整個service方法被回滾。(這就是跟PROPAGATION_REQUIRES_NEW不一樣的地方了,NESTED方式下的內嵌業務會受到外部事務的異常而回滾。)
實現淺析
前面舉例說明了spring事務提供的幾種傳播屬性,用於滿足多種不同的業務需求,大家可以依業務而定。接著我們再來看看spring實現這些傳播屬性最重要的技術依賴是什麼。本小節列舉 PROPAGATION_REQUIRES_NEW 和 Propagation.NESTED 分別進行簡要說明。
1、 PROPAGATION_REQUIRES_NEW 實現原理
如下的程式碼呼叫:
@Transactional
public void service(){
serviceB();
try{
serviceA();
}catch(Exception e){
}
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
serviceA(){
do sql 1
1/0;
do sql 2
}
serviceB(){
do sql
}
執行原理圖如下:
a. 建立事務狀態物件,獲取一個新的連線,重置連線的 autoCommit,fetchSize,timeout等屬性
b. 把連線繫結到ThreadLocal變數
c. 掛起當前事務,把當前事務狀態物件,連線等資訊封裝成一SuspendedResources物件,可用於恢復
d. 建立新的事務狀態物件,重新獲取新的連線,重置新連線的 autoCommit,fetchSize,timeout等屬性,同時,儲存SuspendedResources物件,用於事務的恢復,把新的連線繫結到ThreadLocal變數(覆蓋操作)
e. 捕獲到異常,回滾ThreadLocal中的連線,恢復連線引數,關閉連線,恢復SuspendedResources
f. 提交ThreadLocal變數中的連線(導致serviceB被提交),還原連線引數,關閉連線,連線歸還資料來源
所以程式執行的結果就是 serviceA被回滾了,serviceB成功提交了。
2、 PROPAGATION_NESTED 實現原理
如下的程式碼呼叫:
@Transactional
public void service(){
serviceA();
try{
serviceB();
}catch(Exception e){
}
}
serviceA(){
do sql
}
@Transactional(propagation=Propagation.NESTED)
serviceB(){
do sql1
1/0;
do sql2
}
執行原理圖如下:
a. 建立事務狀態物件,獲取一個新的連線,重置連線的 autoCommit,fetchSize,timeout等屬性
b. 把連線繫結到ThreadLocal變數
c. 標記使用當前事務狀態物件,獲取ThreadLocal連線物件,儲存當前連線的SavePoint,用於異常恢復,此時的SavePoint就是執行完serviceA後的狀態
d. 捕獲到異常,使用c中的SavePoint進行事務回滾,也就是把狀態回滾到執行serviceA後的狀態,serviceB方法所有執行不生效
e. 獲取ThreadLocal中的連線物件,提交事務,恢復連線屬性,關閉連線
其它
spring在底層資料來源的基礎上,利用 ThreadLocal,SavePoint等技術點實現了多種事務傳播屬性,便於實現各種複雜的業務。只有理解了傳播屬性的原理才能更好的駕馭spring事務。Spring回滾事務依賴於對異常的捕獲,預設情況下,只有丟擲RuntimeException和Error才會回滾事務,當然可以進行配置,更多資訊可以檢視 @Transactional 這個註解。
總結
spring的宣告式事務給我們帶來了極大的便利,為了用好這把利器,理解底層的原理還是有必要的,本篇只是spring事務的冰山一角,讀者可以在此基礎上自行深入探索。