解決Spring事務(wù)@Transactional多層嵌套失效問題
場景
在 AService 中,我會直接調(diào)用 A 的數(shù)據(jù)操作層去操作 A的數(shù)據(jù) 以及 A關(guān)聯(lián)密切的其它數(shù)據(jù),在操作完之后,會去調(diào)用 BService 和 CService 中更新對應(yīng)的數(shù)據(jù),并在每個方法上使用了事務(wù),但在調(diào)用 BService 或者 CService 時候出現(xiàn)了異常,此時出現(xiàn)異常的BService 或者 CService 中數(shù)據(jù)沒有改變,回滾了。
但在 AService 中調(diào)用的 update 方法和出現(xiàn)異常前已經(jīng)執(zhí)行完的方法執(zhí)行成功并且沒有回滾。
偽代碼如下:
1、AService實(shí)現(xiàn)類
@Service @Slf4j public class AServiceImpl implements IAService { private final IBService bService; private final ICService cService; public AServiceImpl(IBService bService, ICService cService) { this.bService = bService; this.cService = cService; } @Override @Transactional(rollbackFor = Exception.class) public String modifyA(TestAParam param) throws BaseException { // 兜底:入?yún)⒖兆侄涡r?yàn) this.judgeNull(param); TestModifyParam modifyParam = param.getModifyParam(); String aCode = update(param, Boolean.TRUE); if (null != modifyParam.getStatus() && TestStatusConstant.EDITED.equals(modifyParam.getStatus())){ // 保存B信息 bService.saveInfo(param, aCode); // 保存C信息 cService.saveInfo(param, aCode); } // 更新A數(shù)據(jù)關(guān)聯(lián)其它數(shù)據(jù) if (StringUtils.isNotBlank(aCode)){ setOtherData(param); } return aCode; } @Transactional(rollbackFor = Exception.class) public void setOtherData(TestAParam param) throws BaseException { // 其它關(guān)聯(lián)數(shù)據(jù)處理 } @Transactional(rollbackFor = Exception.class) public String update(TestAParam param, Boolean directlyFlag) throws BaseException { // 更新處理 return null; } @Transactional(rollbackFor = Exception.class) public void judgeNull(TestAParam param) throws BaseException { // 參數(shù)空校驗(yàn)與提醒處理 } }
2、BService 和 CService 實(shí)現(xiàn)類(CService實(shí)現(xiàn)類異常處理差不多相同)
@Service @Slf4j public class BServiceImpl implements IBService { @Override @Transactional(rollbackFor = Exception.class) public void saveInfo(TestAParam param, String aCode) throws BaseException { // 數(shù)據(jù)校驗(yàn) // 模擬代碼,出現(xiàn)不匹配數(shù)據(jù)錯誤情況會拋出異常 if (Objects.isNull(param)){ throw new BaseException(CodeEnum.FAILED.getCode(),"BServiceImpl saveInfo param mistake"); } // 其它操作 } }
一、Spring事務(wù)實(shí)現(xiàn)方式及原理
Spring 事務(wù)的本質(zhì)其實(shí)就是數(shù)據(jù)庫對事務(wù)的支持,沒有數(shù)據(jù)庫的事務(wù)支持,spring 是無法提供事務(wù)功能的。真正的數(shù)據(jù)庫層的事務(wù)提交和回滾是通過 binlog 或者 redo log 實(shí)現(xiàn)的。
一般我們在程序里面使用的都是在方法上面加 @Transaction 注解,這種屬于聲明式事物。
聲明式事務(wù)本質(zhì)是通過 AOP 功能,對方法前后進(jìn)行攔截,將事務(wù)處理的功能編織到攔截的方法中,也就是在目標(biāo)方法開始之前加入一個事務(wù),在執(zhí)行完目標(biāo)方法之后根據(jù)執(zhí)行情況提交或者回滾事務(wù)。
二、事務(wù)失效原因
2.1 數(shù)據(jù)庫本身不支持事物
這里以 MySQL 為例,其 MyISAM 引擎是不支持事務(wù)操作的,InnoDB 才是支持事務(wù)的引擎,一般要支持事務(wù)都會使用 InnoDB。
2.2 方法不是Public
注解 @Transactional 只能放在 public 修飾的方法上才起作用(private 方法是不會被spring代理的)因此是不會有事物產(chǎn)生的,這種做法是無效的。
2.3 未被 Spring 管理的Bean
沒有被spring管理的bean, spring連代理對象都無法生成,事務(wù)自然是無效的。
2.4 當(dāng)前類的調(diào)用
@Service public class UserServiceImpl implements UserService { public void update(User user) { updateUser(user); } @Transactional(rollbackFor = Exception.class) public void updateUser(User user) { // update user } }
上面的這種情況下是不會有事物管理操作的。
通過看聲明式事物的原理可知,spring使用的是AOP切面的方式,本質(zhì)上使用的是動態(tài)代理來達(dá)到事物管理的目的,當(dāng)前類調(diào)用的方法上面加 @Transactional 這個是沒有任何作用的,因?yàn)檎{(diào)用這個方法的是this。
再看下面的一種例子:
@Service public class UserServiceImpl implements UserService { @Transactional(rollbackFor = Exception.class) public void update(User user) { updateUser(user); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateUser(User user) { // update user } }
這次在 update 方法上加了 @Transactional,updateUser 加了 REQUIRES_NEW 新開啟一個事務(wù),那么新開的事務(wù)管用么?
答案是:不管用!
因?yàn)樗鼈儼l(fā)生了自身調(diào)用,就調(diào)該類自己的方法,而沒有經(jīng)過 Spring 的代理類,默認(rèn)只有在外部調(diào)用事務(wù)才會生效,這也是老生常談的經(jīng)典問題了。
還有就是在沒有指定事務(wù)傳播行為時,從源碼中可以看到默認(rèn)是使用 Propagation.REQUIRED。
2.5 配置的事物傳播性有問題
@Service public class UserServiceImpl implements UserService { @Transactional(propagation = Propagation.NOT_SUPPORTED) public void update(User user) { // update user } }
2.6 異常被你 "抓住"了
@Service public class UserServiceImpl implements UserService { @Transactional(rollbackFor = Exception.class) public void update(User user) { try{ // update user }catch(Execption e){ log.error("異常",e) } } }
異常被抓了,這樣子代理類就沒辦法知道你到底有沒有錯誤,需不需要回滾,所以這種情況也是沒辦法回滾。
2.7 rollbackFor 異常指定錯誤
@Service public class UserServiceImpl implements UserService { @Transactional public void update(User user) { // update user } }
上面這種沒有指定回滾異常,這個時候默認(rèn)的回滾異常是 RuntimeException ,如果出現(xiàn)其他異常那么就不會回滾事物。
三、Spring的事務(wù)傳播行為
Spring 事務(wù)的傳播行為說的是,當(dāng)多個事務(wù)同時存在的時候, Spring 如何處理這些事務(wù)的行為。
類型 | 說明 |
---|---|
PROPAGATION_REQUIRED | 如果當(dāng)前沒有事務(wù),就創(chuàng)建一個新事務(wù),如果當(dāng)前存在事務(wù),就加入該事務(wù),該設(shè)置是最常用的設(shè)置。 |
PROPAGATION_SUPPORTS | 支持當(dāng)前事務(wù),如果當(dāng)前存在事務(wù),就加入該事務(wù),如果當(dāng)前不存在事務(wù),就以非事務(wù)執(zhí)行。 |
PROPAGATION_MANDATORY | 支持當(dāng)前事務(wù),如果當(dāng)前存在事務(wù),就加入該事務(wù),如果當(dāng)前不存在事務(wù),就拋出異常。 |
PROPAGATION_REQUIRES_NEW | 創(chuàng)建新事務(wù),無論當(dāng)前存不存在事務(wù),都創(chuàng)建新事務(wù)。 |
PROPAGATION_NOT_SUPPORTED | 以非事務(wù)方式執(zhí)行操作,如果當(dāng)前存在事務(wù),就把當(dāng)前事務(wù)掛起。 |
PROPAGATION_NEVER | 以非事務(wù)方式執(zhí)行,如果當(dāng)前存在事務(wù),則拋出異常。 |
PROPAGATION_NESTED | 如果當(dāng)前存在事務(wù),則在嵌套事務(wù)內(nèi)執(zhí)行。如果當(dāng)前沒有事務(wù),則按 REQUIRED 屬性執(zhí)行。 |
當(dāng)傳播行為設(shè)置了PROPAGATION_NOT_SUPPORTED,PROPAGATION_NEVER,PROPAGATION_SUPPORTS這三種時,就有可能存在事物不生效。
四、解決思路
1、聲明式事務(wù)處理,采用合適的事務(wù)傳播行為,將AService中修改A中數(shù)據(jù)方法update和更新A數(shù)據(jù)關(guān)聯(lián)其它數(shù)據(jù)方法setOtherData通過AppContext.getBean()的方式直接獲取AService中的方法,不違背事務(wù)失效原因中的2.4項(xiàng)(當(dāng)前類的調(diào)用),并將保持B和C信息的方法單獨(dú)抽取出來,提供一個saveBandCInfo方法,加上事務(wù)處理和傳播行為,并拋出異常信息。
2、使用編程式事務(wù)管理,手動配置事務(wù)邊界,確保modifyA中所有
方法在事務(wù)中執(zhí)行。
五、采取方法
由于AService中的方法 modifyA 調(diào)用鏈路比較長(業(yè)務(wù)急需處理好),如果使用聲明式事務(wù)處理,改動起來是比較大的,中間鏈路可能會存在事務(wù)傳播行為失效的情況,此時使用編程式事務(wù)管理解決就會很明顯輕松解決。(這方法并不建議大家日常使用,建議使用聲明事務(wù)更好點(diǎn))
偽代碼如下:
@Service @Slf4j public class AServiceImpl implements IAService { private final IBService bService; private final ICService cService; private final PlatformTransactionManager transactionManager; public AServiceImpl(IBService bService, ICService cService, PlatformTransactionManager transactionManager) { this.bService = bService; this.cService = cService; this.transactionManager = transactionManager; } @Override @Transactional(rollbackFor = Exception.class) public String modifyA(TestAParam param) throws BaseException { // 兜底:入?yún)⒖兆侄涡r?yàn) this.judgeNull(param); // 編程式事務(wù) DefaultTransactionDefinition def = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(def); try { TestModifyParam modifyParam = param.getModifyParam(); String aCode = update(param, Boolean.TRUE); if (null != modifyParam.getStatus() && TestStatusConstant.EDITED.equals(modifyParam.getStatus())){ // 保存B信息 bService.saveInfo(param, aCode); // 保存C信息 cService.saveInfo(param, aCode); } // 更新A數(shù)據(jù)關(guān)聯(lián)其它數(shù)據(jù) if (StringUtils.isNotBlank(aCode)){ setOtherData(param); } // 提交事務(wù) transactionManager.commit(status); return aCode; } catch (Exception e) { // 回滾事務(wù) transactionManager.rollback(status); // 處理異?;蚋鶕?jù)需要重新拋出異常 throw new BaseException(CodeEnum.FAILED.getCode(),"modifyA error message is {}", e.getMessage()); } } @Transactional(rollbackFor = Exception.class) public void setOtherData(TestAParam productStrategyDO) throws BaseException { // 其它關(guān)聯(lián)數(shù)據(jù)處理 } @Transactional(rollbackFor = Exception.class) public String update(TestAParam param, Boolean directlyFlag) throws BaseException { // 更新處理 return null; } @Transactional(rollbackFor = Exception.class) public void judgeNull(TestAParam param) throws BaseException { // 參數(shù)空校驗(yàn)與提醒處理 }
上述代碼使用了 PlatformTransactionManager
接口的實(shí)現(xiàn)來手動管理事務(wù)。
在代碼中,我們首先獲取 transactionManager
的實(shí)例,然后使用該實(shí)例手動創(chuàng)建事務(wù)定義和事務(wù)狀態(tài)。在try
塊中執(zhí)行update
方法和其它方法,并在最后根據(jù)執(zhí)行情況手動提交或回滾事務(wù)。
通過這種方式,可以確保update
方法的操作在事務(wù)中進(jìn)行,且在其它
方法中發(fā)生異常時能夠回滾。如果采取這個方式請確保在相應(yīng)的配置類中將transactionManager
正確配置為適用于你的應(yīng)用程序的事務(wù)管理器實(shí)現(xiàn)。
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
淺談Java中的n種隨機(jī)數(shù)產(chǎn)生辦法
眾所周知,隨機(jī)數(shù)是任何一種編程語言最基本的特征之一。而生成隨機(jī)數(shù)的基本方式也是相同的:產(chǎn)生一個0到1之間的隨機(jī)數(shù)??此坪唵危袝r我們也會忽略了一些有趣的功能。2015-09-09Java中Scanner的常用方法總結(jié)(一次學(xué)懂)
這篇文章主要給大家介紹了關(guān)于Java中Scanner常用方法的相關(guān)資料,Java中的Scanner是一個用于讀取用戶輸入的類,它可以讀取各種類型的數(shù)據(jù),包括整數(shù)、浮點(diǎn)數(shù)、字符串等等,需要的朋友可以參考下2023-11-11springboot學(xué)習(xí)之構(gòu)建簡單項(xiàng)目搭建步驟詳解
這篇文章主要介紹了springboot學(xué)習(xí)之構(gòu)建簡單項(xiàng)目搭建步驟詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10