SPRing提供了編程式事務和聲明式事務,但由于編程性事務的侵入性,開發中普遍會使用Spring的聲明式事務,下文中所說的Spring事務也都是指聲明式事務。
Spring聲明式事務底層是建立在AOP的基礎上的,其本質就是對方法前后進行攔截,然后在目標方法之前創建或加入一個事務,在執行完目標方法之后根據執行執行情況提交或回滾事務。
聲明式事務最大的優點就是不需要在業務邏輯代碼中摻雜事務管理的代碼,Spring事務提供兩種事務管理方式:基于注解和基于xml配置,我們只需要簡單的配置即可使用。
相比于編程式事務,由于聲明式事務是基于AOP實現的,所以只能實現對方法的粗粒度的控制,而無法做到代碼塊級別的細粒度控制。
Spring事務包括5中參數,分別是:傳播行為,隔離級別,是否只讀,事務超時時間,回滾規則
本文分析下5種事務參數,然后對具體的傳播行為給出Demo,以此給出其“細粒度”的事務實現。
傳播行為定義了關于客戶端和被調用方法的事務邊界,Spring中定義了7中傳播行為。
傳播行為 | 意義 |
---|---|
PROPAGATION_REQUIRED | 默認的Spring事務傳播規則;表示當前方法必須在一個事務中運行,如果一個現有事務正在運行中,該方法將在那個事務中運行,否則就會新建一個事務 |
PROPAGATION_REQUIRES_NEW | 表示當前方法必須在它自己的事務里運行。每次都會新建一個事務,如果當前有一個事務在運行的話,則將在這個方法運行期間掛起,待新事務執行完成后,上下文事務再恢復執行 |
PROPAGATION_SUPPORTS | 表示如果上下文中存在事務,就在當前事務中執行,否則使用非事務的方式執行 |
PROPAGATION_NOT_SUPPORTED | 表示該方法不應該在一個事務中運行。如果上下文中存在事務,它將在該方法執行期間被掛起 |
PROPAGATION_MANDATORY | 表示該方法必須在一個事務中運行,如果沒有事務,將拋出異常 |
PROPAGATION_NEVER | 表示該方法不能在一個事務中運行,如果有事務,則會拋出異常 |
PROPAGATION_NESTED | 表示如果當前正有一個事務在運行中,則該方法會運行在一個嵌套事務中。被嵌套的事務可以獨立于封裝事務進行提交或回滾。如果不存在事務,則新建事務。 |
對嵌套事務的理解:
嵌套是子事務在父事務中執行,子事務是父事務的一部分,在進入子事務之前,父事務建立一個回滾點,叫save point,然后執行子事務,這個子事務也算是父事務的一部分,然后子事務執行結束,父事務繼續執行。下面看幾個問題就懂了
如果子事務回滾,會發生什么?
答:父事務會回滾到save point,然后嘗試其他的事務或者其他的業務邏輯,父事務之前的操作不會受到影響,更不會自動回滾
如果父事務回滾,會發生什么?
答:父事務回滾,子事務也會跟著回滾。父事務結束之前,子事務不會提交。
事務的提交是什么情況?
答:子事務是父事務的一部分,所以子事務先提交,父事務再提交
隔離級別定義一個事務可能接受其他并發事務活動受影響的程度。也可以想象成事務對于數據處理的自私程度。
多個事務同時運行,經常會為了完成他們的工作而操作同一個數據。并發雖然是必需的,但是會導致以下問題:
脹讀——A事務讀取數據并修改,未提交之間B事務又讀取了數據不可重復讀——A事務讀取數據,B事務讀取數據并修改,A事務再讀取數據時發現兩次數據不一致幻讀——事務A讀取或刪除了全部數據,事務B又insert一條數據,這時事務A發現莫名其妙的多了一條數據理想狀態下,事務之間是完全隔離的。但是完全隔離會影響性能,因為隔離的實現依賴于數據庫中的鎖,侵占性鎖會阻礙并發,要求事務互相等待
考慮到完全隔離會影響性能,而且并不是所有的情況都要求完全隔離,所以有時候可以在事務隔離方面靈活處理。因此,就有好幾個隔離級別。
隔離級別 | 含義 |
---|---|
ISOLATION_DEFAULT | 使用后端數據庫默認的隔離級別 |
ISOLATION_READ_UNCOMMITTED | 允許讀取未提交的更改。 可能導致臟讀、幻讀或不可重復讀。 |
ISOLATION_READ_COMMITTED | 允許從已經提交的并發事務讀取??煞乐古K讀,但幻讀和不可重復讀仍可能會發生。 |
ISOLATION_REPEATABLE_READ | 對相同字段的多次讀取的結果是一致的,除非數據被當前事務本身改變。可防止臟讀和不可重復讀,但幻影讀仍可能發生。 |
ISOLATION_SERIALIZABLE | 完全服從ACID的隔離級別,確保不發生臟讀、不可重復讀和幻影讀。這在所有隔離級別中也是最慢的,因為它通常是通過完全鎖定當前事務所涉及的數據表來完成的。 |
如果一個事務只對后端數據庫執行讀操作,那么該數據庫就可能利用那個事務的只讀特性,采取某些優化措施。
由于只讀的優化措施是在一個事務啟動時由后端數據庫實施的,因此,只有對于那些具有可能啟動一個新事物的傳播行為(PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED)的方法來說,將事務聲明為只讀才有意義
假設事務的運行時間變得格外的長,由于事務可能涉及對后端數據庫的鎖定,所以長時間運行的事務會不必要地占用數據庫資源,這時就可以聲明一個事務在特定時間后自動回滾
由于超時時鐘在一個事務啟動的時候開始的,因此,有對于那些具有可能啟動一個新事物的傳播行為(PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED)的方法來說,聲明事務超時才有意義,默認為30S
回滾規則定義了哪些異常引起回滾,哪些不引起。在默認情況下,事務只出現運行時異常(Runtime Exception)時回滾,而在出現受檢查異常(Checked Exception)時不回滾。
在本節中,我們只會考慮事務的傳播規則,不會考慮到其隔離級別等其他參數。測試Service為TestServiceA和TestServiceB
TestSeviceA代碼為:
@Autowiredprivate TransactionDAO transactionDAO;@Autowiredprivate TestServiceB testService;// 會開啟事務,在事務范圍內使用同一個事務,否則開啟新事務@Transactional(propagation = Propagation.REQUIRES_NEW)public void addOrder() { transactionDAO.insertOrder(); testService.addVoucher(); throw new RuntimeException();}TestServiceB代碼為:
@Autowired private TransactionDAO transactionDAO; @Transactional(propagation = Propagation.REQUIRES_NEW) public void addVoucher() { transactionDAO.insertOrderVoucher();// throw new RuntimeException(); }經測試,無論是在addOrder還是addVoucher中拋出異常,數據都會提交失??;當且僅當兩個方法都成功時,數據提交成功。
這說明這兩個Service使用的是同一個事務,并且只要方法被調用就會開啟事務。
注意:一旦有前置方法開啟了Spring事務,在調用鏈中的方法即使沒有聲明事務,也都會加入到事務中。
如果在ServiceA中使用了try catch語句后會是什么情況呢?即代碼如下:
TestSeviceA代碼為:
@Autowiredprivate TransactionDAO transactionDAO;@Autowiredprivate TestServiceB testService;// 會開啟事務,在事務范圍內使用同一個事務,否則開啟新事務@Transactional(propagation = Propagation.REQUIRED)public void addOrder() { transactionDAO.insertOrder(); try { testService.addVoucher(); } catch (Exception e) { }}TestServiceB代碼為:
@Autowired private TransactionDAO transactionDAO; @Transactional(propagation = Propagation.REQUIRED) public void addVoucher() { transactionDAO.insertOrderVoucher(); throw new RuntimeException(); }在ServiceB中拋出RuntimeException異常,但是在ServiceA中使用try catch捕獲該異常。
執行后發現數據沒有提交,但是出現了一個異常:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
原因:在addVoucher()方法return時會將Transactional標記為Rollback only, 而addOrder()方法捕獲了bar的RuntimeException,所以Spring將會commit addOrder()的事務,但是這兩個方法使用的是同一事務,因此在commit addOrder()事務時,將會拋出UnexpectedRollbackException。
TestServiceA代碼為:
@Autowiredprivate TransactionDAO transactionDAO;@Autowiredprivate TestServiceB testService;// 無論何時都會開啟事務@Transactional(propagation = Propagation.REQUIRES_NEW)public void addOrder() { transactionDAO.insertOrder(); testService.addVoucher(); throw new RuntimeException();}TestServiceB代碼為:
@Autowired private TransactionDAO transactionDAO; @Transactional(propagation = Propagation.REQUIRES_NEW) public void addVoucher() { transactionDAO.insertOrderVoucher();// throw new RuntimeException(); }經測試如果在addOrder中拋出異常,order數據不能提交,voucher數據被正確提交。相反addVoucher中拋出異常,就只能提交order數據。
說明兩個Service是在兩個獨立是事務中運行,并且只要方法被調用就開啟事務。
TestServiceA代碼為:
@Autowiredprivate TransactionDAO transactionDAO;@Autowiredprivate TestServiceB testService;// 自身不會開啟事務,在事務范圍內則使用相同事務,否則不使用事務@Transactional(propagation = Propagation.SUPPORTS)public void addOrder() { transactionDAO.insertOrder(); testService.addVoucher(); throw new RuntimeException();}TestServiceB代碼為:
@Autowired private TransactionDAO transactionDAO; @Transactional(propagation = Propagation.SUPPORTS) public void addVoucher() { transactionDAO.insertOrderVoucher();// throw new RuntimeException(); }由于兩個方法都被申明為SUPPORTS的傳播規則,所以即使拋出異常,數據都也都能被正確提交。說明兩個方法并沒有使用Spring事務,而是使用本地事務,需要注意的是,這兩個方法的本地事務并不是同一個事務,一旦出現異常將導致數據不一致。
我們來考慮一下更多的組合情況:
- REQUIRED → SUPPORTS 這時開啟事務,并使用同一個事務- REQUIRES_NEW → SUPPORTS 同a- SUPPORTS → REQUIRED ServiceA由本地事務管理,ServiceB由Spring事務管理- SUPPORTS → REQUIRES_NEW 同cTestServiceA代碼為:
@Autowiredprivate TransactionDAO transactionDAO;@Autowiredprivate TestServiceB testService;// 自身不會開啟事務,在事務范圍內使用會掛起事務執行,運行完畢恢復事務@Transactional(propagation = Propagation.REQUIRED)public void addOrder() { transactionDAO.insertOrder(); testService.addVoucher();}TestServiceB代碼為:
@Autowired private TransactionDAO transactionDAO; @Transactional(propagation = Propagation.SUPPORTS) public void addVoucher() { transactionDAO.insertOrderVoucher(); throw new RuntimeException(); }經測試如果在addVoucher中拋出異常,addOrder數據提交失敗,addVoucher數據會提交成功。說明ServiceA開了事務,ServiceB沒有開啟事務,而是使用了本地事務。
TestServiceA代碼
@Autowiredprivate TransactionDAO transactionDAO;@Autowiredprivate TestServiceB testService;// 自身不會開啟事務,必須在事務環境使用,否則報錯@Transactional(propagation = Propagation.MANDATORY)public void addOrder() { transactionDAO.insertOrder(); testService.addVoucher();}經測試以上代碼報錯
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation ‘mandatory’ 沒有找到事務環境
即該注解必須要和能創建事務的注解使用(PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED)
TestServiceA代碼為:
@Autowiredprivate TransactionDAO transactionDAO;@Autowiredprivate TestServiceB testService;@Transactional(propagation = Propagation.REQUIRED)public void addOrder() { transactionDAO.insertOrder(); testService.addVoucher();}TestServiceB代碼為:
@Autowired private TransactionDAO transactionDAO; // 自身不會開啟事務,在事務范圍內使用拋出異常 @Transactional(propagation = Propagation.NEVER) public void addVoucher() { transactionDAO.insertOrderVoucher(); throw new RuntimeException(); }經測試代碼拋出異常
org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation ‘never’ 存在事務環境
TestServiceA代碼為:
@Autowiredprivate TransactionDAO transactionDAO;@Autowiredprivate TestServiceB testService;// 新開啟一個事務 @Transactional(propagation = Propagation.REQUIRED) public void addOrder() { transactionDAO.insertOrder(); try { testService.addVoucher(); } catch (Exception e) { }// throw new RuntimeException(); }TestServiceB代碼為:
@Autowired private TransactionDAO transactionDAO; // 啟用嵌套事務 @Transactional(propagation = Propagation.NESTED) public void addVoucher() { transactionDAO.insertOrderVoucher(); throw new RuntimeException(); }經測試,在ServiceB中拋出異常后,addVoucher數據提交失敗,addOrder數據提交成功。
如果在ServiceA中拋出異常,那么數據都會提交失敗。
https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/ https://my.oschina.net/dongli/blog/56904 http://blog.csdn.net/aya19880214/article/details/50640596
新聞熱點
疑難解答