Spring事務(wù)失效的9大場(chǎng)景與解決方法
前言
在日常開(kāi)發(fā)中,我們經(jīng)常使用Spring
事務(wù)。最近,一個(gè)朋友去面試,被問(wèn)到了這樣一個(gè)面試題:在什么情況下,Spring 事務(wù)會(huì)失效?
今天,我將和大家聊聊Spring
事務(wù)失效的 9 種場(chǎng)景。
1. 拋出檢查異常(checked exceptions)
例如,你的事務(wù)控制代碼如下:
@Transactional public void transactionTest() throws IOException { User user = new User(); UserService.insert(user); throw new IOException(); }
如果沒(méi)有特別指定@Transactional
,Spring 默認(rèn)只會(huì)在遇到運(yùn)行時(shí)異常RuntimeException
或錯(cuò)誤時(shí)回滾,而檢查異常如IOException
不會(huì)觸發(fā)回滾。
public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); }
解決方案:
知道原因后,解決方案也很簡(jiǎn)單。配置rollbackFor
屬性,例如:@Transactional(rollbackFor = Exception.class)
。
@Transactional(rollbackFor = Exception.class) public void transactionTest() throws IOException { User user = new User(); UserService.insert(user); throw new IOException(); }
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(); } }
在這個(gè)場(chǎng)景中,事務(wù)失效的原因也很簡(jiǎn)單。Spring 是否回滾事務(wù)取決于你是否拋出了異常。如果你自己捕獲了異常,Spring 就無(wú)法處理事務(wù)了。
看了上面的代碼,你可能會(huì)覺(jué)得這么簡(jiǎn)單的問(wèn)題,自己不可能犯這種低級(jí)錯(cuò)誤。但我想告訴你,我身邊幾乎有一半的人都曾因此困擾過(guò)。
在編寫業(yè)務(wù)代碼時(shí),代碼可能會(huì)更復(fù)雜,有很多嵌套的方法。稍不注意,就很容易觸發(fā)這個(gè)問(wèn)題。舉個(gè)簡(jiǎn)單的例子,假設(shè)你有一個(gè)審計(jì)功能,每次方法執(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(); } }
下面的切面會(huì)作用于TransactionService
:
@Component publicclass AuditAspect { @Autowired private AuditService auditService; @Around(value = "execution (* com.dylan.service.*.*(..))") 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); returnnull; } }
在上面的例子中,如果程序執(zhí)行異常,事務(wù)也會(huì)失效。原因是Spring
的事務(wù)切面優(yōu)先級(jí)最低。如果異常被切面捕獲,Spring 自然無(wú)法正確處理事務(wù),因?yàn)槭聞?wù)管理器無(wú)法捕獲到異常。
解決方案:
只需移除try-catch
。雖然我們知道在處理事務(wù)時(shí),業(yè)務(wù)代碼不能自己捕獲異常,但只要代碼變得復(fù)雜,我們很容易不小心犯錯(cuò)。
3. 同一個(gè)類中的方法調(diào)用
@Service publicclass DefaultTransactionService implements 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); thrownew IOException(); } }
這也是一個(gè)容易出錯(cuò)的場(chǎng)景。事務(wù)失效的原因也很簡(jiǎn)單。因?yàn)?Spring 的事務(wù)管理功能是通過(guò)動(dòng)態(tài)代理實(shí)現(xiàn)的,而 Spring 默認(rèn)使用 JDK 動(dòng)態(tài)代理,JDK 動(dòng)態(tài)代理通過(guò)接口實(shí)現(xiàn),并通過(guò)反射調(diào)用目標(biāo)類。簡(jiǎn)單理解,在saveUser()
方法中,調(diào)用this.doInsert()
時(shí),this
是真實(shí)對(duì)象,因此會(huì)直接執(zhí)行doInsert
的業(yè)務(wù)邏輯,而不是代理邏輯,從而導(dǎo)致事務(wù)失效。
解決方案:
方案 1:直接在saveUser
方法上添加@Transactional
注解。
方案 2:可以將這兩個(gè)方法拆分到不同的類中。
方案 3:不使用注解實(shí)現(xiàn)事務(wù),而是使用編程式事務(wù)來(lái)包裹需要開(kāi)啟事務(wù)的代碼塊。例如:transactionTemplate.execute()
。
public void doInsert() throws IOException { transactionTemplate.execute(() -> { User user = new User(); UserService.insert(user); throw new IOException(); }); }
4. 方法使用了final或static關(guān)鍵字
如果 Spring 使用 Cglib 代理實(shí)現(xiàn)(當(dāng)你的代理類沒(méi)有實(shí)現(xiàn)接口時(shí)),而你的業(yè)務(wù)方法恰好使用了final
或static
關(guān)鍵字,那么事務(wù)控制也會(huì)失效。因?yàn)?Cglib 使用字節(jié)碼增強(qiáng)技術(shù)生成被代理類的子類,并重寫被代理類的方法來(lái)實(shí)現(xiàn)代理。如果被代理的方法使用了final
或static
關(guān)鍵字,子類就無(wú)法重寫被代理的方法。
如果 Spring 使用 JDK 動(dòng)態(tài)代理實(shí)現(xiàn),JDK 動(dòng)態(tài)代理是基于接口實(shí)現(xiàn)的,那么被final
和static
修飾的方法也無(wú)法被代理。
總之,如果方法連代理都沒(méi)有,那么事務(wù)回滾肯定無(wú)法實(shí)現(xiàn)。
解決方案:
盡量移除方法上的final
或static
關(guān)鍵字。
5. 方法不是public
如果方法不是public
,Spring 事務(wù)也會(huì)失效,因?yàn)樵?Spring 事務(wù)管理的源碼AbstractFallbackTransactionAttributeSource
中,computeTransactionAttribute()
方法會(huì)判斷目標(biāo)方法是否是public
。如果不是public
,則返回null
。
// Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; }
解決方案:
將當(dāng)前方法的訪問(wèn)級(jí)別改為public
。
6. 傳播機(jī)制使用不當(dāng)
Spring
事務(wù)的傳播機(jī)制指的是當(dāng)多個(gè)事務(wù)方法相互調(diào)用時(shí),事務(wù)應(yīng)該如何傳播的策略。Spring
提供了七種事務(wù)傳播機(jī)制:REQUIRED
、SUPPORTS
、MANDATORY
、REQUIRES_NEW
、NOT_SUPPORTED
、NEVER
、NESTED
。如果你不了解這些傳播策略的原理,很容易導(dǎo)致事務(wù)失效。
@Service publicclass TransactionsService { @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)檫@里使用的傳播機(jī)制是REQUIRES_NEW
。REQUIRES_NEW
的原理是,如果當(dāng)前方法沒(méi)有事務(wù),則創(chuàng)建一個(gè)新事務(wù)。如果當(dāng)前方法已經(jīng)有事務(wù),則掛起當(dāng)前事務(wù)并創(chuàng)建一個(gè)新事務(wù)。父事務(wù)會(huì)等到當(dāng)前事務(wù)完成后才提交。如果父事務(wù)發(fā)生異常,不會(huì)影響子事務(wù)的提交。
解決方案:
將事務(wù)傳播策略改為默認(rèn)值REQUIRED
。REQUIRED
的原理是,如果當(dāng)前有事務(wù),則加入該事務(wù)。如果沒(méi)有事務(wù),則創(chuàng)建一個(gè)新事務(wù)。父事務(wù)和被調(diào)用的事務(wù)處于同一個(gè)事務(wù)中。即使被調(diào)用的事務(wù)捕獲了異常,整個(gè)事務(wù)仍然會(huì)回滾。
7. 沒(méi)有被 Spring 管理
// @Service public class OrderServiceImpl implements OrderService { @Transactional public void updateOrder(Order order) { // update order } }
如果此時(shí)@Service
注解被注釋掉,這個(gè)類就不會(huì)被 Spring 加載為 Bean,那么這個(gè)類就不會(huì)被 Spring 管理,事務(wù)自然也會(huì)失效。
解決方案:
確保每個(gè)使用事務(wù)注解的Service
都被 Spring 管理。
8. 多線程調(diào)用
@Service publicclass 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 publicclass RoleService { @Transactional public void doOtherThing() { try { int i = 1 / 0; System.out.println("save role table data"); } catch (Exception e) { thrownew RuntimeException(); } } }
我們可以看到,在事務(wù)方法add
中,調(diào)用了事務(wù)方法doOtherThing
,但doOtherThing
是在另一個(gè)線程中被調(diào)用的。
這會(huì)導(dǎo)致兩個(gè)方法不在同一個(gè)線程中,獲取的數(shù)據(jù)庫(kù)連接也不同,因此是兩個(gè)不同的事務(wù)。如果在doOtherThing
方法中拋出異常,add
方法是不可能回滾的。
我們所說(shuō)的同一個(gè)事務(wù),實(shí)際上指的是同一個(gè)數(shù)據(jù)庫(kù)連接。只有在同一個(gè)數(shù)據(jù)庫(kù)連接下,才能同時(shí)提交和回滾。如果在不同的線程中,獲取的數(shù)據(jù)庫(kù)連接肯定不同,因此它們是不同的事務(wù)。
解決方案:
這有點(diǎn)像分布式事務(wù)。盡量確保在同一個(gè)事務(wù)中處理。
9. 沒(méi)有配置開(kāi)啟事務(wù)
如果在項(xiàng)目中沒(méi)有配置 Spring 的事務(wù)管理器,即使使用了 Spring 的事務(wù)管理功能,Spring 的事務(wù)也不會(huì)生效。例如,如果你是一個(gè) Spring Boot 項(xiàng)目,并且沒(méi)有在 Spring Boot 項(xiàng)目中配置以下代碼:
@EnableTransactionManagement
解決方案:
確保在項(xiàng)目中正確配置了事務(wù)管理器。
總結(jié)
本文簡(jiǎn)要闡述了 Spring 事務(wù)的實(shí)現(xiàn)原理,并列出了 9 種 Spring 事務(wù)失效的場(chǎng)景。相信很多朋友可能都遇到過(guò)這些問(wèn)題。文章也詳細(xì)解釋了失效的原因,希望大家對(duì) Spring 事務(wù)有新的理解。
到此這篇關(guān)于Spring事務(wù)失效的9大場(chǎng)景與解決方法的文章就介紹到這了,更多相關(guān)Spring事務(wù)失效內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring aop實(shí)現(xiàn)用戶權(quán)限管理的示例
本篇文章主要介紹了spring aop實(shí)現(xiàn)用戶權(quán)限管理的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12小伙熬夜用Java重現(xiàn)經(jīng)典超級(jí)馬里奧代碼實(shí)例
這篇文章主要介紹了Java重現(xiàn)經(jīng)典超級(jí)馬里奧,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04java 中動(dòng)態(tài)代理(JDK,cglib)實(shí)例代碼
這篇文章主要介紹了java 中動(dòng)態(tài)代理,這里介紹了JDK 動(dòng)態(tài)代理與 cglib 動(dòng)態(tài)代理的相關(guān)資料2017-04-04二種jar包制作方法講解(dos打包jar eclipse打包jar文件)
這篇文章主要介紹了二種jar包制作方法講解:dos打包jar和eclipse打包jar文件,大家參考使用吧2013-11-11java面試常見(jiàn)問(wèn)題之Hibernate總結(jié)
這篇文章主要介紹了在java面試過(guò)程中hibernate比較常見(jiàn)的問(wèn)題,包括Hibernate的檢索方式,Hibernate中對(duì)象的狀態(tài),Hibernate的3種檢索策略是什么,Session的find()方法以及Query接口的區(qū)別等方面問(wèn)題的總結(jié),需要的朋友可以參考下2015-07-07在IDEA中創(chuàng)建SpringBoot項(xiàng)目的詳細(xì)步驟
這篇文章主要給大家介紹了在IDEA中創(chuàng)建SpringBoot項(xiàng)目的詳細(xì)步驟,文中有詳細(xì)的圖文介紹和代碼示例,對(duì)大家的學(xué)習(xí)和工作有一定的幫助,需要的朋友可以參考下2023-09-09理解Java當(dāng)中的回調(diào)機(jī)制(翻譯)
今天我要和大家分享一些東西,舉例來(lái)說(shuō)這個(gè)在JavaScript中用的很多。我要講講回調(diào)(callbacks)。你知道什么時(shí)候用,怎么用這個(gè)嗎?你真的理解了它在java環(huán)境中的用法了嗎?當(dāng)我也問(wèn)我自己這些問(wèn)題,這也是我開(kāi)始研究這些的原因2014-10-10SpringBoot+websocket實(shí)現(xiàn)消息對(duì)話功能
WebSocket是一種在Web應(yīng)用程序中實(shí)現(xiàn)實(shí)時(shí)雙向通信的技術(shù),它可以用于在線游戲、在線聊天、推送通知、實(shí)時(shí)監(jiān)控等,并且比傳統(tǒng)的輪詢技術(shù)更加高效和可靠,本文就給大家介紹基于SpringBoot+websocket實(shí)現(xiàn)消息對(duì)話功能,感興趣的小伙伴可以自己動(dòng)手試一試2023-09-09Java經(jīng)典面試題匯總:網(wǎng)絡(luò)編程
本篇總結(jié)的是Java 網(wǎng)絡(luò)編程相關(guān)的面試題,后續(xù)會(huì)持續(xù)更新,希望我的分享可以幫助到正在備戰(zhàn)面試的實(shí)習(xí)生或者已經(jīng)工作的同行,如果發(fā)現(xiàn)錯(cuò)誤還望大家多多包涵,不吝賜教,謝謝2021-07-07MybatisPlus使用@TableId主鍵id自增長(zhǎng)無(wú)效的解決
本文主要介紹了MybatisPlus使用@TableId主鍵id自增長(zhǎng)無(wú)效的解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04