解決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實現(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?
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ù)空校驗與提醒處理
}
}2、BService 和 CService 實現(xiàn)類(CService實現(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ù)校驗
// 模擬代碼,出現(xiàn)不匹配數(shù)據(jù)錯誤情況會拋出異常
if (Objects.isNull(param)){
throw new BaseException(CodeEnum.FAILED.getCode(),"BServiceImpl saveInfo param mistake");
}
// 其它操作
}
}一、Spring事務(wù)實現(xiàn)方式及原理
Spring 事務(wù)的本質(zhì)其實就是數(shù)據(jù)庫對事務(wù)的支持,沒有數(shù)據(jù)庫的事務(wù)支持,spring 是無法提供事務(wù)功能的。真正的數(shù)據(jù)庫層的事務(wù)提交和回滾是通過 binlog 或者 redo log 實現(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 這個是沒有任何作用的,因為調(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ù)管用么?
答案是:不管用!
因為它們發(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項(當(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?
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ù)空校驗與提醒處理
}
上述代碼使用了 PlatformTransactionManager接口的實現(xiàn)來手動管理事務(wù)。
在代碼中,我們首先獲取 transactionManager的實例,然后使用該實例手動創(chuàng)建事務(wù)定義和事務(wù)狀態(tài)。在try塊中執(zhí)行update方法和其它方法,并在最后根據(jù)執(zhí)行情況手動提交或回滾事務(wù)。
通過這種方式,可以確保update方法的操作在事務(wù)中進(jìn)行,且在其它方法中發(fā)生異常時能夠回滾。如果采取這個方式請確保在相應(yīng)的配置類中將transactionManager正確配置為適用于你的應(yīng)用程序的事務(wù)管理器實現(xiàn)。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
淺談Java中的n種隨機(jī)數(shù)產(chǎn)生辦法
眾所周知,隨機(jī)數(shù)是任何一種編程語言最基本的特征之一。而生成隨機(jī)數(shù)的基本方式也是相同的:產(chǎn)生一個0到1之間的隨機(jī)數(shù)??此坪唵危袝r我們也會忽略了一些有趣的功能。2015-09-09
Java中Scanner的常用方法總結(jié)(一次學(xué)懂)
這篇文章主要給大家介紹了關(guān)于Java中Scanner常用方法的相關(guān)資料,Java中的Scanner是一個用于讀取用戶輸入的類,它可以讀取各種類型的數(shù)據(jù),包括整數(shù)、浮點(diǎn)數(shù)、字符串等等,需要的朋友可以參考下2023-11-11
springboot學(xué)習(xí)之構(gòu)建簡單項目搭建步驟詳解
這篇文章主要介紹了springboot學(xué)習(xí)之構(gòu)建簡單項目搭建步驟詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10

