8個(gè)Spring事務(wù)失效場(chǎng)景詳解
前言
作為Java開發(fā)工程師,相信大家對(duì)Spring
種事務(wù)的使用并不陌生。但是你可能只是停留在基礎(chǔ)的使用層面上,在遇到一些比較特殊的場(chǎng)景,事務(wù)可能沒有生效,直接在生產(chǎn)上暴露了,這可能就會(huì)導(dǎo)致比較嚴(yán)重的生產(chǎn)事故。今天,我們就簡(jiǎn)單來說下Spring事務(wù)的原理,然后總結(jié)一下spring事務(wù)失敗的場(chǎng)景,并提出對(duì)應(yīng)的解決方案。
Spring事務(wù)原理
大家還記得在JDBC中是如何操作事務(wù)的嗎?偽代碼可能如下:
//Get database connection Connection connection = DriverManager.getConnection(); //Set autoCommit is false connection.setAutoCommit(false); //use sql to operate database ......... //Commit or rollback connection.commit()/connection.rollback connection.close();
需要在各個(gè)業(yè)務(wù)代碼中編寫代碼如commit()
、close()
來控制事務(wù)。
但是Spring不樂意這么干了,這樣對(duì)業(yè)務(wù)代碼侵入性太大了,所有就用一個(gè)事務(wù)注解@Transactional
來控制事務(wù),底層實(shí)現(xiàn)是基于切面編程AOP
實(shí)現(xiàn)的,而Spring
中實(shí)現(xiàn)AOP
機(jī)制采用的是動(dòng)態(tài)代理,具體分為JDK
動(dòng)態(tài)代理和CGLIB
動(dòng)態(tài)代理兩種模式。
Spring
的bean
的初始化過程中,發(fā)現(xiàn)方法有Transactional
注解,就需要對(duì)相應(yīng)的Bean
進(jìn)行代理,生成代理對(duì)象。- 然后在方法調(diào)用的時(shí)候,會(huì)執(zhí)行切面的邏輯,而這里切面的邏輯中就包含了開啟事務(wù)、提交事務(wù)或者回滾事務(wù)等邏輯。
另外注意一點(diǎn)的是,Spring
本身不實(shí)現(xiàn)事務(wù),底層還是依賴于數(shù)據(jù)庫(kù)的事務(wù)。沒有數(shù)據(jù)庫(kù)事務(wù)的支持,Spring
事務(wù)是不會(huì)生效的。
接下來我們進(jìn)入正題,看看哪些場(chǎng)景會(huì)導(dǎo)致Spring
事務(wù)失敗。
Spring事務(wù)失效場(chǎng)景
1. 拋出檢查異常
比如你的事務(wù)控制代碼如下:
@Transactional public void transactionTest() throws IOException{ User user = new User(); UserService.insert(user); throw new IOException(); }
如果@Transactional
沒有特別指定,Spring 只會(huì)在遇到運(yùn)行時(shí)異常RuntimeException或者error時(shí)進(jìn)行回滾,而IOException
等檢查異常不會(huì)影響回滾。
public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); }
解決方案:
知道原因后,解決方法也很簡(jiǎn)單。配置rollbackFor
屬性,例如@Transactional(rollbackFor = Exception.class)
。
2. 業(yè)務(wù)方法本身捕獲了異常
@Transactional(rollbackFor = Exception.class) public void transactionTest() { try { User user = new User(); UserService.insert(user); int i = 1 / 0; }catch (Exception e) { e.printStackTrace(); } }
這種場(chǎng)景下,事務(wù)失敗的原因也很簡(jiǎn)單,Spring
是否進(jìn)行回滾是根據(jù)你是否拋出異常決定的,所以如果你自己捕獲了異常,Spring
也無能為力。
看了上面的代碼,你可能認(rèn)為這么簡(jiǎn)單的問題你不可能犯這么愚蠢的錯(cuò)誤,但是我想告訴你的是,我身邊幾乎一半的人都被這一幕困擾過。
寫業(yè)務(wù)代碼的時(shí)候,代碼可能比較復(fù)雜,嵌套的方法很多。如果你不小心,很可能會(huì)觸發(fā)此問題。舉一個(gè)非常簡(jiǎn)單的例子,假設(shè)你有一個(gè)審計(jì)功能。每個(gè)方法執(zhí)行后,審計(jì)結(jié)果保存在數(shù)據(jù)庫(kù)中,那么代碼可能會(huì)這樣寫。
@Service public class TransactionService { @Transactional(rollbackFor = Exception.class) public void transactionTest() throws IOException { User user = new User(); UserService.insert(user); throw new IOException(); } } @Component public class AuditAspect { @Autowired private auditService auditService; @Around(value = "execution (* com.alvin.*.*(..))") public Object around(ProceedingJoinPoint pjp) { try { Audit audit = new Audit(); Signature signature = pjp.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; String[] strings = methodSignature.getParameterNames(); audit.setMethod(signature.getName()); audit.setParameters(strings); Object proceed = pjp.proceed(); audit.success(true); return proceed; } catch (Throwable e) { log.error("{}", e); audit.success(false); } auditService.save(audit); return null; } }
在上面的示例中,事務(wù)將失敗。原因是Spring
的事務(wù)切面優(yōu)先級(jí)最低,所以如果異常被切面捕獲,Spring自然不能正常處理事務(wù),因?yàn)槭聞?wù)管理器無法捕獲異常。
解決方案:
看,雖然我們知道在處理事務(wù)時(shí)業(yè)務(wù)代碼不能自己捕獲異常,但是只要代碼變得復(fù)雜,我們就很可能再次出錯(cuò),所以我們?cè)谔幚硎聞?wù)的時(shí)候要小心,還是不要使用聲明式事務(wù), 并使用編程式事務(wù)— transactionTemplate.execute()
。
3. 同一類中的方法調(diào)用
@Service public class DefaultTransactionService implement Service { public void saveUser() throws Exception { //do something doInsert(); } @Transactional(rollbackFor = Exception.class) public void doInsert() throws IOException { User user = new User(); UserService.insert(user); throw new IOException(); } }
這也是一個(gè)容易出錯(cuò)的場(chǎng)景。事務(wù)失敗的原因也很簡(jiǎn)單,因?yàn)?code>Spring的事務(wù)管理功能是通過動(dòng)態(tài)代理實(shí)現(xiàn)的,而Spring
默認(rèn)使用JDK
動(dòng)態(tài)代理,而JDK
動(dòng)態(tài)代理采用接口實(shí)現(xiàn)的方式,通過反射調(diào)用目標(biāo)類。簡(jiǎn)單理解,就是saveUser()
方法中調(diào)用this.doInsert()
,這里的this
是被真實(shí)對(duì)象,所以會(huì)直接走doInsert
的業(yè)務(wù)邏輯,而不會(huì)走切面邏輯,所以事務(wù)失敗。
解決方案:
方案一:解決方法可以是直接在啟動(dòng)類中添加@Transactional注解saveUser()
方案二:@EnableAspectJAutoProxy(exposeProxy = true)
在啟動(dòng)類中添加,會(huì)由Cglib
代理實(shí)現(xiàn)。
4. 方法使用 final 或 static關(guān)鍵字
如果Spring
使用了Cglib
代理實(shí)現(xiàn)(比如你的代理類沒有實(shí)現(xiàn)接口),而你的業(yè)務(wù)方法恰好使用了final
或者static
關(guān)鍵字,那么事務(wù)也會(huì)失敗。更具體地說,它應(yīng)該拋出異常,因?yàn)?code>Cglib使用字節(jié)碼增強(qiáng)技術(shù)生成被代理類的子類并重寫被代理類的方法來實(shí)現(xiàn)代理。如果被代理的方法的方法使用final
或static
關(guān)鍵字,則子類不能重寫被代理的方法。
如果Spring
使用JDK
動(dòng)態(tài)代理實(shí)現(xiàn),JDK
動(dòng)態(tài)代理是基于接口實(shí)現(xiàn)的,那么final
和static
修飾的方法也就無法被代理。
總而言之,方法連代理都沒有,那么肯定無法實(shí)現(xiàn)事務(wù)回滾了。
解決方案:
想辦法去掉final或者static關(guān)鍵字
5. 方法不是public
如果方法不是public
,Spring
事務(wù)也會(huì)失敗,因?yàn)?code>Spring的事務(wù)管理源碼AbstractFallbackTransactionAttributeSource
中有判斷computeTransactionAttribute()。
如果目標(biāo)方法不是公共的,則TransactionAttribute
返回null
。
// Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; }
解決方案:
是將當(dāng)前方法訪問級(jí)別更改為public
。
6. 錯(cuò)誤使用傳播機(jī)制
Spring
事務(wù)的傳播機(jī)制是指在多個(gè)事務(wù)方法相互調(diào)用時(shí),確定事務(wù)應(yīng)該如何傳播的策略。Spring
提供了七種事務(wù)傳播機(jī)制:REQUIRED
、SUPPORTS
、MANDATORY
、REQUIRES_NEW
、NOT_SUPPORTED
、NEVER
、NESTED
。如果不知道這些傳播策略的原理,很可能會(huì)導(dǎo)致交易失敗。
@Service public class TransactionService { @Autowired private UserMapper userMapper; @Autowired private AddressMapper addressMapper; @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class) public void doInsert(User user,Address address) throws Exception { //do something userMapper.insert(user); saveAddress(address); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveAddress(Address address) { //do something addressMapper.insert(address); } }
在上面的例子中,如果用戶插入失敗,不會(huì)導(dǎo)致saveAddress()
回滾,因?yàn)檫@里使用的傳播是REQUIRES_NEW
,傳播機(jī)制REQUIRES_NEW
的原理是如果當(dāng)前方法中沒有事務(wù),就會(huì)創(chuàng)建一個(gè)新的事務(wù)。如果一個(gè)事務(wù)已經(jīng)存在,則當(dāng)前事務(wù)將被掛起,并創(chuàng)建一個(gè)新事務(wù)。在當(dāng)前事務(wù)完成之前,不會(huì)提交父事務(wù)。如果父事務(wù)發(fā)生異常,則不影響子事務(wù)的提交。
事務(wù)的傳播機(jī)制說明如下:
REQUIRED
如果當(dāng)前上下文中存在事務(wù),那么加入該事務(wù),如果不存在事務(wù),創(chuàng)建一個(gè)事務(wù),這是默認(rèn)的傳播屬性值。SUPPORTS
如果當(dāng)前上下文存在事務(wù),則支持事務(wù)加入事務(wù),如果不存在事務(wù),則使用非事務(wù)的方式執(zhí)行。MANDATORY
如果當(dāng)前上下文中存在事務(wù),否則拋出異常。REQUIRES_NEW
每次都會(huì)新建一個(gè)事務(wù),并且同時(shí)將上下文中的事務(wù)掛起,執(zhí)行當(dāng)前新建事務(wù)完成以后,上下文事務(wù)恢復(fù)再執(zhí)行。NOT_SUPPORTED
如果當(dāng)前上下文中存在事務(wù),則掛起當(dāng)前事務(wù),然后新的方法在沒有事務(wù)的環(huán)境中執(zhí)行。NEVER
如果當(dāng)前上下文中存在事務(wù),則拋出異常,否則在無事務(wù)環(huán)境上執(zhí)行代碼。NESTED
如果當(dāng)前上下文中存在事務(wù),則嵌套事務(wù)執(zhí)行,如果不存在事務(wù),則新建事務(wù)。
解決方案:
將事務(wù)傳播策略更改為默認(rèn)值REQUIRED
。REQUIRED
原理是如果當(dāng)前有一個(gè)事務(wù)被添加到一個(gè)事務(wù)中,如果沒有,則創(chuàng)建一個(gè)新的事務(wù),父事務(wù)和被調(diào)用的事務(wù)在同一個(gè)事務(wù)中。即使被調(diào)用的異常被捕獲,整個(gè)事務(wù)仍然會(huì)被回滾。
7. 沒有被Spring管理
// @Service public class OrderServiceImpl implements OrderService { @Transactional public void updateOrder(Order order) { // update order } }
如果此時(shí)把 @Service
注解注釋掉,這個(gè)類就不會(huì)被加載成一個(gè) Bean
,那這個(gè)類就不會(huì)被 Spring
管理了,事務(wù)自然就失效了。
解決方案:
需要保證每個(gè)事務(wù)注解的每個(gè)Bean被Spring管理。
8. 多線程
@Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); new Thread(() -> { try { test(); } catch (Exception e) { roleService.doOtherThing(); } }).start(); } } @Service public class RoleService { @Transactional public void doOtherThing() { try { int i = 1/0; System.out.println("保存role表數(shù)據(jù)"); }catch (Exception e) { throw new RuntimeException(); } } }
我們可以看到事務(wù)方法add中,調(diào)用了事務(wù)方法doOtherThing
,但是事務(wù)方法doOtherThing
是在另外一個(gè)線程中調(diào)用的。
這樣會(huì)導(dǎo)致兩個(gè)方法不在同一個(gè)線程中,獲取到的數(shù)據(jù)庫(kù)連接不一樣,從而是兩個(gè)不同的事務(wù)。如果想doOtherThing
方法中拋了異常,add
方法也回滾是不可能的。
我們說的同一個(gè)事務(wù),其實(shí)是指同一個(gè)數(shù)據(jù)庫(kù)連接,只有擁有同一個(gè)數(shù)據(jù)庫(kù)連接才能同時(shí)提交和回滾。如果在不同的線程,拿到的數(shù)據(jù)庫(kù)連接肯定是不一樣的,所以是不同的事務(wù)。
解決方案:
這里就有點(diǎn)分布式事務(wù)的感覺了,盡量還是保證在同一個(gè)事務(wù)中處理。
總結(jié)
本文簡(jiǎn)單闡述了下Spring
中事務(wù)實(shí)現(xiàn)的原理,同時(shí)列舉了8種Spring
事務(wù)失敗的場(chǎng)景,相信很多朋友可能都遇到過, 失敗的原因也有詳細(xì)說明。希望大家對(duì)Spring
事務(wù)有一個(gè)新的認(rèn)識(shí)。
以上就是8個(gè)Spring事務(wù)失效場(chǎng)景詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring事務(wù)失效場(chǎng)景的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java集合框架之List ArrayList LinkedList使用詳解刨析
早在 Java 2 中之前,Java 就提供了特設(shè)類。比如:Dictionary, Vector, Stack, 和 Properties 這些類用來存儲(chǔ)和操作對(duì)象組。雖然這些類都非常有用,但是它們?nèi)鄙僖粋€(gè)核心的,統(tǒng)一的主題。由于這個(gè)原因,使用 Vector 類的方式和使用 Properties 類的方式有著很大不同2021-10-10如何解決Spring in action @valid驗(yàn)證不生效的問題
這篇文章主要介紹了如何解決Spring in action @valid驗(yàn)證不生效的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06Spring占位符Placeholder的實(shí)現(xiàn)原理解析
這篇文章主要介紹了Spring占位符Placeholder的實(shí)現(xiàn)原理,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03springboot整合mybatis-plus代碼生成器的配置解析
這篇文章主要介紹了springboot整合mybatis-plus代碼生成器的配置解析,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02Java?C++題解leetcode769最多能完成排序的塊
這篇文章主要為大家介紹了Java?C++題解leetcode769最多能完成排序的塊示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10java poi設(shè)置生成的word的圖片為上下型環(huán)繞以及其位置的實(shí)現(xiàn)
這篇文章主要介紹了java poi設(shè)置生成的word的圖片為上下型環(huán)繞以及其位置的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09java實(shí)現(xiàn)圖片水平和垂直翻轉(zhuǎn)效果
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)圖片水平和垂直翻轉(zhuǎn)效果,圖片旋轉(zhuǎn)的靈活運(yùn)用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01SpringBoot整合GRPC微服務(wù)遠(yuǎn)程通信的實(shí)現(xiàn)示例
本文主要介紹了SpringBoot整合GRPC微服務(wù)遠(yuǎn)程通信的實(shí)現(xiàn)示例,包含gRPC的工作原理,以及如何在Spring Boot應(yīng)用中集成gRPC,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02