基本介紹
事務是數據一致性最基本的保證,也就是說一個事務中的操作要么都成功,要么都失敗,不允許部分成功。我們常說的事務就是jdbc事務,當然Java中還有其他事務,並且在使用jdbc事務有很多注意點,請詳細了解“注意點”中的內容。但是這里有個誤區,因為我們一般是使用spring的注解@Transactional來實現事務,所以很多人會認為spring提供了事務,其實spring本身並沒有提供事務,它只是對jdbc的事務進行了封裝,然后通過AOP動態代理來實現事務的功能,這樣簡化了jdbc事務調用的相關步驟,讓我們更專注於業務功能。
注意關鍵詞“動態代理”,這意味着要生成一個代理類,那么我們就不能在一個類內直接調用事務方法,否則無法代理,而且該事務方法必須是public,如果定義成 protected、private 或者默認可見性,則無法調用!
實現原理
注意點(容易引起事務不生效)
- 使用的數據庫引擎是否支持事務
- 加事務的方法必須是public,否則事務不起作用(這一點由Spring的AOP特性決定的,理論上而言,不public也能切入,但spring可能是覺得private自己用的方法,應該自己控制,不應該用事務切進去吧)。另外private 方法, final 方法 和 static 方法不能添加事務,加了也不生效
- 處理的業務和事務的入口必須在一個線程內,否則事務不生效
- 對於jdbc事務而言,必須是一個connection中才有效的
- Spring的事務管理默認只對出現運行期異常(java.lang.RuntimeException及其子類)進行回滾(至於為什么spring要這么設計:因為spring認為Checked的異常屬於業務的,coder需要給出解決方案而不應該直接扔該框架)。如果業務需要,一定要拋出checked異常的話,可以通過rollbackFor屬性指定異常類型即可。
- 確認調用的類是否被代理了
- 在本類中調用另一個帶有事務的方法,事務時不生效的。這個時最容易犯的錯誤,不生效的原因主要是和事務的實現原理有關
- @EnableTransactionManagement // 啟注解事務管理,等同於xml配置方式的 <tx:annotation-driven />備注:本系列所有博文的討論都針對於springboot而不再對spring做說明
- 事務注解不能注到接口上,要寫到具體類上,否者不生效
類型
- JDBC事務
- JAT事務
- 容器事務
特性
- 原子性:一個事務中所有對數據庫的操作是一個不可分割的操作序列,要么全做,要么全部做。
- 一致性:數據不會因為事務的執行而遭到破壞。
- 隔離性:一個事務的執行,不受其他事務(進程)的干擾。既並發執行的個事務之間互不干擾。
- 持久性:一個事務一旦提交,它對數據庫的改變將是永久的。
7種傳播屬性
- PROPAGATION_REQUIRED -- 支持當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇。
- PROPAGATION_SUPPORTS -- 支持當前事務,如果當前沒有事務,就以非事務方式執行。
- PROPAGATION_MANDATORY -- 支持當前事務,如果當前沒有事務,就拋出異常。
- PROPAGATION_REQUIRES_NEW -- 新建事務,如果當前存在事務,把當前事務掛起。會啟動一個獨立的新事務,這個事務將被完全 commited 或 rollback 而不依賴於外部事務,它擁有自己的隔離范圍,自己的鎖等等。當內部事務開始執行時,外部事務將被掛起,內務事務結束時,外部事務將繼續執行
- PROPAGATION_NOT_SUPPORTED -- 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
- PROPAGATION_NEVER -- 以非事務方式執行,如果當前存在事務,則拋出異常。
- PROPAGATION_NESTED -- 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則進行與PROPAGATION_REQUIRED類似的操作。會開始一個 "嵌套的" 事務,它是已經存在事務的一個真正的子事務。 嵌套事務開始執行時,它將取得一個
savepoint
,如果這個嵌套事務失敗,將回滾到此savepoint
。 嵌套事務是外部事務的一部分,只有外部事務結束后它才會被提交,這個規則同樣適用於rollback。
前六個策略類似於EJB CMT,第七個(PROPAGATION_NESTED)是Spring所提供的一個特殊變量。 它要求事務管理器或者使用JDBC 3.0 Savepoint API提供嵌套事務行為(如Spring的DataSourceTransactionManager)
基本場景
圖一:事務不生效:.@Transactional的事務開啟 ,或者是基於接口的 或者是基於類的代理被創建。所以在同一個類中一個無事務的方法調用另一個有事務的方法,事務是不會起作用的(這就是業界老問題:類內部方法調用事務不生效的問題原因)。
圖二:事務生效
圖三:事務生效
圖四:事務生效
圖五:事務生效(稍微解釋一下,這里雖然是方法內部調用,但是事務切入了addInfo方法,所以即使內部拋出異常,也是可以生效的。當年我竟然驚訝,看來還是太年輕,哈哈)
圖六:事務不生效(准確的說這叫沒有事務)
圖七:如何使本類中調用方法的事務生效。
這是我們解決方法內部調用事務不生效的最常用方法之一:內部維護一個注入自己的Bean,然后使用這個屬性來調用方法。其實還有一種方法,那就是利用Aop上下文來獲取代理對象(((TestService)AopContext.currentProxy()).create(); ),然后通過代理對象來調用。這里需要注意:Aop上下文spring默認是關閉的,需要手動開啟:<aop:aspectj-autoproxy expose-proxy="true"/>
嵌套事務
七種事務傳播屬性的應用
1: REQUIRED
加入當前正要執行的事務不在另外一個事務里,那么就起一個新的事務:
比如說,DemoServiceB.demoMethodB的事務級別定義為REQUIRED, 那么由於執行DemoServiceA.demoMethodA的時候,DemoServiceA.demoMethodA已經起了事務,這時調用DemoServiceB.demoMethodB,
DemoServiceB.demoMethodB看到自己已經運行在DemoServiceA.demoMethodA的事務內部,就不再起新的事務。而假如DemoServiceA.demoMethodA運行的時候發現自己沒有在事務中,他就會為自己分配一個事務。
這樣,在DemoServiceA.demoMethodA或者在DemoServiceB.demoMethodB內的任何地方出現異常,事務都會被回滾。即使DemoServiceB.demoMethodB的事務已經被提交,但是DemoServiceA.demoMethodA在接下來fail要回滾,DemoServiceB.demoMethodB也要回滾
REQUIRED保證其處理過程同一個事務,如果調用的同一個類的配置的REQUIRED的方法,且此方法存在TRY CATCH代碼塊, 如果此代碼塊出現異常,程序可以繼續執行。這其實使因為同類調用的方法的事務根本不生效。
但如果調用的其他類的配置REQUIRED方法,且TRY CATCH住,則全部的提交全部回滾,且報出異常:Transaction rolled back because it has been marked as rollback-only因為事務報出異常后要全部回滾,包括父類的調用。
如果service中包含多個dao的方法,其都屬於同一個事務,其中報錯全部回滾,try catch住不影響程序代碼的繼續執行.
2: SUPPORTS 如果當前在事務中,即以事務的形式運行,如果當前不再一個事務中,那么就以非事務的形式運行 3: MANDATORY 必須在一個事務中運行。也就是說,他只能被一個父事務調用。否則,他就要拋出異常 4: REQUIRES_NEW 這個就比較繞口了。 比如我們設計DemoServiceA.demoMethodA的事務級別為REQUIRED,DemoServiceB.demoMethodB的事務級別為REQUIRES_NEW, 那么當執行到DemoServiceB.demoMethodB的時候,DemoServiceA.demoMethodA所在的事務就會掛起,DemoServiceB.demoMethodB會起一個新的事務,等待DemoServiceB.demoMethodB的事務完成以后, 他才繼續執行。他與REQUIRED 的事務區別在於事務的回滾程度了。因為DemoServiceB.demoMethodB是新起一個事務,那么就是存在 兩個不同的事務。如果DemoServiceB.demoMethodB已經提交,那么DemoServiceA.demoMethodA失敗回滾,DemoServiceB.demoMethodB是不會回滾的。如果DemoServiceB.demoMethodB失敗回滾, 如果他拋出的異常被DemoServiceA.demoMethodA捕獲,DemoServiceA.demoMethodA事務仍然可能提交。 5: NOT_SUPPORTED 當前不支持事務。比如DemoServiceA.demoMethodA的事務級別是REQUIRED ,而DemoServiceB.demoMethodB的事務級別是NOT_SUPPORTED , 那么當執行到DemoServiceB.demoMethodB時,DemoServiceA.demoMethodA的事務掛起,而他以非事務的狀態運行完,再繼續DemoServiceA.demoMethodA的事務。 6: NEVER 不能在事務中運行。假設DemoServiceA.demoMethodA的事務級別是REQUIRED, 而DemoServiceB.demoMethodB的事務級別是NEVER , 那么DemoServiceB.demoMethodB就要拋出異常了。 7: NESTED 理解Nested的關鍵是savepoint。他與REQUIRES_NEW的區別是,REQUIRES_NEW另起一個事務,將會與他的父事務相互獨立, 而Nested的事務和他的父事務是相依的,他的提交是要等和他的父事務一塊提交的。也就是說,如果父事務最后回滾,他也要回滾的。
例如:類serviceA和其中的方法methodA,類serviceB和其中的方法methodB,並且methodA的事務類型為REQUIRED,methodB的事務類型為NESTED.
調用代碼如下:
代碼一:
ServiceA { /** * 事務屬性配置為 PROPAGATION_REQUIRED */ void methodA() { ServiceB.methodB(); } } ServiceB { /** * 事務屬性配置為 PROPAGATION_REQUIRES_NEW */ void methodB() { } }
改成:
代碼二:
ServiceA { /** * 事務屬性配置為 PROPAGATION_REQUIRED */ void methodA() { try { ServiceB.methodB(); } catch (SomeException) { // 執行其他業務, 如 ServiceC.methodC(); } } }
這種方式也是潛套事務最有價值的地方, 它起到了分支執行的效果, 如果 ServiceB.methodB 失敗, 那么執行 ServiceC.methodC(), 而 ServiceB.methodB 已經回滾到它執行之前的 SavePoint, 所以不會產生臟數據(相當於此方法從未執行過), 這種特性可以用在某些特殊的業務中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都沒有辦法做到這一點. (題外話 : 看到這種代碼, 似乎似曾相識, 想起了 prototype.js 中的 Try 函數 )
如果代碼不做任何修改(代碼一), 那么如果內部事務(即 ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滾到它執行之前的 SavePoint(在任何情況下都會如此), 外部事務(即 ServiceA#methodA) 將根據具體的配置決定自己是 commit 還是 rollback (+MyCheckedException),也就是如果設置了rollbackfor,並且methodB拋出的異常符合rollbackfor設置的異常,那么methodA就會回滾,否者不會滾。
學習鏈接