javascript
太难了~面试官让我结合案例讲讲自己对Spring事务传播行为的理解!
摘要: 原創出處 sf.gg/a/1190000013341344 「handaqiang」歡迎轉載,保留摘要,謝謝!
-
前言
-
基礎概念
-
1. 什么是事務傳播行為?
-
2. Spring 中七種事務傳播行為
-
-
代碼驗證
-
1.PROPAGATION_REQUIRED
-
2.PROPAGATION_REQUIRES_NEW
-
3.PROPAGATION_NESTED
-
4. REQUIRED,REQUIRES_NEW,NESTED 異同
-
5. 其他事務傳播行為
-
-
模擬用例
-
結論
前言
Spring 在 TransactionDefinition 接口中規定了 7 種類型的事務傳播行為。事務傳播行為是 Spring 框架獨有的事務增強特性,他不屬于的事務實際提供方數據庫行為。這是 Spring 為我們提供的強大的工具箱,使用事務傳播行可以為我們的開發工作提供許多便利。但是人們對他的誤解也頗多,你一定也聽過“service 方法事務最好不要嵌套”的傳言。要想正確的使用工具首先需要了解工具。本文對七種事務傳播行為做詳細介紹,內容主要代碼示例的方式呈現。
基礎概念
1. 什么是事務傳播行為?
事務傳播行為用來描述由某一個事務傳播行為修飾的方法被嵌套進另一個方法的時事務如何傳播。
用偽代碼說明:
?public?void?methodA(){methodB();//doSomething}@Transaction(Propagation=XXX)public?void?methodB(){//doSomething}代碼中methodA()方法嵌套調用了methodB()方法,methodB()的事務傳播行為由@Transaction(Propagation=XXX)設置決定。這里需要注意的是methodA()并沒有開啟事務,某一個事務傳播行為修飾的方法并不是必須要在開啟事務的外圍方法中調用。
2. Spring 中七種事務傳播行為
定義非常簡單,也很好理解,下面我們就進入代碼測試部分,驗證我們的理解是否正確。
代碼驗證
文中代碼以傳統三層結構中兩層呈現,即 Service 和 Dao 層,由 Spring 負責依賴注入和注解式事務管理,DAO 層由 Mybatis 實現,你也可以使用任何喜歡的方式,例如,Hibernate,JPA,JDBCTemplate 等。數據庫使用的是 MySQL 數據庫,你也可以使用任何支持事務的數據庫,并不會影響驗證結果。
首先我們在數據庫中創建兩張表:
user1
CREATE?TABLE?`user1`?(`id`?INTEGER?UNSIGNED?NOT?NULL?AUTO_INCREMENT,`name`?VARCHAR(45)?NOT?NULL?DEFAULT?'',PRIMARY?KEY(`id`) ) ENGINE?=?InnoDB;user2
CREATE?TABLE?`user2`?(`id`?INTEGER?UNSIGNED?NOT?NULL?AUTO_INCREMENT,`name`?VARCHAR(45)?NOT?NULL?DEFAULT?'',PRIMARY?KEY(`id`) ) ENGINE?=?InnoDB;然后編寫相應的 Bean 和 DAO 層代碼:
User1
public?class?User1?{private?Integer?id;private?String?name;//get和set方法省略... }User2
public?class?User2?{private?Integer?id;private?String?name;//get和set方法省略... }User1Mapper
public?interface?User1Mapper?{int?insert(User1?record);User1?selectByPrimaryKey(Integer?id);//其他方法省略... }User2Mapper
public?interface?User2Mapper?{int?insert(User2?record);User2?selectByPrimaryKey(Integer?id);//其他方法省略... }最后也是具體驗證的代碼由 service 層實現,下面我們分情況列舉。
1.PROPAGATION_REQUIRED
我們為 User1Service 和 User2Service 相應方法加上Propagation.REQUIRED屬性。
User1Service 方法:
@Service public?class?User1ServiceImpl?implements?User1Service?{//省略其他...@Override@Transactional(propagation?=?Propagation.REQUIRED)public?void?addRequired(User1?user){user1Mapper.insert(user);} }User2Service 方法:
@Service public?class?User2ServiceImpl?implements?User2Service?{//省略其他...@Override@Transactional(propagation?=?Propagation.REQUIRED)public?void?addRequired(User2?user){user2Mapper.insert(user);}@Override@Transactional(propagation?=?Propagation.REQUIRED)public?void?addRequiredException(User2?user){user2Mapper.insert(user);throw?new?RuntimeException();}}1.1 場景一
此場景外圍方法沒有開啟事務。
驗證方法 1:
????@Overridepublic?void?notransaction_exception_required_required(){User1?user1=new?User1();user1.setName("張三");user1Service.addRequired(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addRequired(user2);throw?new?RuntimeException();}驗證方法 2:
????@Overridepublic?void?notransaction_required_required_exception(){User1?user1=new?User1();user1.setName("張三");user1Service.addRequired(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addRequiredException(user2);}分別執行驗證方法,結果:
結論:通過這兩個方法我們證明了在外圍方法未開啟事務的情況下Propagation.REQUIRED修飾的內部方法會新開啟自己的事務,且開啟的事務相互獨立,互不干擾。
1.2 場景二
外圍方法開啟事務,這個是使用率比較高的場景。
驗證方法 1:
??? @Override@Transactional(propagation?=?Propagation.REQUIRED)public?void?transaction_exception_required_required(){User1?user1=new?User1();user1.setName("張三");user1Service.addRequired(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addRequired(user2);throw?new?RuntimeException();}驗證方法 2:
????@Override@Transactional(propagation?=?Propagation.REQUIRED)public?void?transaction_required_required_exception(){User1?user1=new?User1();user1.setName("張三");user1Service.addRequired(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addRequiredException(user2);}驗證方法 3:
????@Transactional@Overridepublic?void?transaction_required_required_exception_try(){User1?user1=new?User1();user1.setName("張三");user1Service.addRequired(user1);User2?user2=new?User2();user2.setName("李四");try?{user2Service.addRequiredException(user2);}?catch?(Exception?e)?{System.out.println("方法回滾");}}分別執行驗證方法,結果:
結論:以上試驗結果我們證明在外圍方法開啟事務的情況下Propagation.REQUIRED修飾的內部方法會加入到外圍方法的事務中,所有Propagation.REQUIRED修飾的內部方法和外圍方法均屬于同一事務,只要一個方法回滾,整個事務均回滾。
2.PROPAGATION_REQUIRES_NEW
我們為 User1Service 和 User2Service 相應方法加上Propagation.REQUIRES_NEW屬性。User1Service 方法:
@Service public?class?User1ServiceImpl?implements?User1Service?{//省略其他...@Override@Transactional(propagation?=?Propagation.REQUIRES_NEW)public?void?addRequiresNew(User1?user){user1Mapper.insert(user);}@Override@Transactional(propagation?=?Propagation.REQUIRED)public?void?addRequired(User1?user){user1Mapper.insert(user);} }User2Service 方法:
@Service public?class?User2ServiceImpl?implements?User2Service?{//省略其他...@Override@Transactional(propagation?=?Propagation.REQUIRES_NEW)public?void?addRequiresNew(User2?user){user2Mapper.insert(user);}@Override@Transactional(propagation?=?Propagation.REQUIRES_NEW)public?void?addRequiresNewException(User2?user){user2Mapper.insert(user);throw?new?RuntimeException();} }2.1 場景一
外圍方法沒有開啟事務。
驗證方法 1:
????@Overridepublic?void?notransaction_exception_requiresNew_requiresNew(){User1?user1=new?User1();user1.setName("張三");user1Service.addRequiresNew(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addRequiresNew(user2);throw?new?RuntimeException();}驗證方法 2:
????@Overridepublic?void?notransaction_requiresNew_requiresNew_exception(){User1?user1=new?User1();user1.setName("張三");user1Service.addRequiresNew(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addRequiresNewException(user2);}分別執行驗證方法,結果:
?
結論:通過這兩個方法我們證明了在外圍方法未開啟事務的情況下Propagation.REQUIRES_NEW修飾的內部方法會新開啟自己的事務,且開啟的事務相互獨立,互不干擾。
2.2 場景二
外圍方法開啟事務。
驗證方法 1:
????@Override@Transactional(propagation?=?Propagation.REQUIRED)public?void?transaction_exception_required_requiresNew_requiresNew(){User1?user1=new?User1();user1.setName("張三");user1Service.addRequired(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addRequiresNew(user2);User2?user3=new?User2();user3.setName("王五");user2Service.addRequiresNew(user3);throw?new?RuntimeException();}驗證方法 2:
????@Override@Transactional(propagation?=?Propagation.REQUIRED)public?void?transaction_required_requiresNew_requiresNew_exception(){User1?user1=new?User1();user1.setName("張三");user1Service.addRequired(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addRequiresNew(user2);User2?user3=new?User2();user3.setName("王五");user2Service.addRequiresNewException(user3);}驗證方法 3:
????@Override@Transactional(propagation?=?Propagation.REQUIRED)public?void?transaction_required_requiresNew_requiresNew_exception_try(){User1?user1=new?User1();user1.setName("張三");user1Service.addRequired(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addRequiresNew(user2);User2?user3=new?User2();user3.setName("王五");try?{user2Service.addRequiresNewException(user3);}?catch?(Exception?e)?{System.out.println("回滾");}}分別執行驗證方法,結果:
結論:在外圍方法開啟事務的情況下Propagation.REQUIRES_NEW修飾的內部方法依然會單獨開啟獨立事務,且與外部方法事務也獨立,內部方法之間、內部方法和外部方法事務均相互獨立,互不干擾。
3.PROPAGATION_NESTED
我們為 User1Service 和 User2Service 相應方法加上Propagation.NESTED屬性。User1Service 方法:
@Service public?class?User1ServiceImpl?implements?User1Service?{//省略其他...@Override@Transactional(propagation?=?Propagation.NESTED)public?void?addNested(User1?user){user1Mapper.insert(user);} }User2Service 方法:
@Service public?class?User2ServiceImpl?implements?User2Service?{//省略其他...@Override@Transactional(propagation?=?Propagation.NESTED)public?void?addNested(User2?user){user2Mapper.insert(user);}@Override@Transactional(propagation?=?Propagation.NESTED)public?void?addNestedException(User2?user){user2Mapper.insert(user);throw?new?RuntimeException();} }3.1 場景一
此場景外圍方法沒有開啟事務。
驗證方法 1:
????@Overridepublic?void?notransaction_exception_nested_nested(){User1?user1=new?User1();user1.setName("張三");user1Service.addNested(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addNested(user2);throw?new?RuntimeException();}驗證方法 2:
????@Overridepublic?void?notransaction_nested_nested_exception(){User1?user1=new?User1();user1.setName("張三");user1Service.addNested(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addNestedException(user2);}分別執行驗證方法,結果:
結論:通過這兩個方法我們證明了在外圍方法未開啟事務的情況下Propagation.NESTED和Propagation.REQUIRED作用相同,修飾的內部方法都會新開啟自己的事務,且開啟的事務相互獨立,互不干擾。
3.2 場景二
外圍方法開啟事務。
驗證方法 1:
????@Transactional@Overridepublic?void?transaction_exception_nested_nested(){User1?user1=new?User1();user1.setName("張三");user1Service.addNested(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addNested(user2);throw?new?RuntimeException();}驗證方法 2:
????@Transactional@Overridepublic?void?transaction_nested_nested_exception(){User1?user1=new?User1();user1.setName("張三");user1Service.addNested(user1);User2?user2=new?User2();user2.setName("李四");user2Service.addNestedException(user2);}驗證方法 3:
????@Transactional@Overridepublic?void?transaction_nested_nested_exception_try(){User1?user1=new?User1();user1.setName("張三");user1Service.addNested(user1);User2?user2=new?User2();user2.setName("李四");try?{user2Service.addNestedException(user2);}?catch?(Exception?e)?{System.out.println("方法回滾");}}分別執行驗證方法,結果:
結論:以上試驗結果我們證明在外圍方法開啟事務的情況下Propagation.NESTED修飾的內部方法屬于外部事務的子事務,外圍主事務回滾,子事務一定回滾,而內部子事務可以單獨回滾而不影響外圍主事務和其他子事務
4. REQUIRED,REQUIRES_NEW,NESTED 異同
由“1.2 場景二”和“3.2 場景二”對比,我們可知:NESTED 和 REQUIRED 修飾的內部方法都屬于外圍方法事務,如果外圍方法拋出異常,這兩種方法的事務都會被回滾。但是 REQUIRED 是加入外圍方法事務,所以和外圍事務同屬于一個事務,一旦 REQUIRED 事務拋出異常被回滾,外圍方法事務也將被回滾。而 NESTED 是外圍方法的子事務,有單獨的保存點,所以 NESTED 方法拋出異常被回滾,不會影響到外圍方法的事務。
由“2.2 場景二”和“3.2 場景二”對比,我們可知:NESTED 和 REQUIRES_NEW 都可以做到內部方法事務回滾而不影響外圍方法事務。但是因為 NESTED 是嵌套事務,所以外圍方法回滾之后,作為外圍方法事務的子事務也會被回滾。而 REQUIRES_NEW 是通過開啟新的事務實現的,內部事務和外圍事務是兩個事務,外圍事務回滾不會影響內部事務。
5. 其他事務傳播行為
鑒于文章篇幅問題,其他事務傳播行為的測試就不在此一一描述了,感興趣的讀者可以去源碼中自己尋找相應測試代碼和結果解釋。傳送門:https://github.com/TmTse/transaction-test
模擬用例
介紹了這么多事務傳播行為,我們在實際工作中如何應用呢?下面我來舉一個示例:
假設我們有一個注冊的方法,方法中調用添加積分的方法,如果我們希望添加積分不會影響注冊流程(即添加積分執行失敗回滾不能使注冊方法也回滾),我們會這樣寫:
???@Servicepublic?class?UserServiceImpl?implements?UserService?{@Transactionalpublic?void?register(User?user){try?{membershipPointService.addPoint(Point?point);}?catch?(Exception?e)?{//省略...}//省略...}//省略...}我們還規定注冊失敗要影響addPoint()方法(注冊方法回滾添加積分方法也需要回滾),那么addPoint()方法就需要這樣實現:
???@Servicepublic?class?MembershipPointServiceImpl?implements?MembershipPointService{@Transactional(propagation?=?Propagation.NESTED)public?void?addPoint(Point?point){try?{recordService.addRecord(Record?record);}?catch?(Exception?e)?{//省略...}//省略...}//省略...}我們注意到了在addPoint()中還調用了addRecord()方法,這個方法用來記錄日志。他的實現如下:
???@Servicepublic?class?RecordServiceImpl?implements?RecordService{@Transactional(propagation?=?Propagation.NOT_SUPPORTED)public?void?addRecord(Record?record){//省略...}//省略...}我們注意到addRecord()方法中propagation = Propagation.NOT_SUPPORTED,因為對于日志無所謂精確,可以多一條也可以少一條,所以addRecord()方法本身和外圍addPoint()方法拋出異常都不會使addRecord()方法回滾,并且addRecord()方法拋出異常也不會影響外圍addPoint()方法的執行。
通過這個例子相信大家對事務傳播行為的使用有了更加直觀的認識,通過各種屬性的組合確實能讓我們的業務實現更加靈活多樣。
結論
通過上面的介紹,相信大家對 Spring 事務傳播行為有了更加深入的理解,希望大家日常開發工作有所幫助。
總結
以上是生活随笔為你收集整理的太难了~面试官让我结合案例讲讲自己对Spring事务传播行为的理解!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里面试题:如何检测并避免 Java 中
- 下一篇: 重构,还是重写?(2020版)