Spring事務(wù)的那些坑,這里都給你總結(jié)好了!
作者:蚊子squirrel
www.jianshu.com/p/a4229aa79ace
Spring框架已是JAVA項(xiàng)目的標(biāo)配,其中Spring事務(wù)管理也是最常用的一個(gè)功能,但如果不了解其實(shí)現(xiàn)原理,使用姿勢(shì)不對(duì),一不小心就可能掉坑里。
為了更透徹的說(shuō)明這些坑,本文分四部分展開(kāi)闡述:第一部分簡(jiǎn)單介紹下Spring事務(wù)集成的幾種方式;第二部分結(jié)合Spring源代碼說(shuō)明Spring事務(wù)的實(shí)現(xiàn)原理;第三部分通過(guò)實(shí)際測(cè)試代碼介紹關(guān)于Spring事務(wù)的坑;第四部分是對(duì)本文的總結(jié)。
一、Spring事務(wù)管理的幾種方式:
Spring事務(wù)在具體使用方式上可分為兩大類:
1. ?聲明式
基于 TransactionProxyFactoryBean的聲明式事務(wù)管理
基于
和命名空間的事務(wù)管理基于
@Transactional的聲明式事務(wù)管理
2. ?編程式
基于事務(wù)管理器API 的編程式事務(wù)管理
基于TransactionTemplate 的編程式事務(wù)管理
目前大部分項(xiàng)目使用的是聲明式的后兩種:
基于 和命名空間的聲明式事務(wù)管理可以充分利用切點(diǎn)表達(dá)式的強(qiáng)大支持,使得管理事務(wù)更加靈活。基于 @Transactional的方式需要實(shí)施事務(wù)管理的方法或者類上使用@Transactional指定事務(wù)規(guī)則即可實(shí)現(xiàn)事務(wù)管理,在Spring Boot中通常也建議使用這種注解方式來(lái)標(biāo)記事務(wù)。
二、Spring事務(wù)實(shí)現(xiàn)機(jī)制
接下來(lái)我們?cè)敿?xì)看下Spring事務(wù)的源代碼,進(jìn)而了解其工作原理。我們從標(biāo)簽的解析類開(kāi)始:
@Override
public?void?init()?{
????????registerBeanDefinitionParser("advice",?new?TxAdviceBeanDefinitionParser());
????????registerBeanDefinitionParser("annotation-driven",?new?AnnotationDrivenBeanDefinitionParser());
????????registerBeanDefinitionParser("jta-transaction-manager",?new?JtaTransactionManagerBeanDefinitionParser());
????}
}
class?TxAdviceBeanDefinitionParser?extends?AbstractSingleBeanDefinitionParser?{
????@Override
????protected?Class>?getBeanClass(Element?element)?{
????????return?TransactionInterceptor.class;
????}
}
由此可看到Spring事務(wù)的核心實(shí)現(xiàn)類TransactionInterceptor及其父類TransactionAspectSupport,其實(shí)現(xiàn)了事務(wù)的開(kāi)啟、數(shù)據(jù)庫(kù)操作、事務(wù)提交、回滾等。我們平時(shí)在開(kāi)發(fā)時(shí)如果想確定是否在事務(wù)中,也可以在該方法進(jìn)行斷點(diǎn)調(diào)試。
TransactionInterceptor:
public?Object?invoke(final?MethodInvocation?invocation)?throws?Throwable?{
????????Class>?targetClass?=?(invocation.getThis()?!=?null???AopUtils.getTargetClass(invocation.getThis())?:?null);
????????//?Adapt?to?TransactionAspectSupport's?invokeWithinTransaction...
????????return?invokeWithinTransaction(invocation.getMethod(),?targetClass,?new?InvocationCallback()?{
????????????@Override
????????????public?Object?proceedWithInvocation()?throws?Throwable?{
????????????????return?invocation.proceed();
????????????}
????????});
????}
TransactionAspectSupport
protected?Object?invokeWithinTransaction(Method?method,?Class>?targetClass,?final?InvocationCallback?invocation)
????????????throws?Throwable?{
????????//?If?the?transaction?attribute?is?null,?the?method?is?non-transactional.
????????final?TransactionAttribute?txAttr?=?getTransactionAttributeSource().getTransactionAttribute(method,?targetClass);
????????final?PlatformTransactionManager?tm?=?determineTransactionManager(txAttr);
????????final?String?joinpointIdentification?=?methodIdentification(method,?targetClass,?txAttr);
????????if?(txAttr?==?null?||?!(tm?instanceof?CallbackPreferringPlatformTransactionManager))?{
????????????//?Standard?transaction?demarcation?with?getTransaction?and?commit/rollback?calls.
????????????TransactionInfo?txInfo?=?createTransactionIfNecessary(tm,?txAttr,?joinpointIdentification);
????????????Object?retVal?=?null;
????????????try?{
????????????????//?This?is?an?around?advice:?Invoke?the?next?interceptor?in?the?chain.
????????????????//?This?will?normally?result?in?a?target?object?being?invoked.
????????????????retVal?=?invocation.proceedWithInvocation();
????????????}
????????????catch?(Throwable?ex)?{
????????????????//?target?invocation?exception
????????????????completeTransactionAfterThrowing(txInfo,?ex);
????????????????throw?ex;
????????????}
????????????finally?{
????????????????cleanupTransactionInfo(txInfo);
????????????}
????????????commitTransactionAfterReturning(txInfo);
????????????return?retVal;
????????}
}
至此我們了解事務(wù)的整個(gè)調(diào)用流程,但還有一個(gè)重要的機(jī)制沒(méi)分析到,那就是Spring 事務(wù)針對(duì)不同的傳播級(jí)別控制當(dāng)前獲取的數(shù)據(jù)庫(kù)連接。接下來(lái)我們看下Spring獲取連接的工具類DataSourceUtils,JdbcTemplate、Mybatis-Spring也都是通過(guò)該類獲取Connection。
public?abstract?class?DataSourceUtils?{
…
public?static?Connection?getConnection(DataSource?dataSource)?throws?CannotGetJdbcConnectionException?{
????????try?{
????????????return?doGetConnection(dataSource);
????????}
????????catch?(SQLException?ex)?{
????????????throw?new?CannotGetJdbcConnectionException("Could?not?get?JDBC?Connection",?ex);
????????}
????}
public?static?Connection?doGetConnection(DataSource?dataSource)?throws?SQLException?{
????????Assert.notNull(dataSource,?"No?DataSource?specified");
????????ConnectionHolder?conHolder?=?(ConnectionHolder)?TransactionSynchronizationManager.getResource(dataSource);
????????if?(conHolder?!=?null?&&?(conHolder.hasConnection()?||?conHolder.isSynchronizedWithTransaction()))?{
????????????conHolder.requested();
????????????if?(!conHolder.hasConnection())?{
????????????????logger.debug("Fetching?resumed?JDBC?Connection?from?DataSource");
????????????????conHolder.setConnection(dataSource.getConnection());
????????????}
????????????return?conHolder.getConnection();
????????}
…
}
TransactionSynchronizationManager也是一個(gè)事務(wù)同步管理的核心類,它實(shí)現(xiàn)了事務(wù)同步管理的職能,包括記錄當(dāng)前連接持有connection holder。
TransactionSynchronizationManager
private?static?final?ThreadLocal在事務(wù)管理器類AbstractPlatformTransactionManager中,getTransaction獲取事務(wù)時(shí),會(huì)處理不同的事務(wù)傳播行為,例如當(dāng)前存在事務(wù),但調(diào)用方法事務(wù)傳播級(jí)別為REQUIRES_NEW、PROPAGATION_NOT_SUPPORTED時(shí),對(duì)當(dāng)前事務(wù)進(jìn)行掛起、恢復(fù)等操作,以此保證了當(dāng)前數(shù)據(jù)庫(kù)操作獲取正確的Connection。
具體是在子事務(wù)提交的最后會(huì)將掛起的事務(wù)恢復(fù),恢復(fù)時(shí)重新調(diào)用TransactionSynchronizationManager. bindResource設(shè)置之前的connection holder,這樣再獲取的連接就是被恢復(fù)的數(shù)據(jù)庫(kù)連接, TransactionSynchronizationManager當(dāng)前激活的連接只能是一個(gè)。
AbstractPlatformTransactionManager
private?TransactionStatus?handleExistingTransaction(
????????????TransactionDefinition?definition,?Object?transaction,?boolean?debugEnabled)
????????????throws?TransactionException?{
…
????????if?(definition.getPropagationBehavior()?==?TransactionDefinition.PROPAGATION_REQUIRES_NEW)?{
????????????if?(debugEnabled)?{
????????????????logger.debug("Suspending?current?transaction,?creating?new?transaction?with?name?["?+
????????????????????????definition.getName()?+?"]");
????????????}
????????????SuspendedResourcesHolder?suspendedResources?=?suspend(transaction);
????????????try?{
????????????????boolean?newSynchronization?=?(getTransactionSynchronization()?!=?SYNCHRONIZATION_NEVER);
????????????????DefaultTransactionStatus?status?=?newTransactionStatus(
????????????????????????definition,?transaction,?true,?newSynchronization,?debugEnabled,?suspendedResources);
????????????????doBegin(transaction,?definition);
????????????????prepareSynchronization(status,?definition);
????????????????return?status;
????????????}
????????????catch?(RuntimeException?beginEx)?{
????????????????resumeAfterBeginException(transaction,?suspendedResources,?beginEx);
????????????????throw?beginEx;
????????????}
????????????catch?(Error?beginErr)?{
????????????????resumeAfterBeginException(transaction,?suspendedResources,?beginErr);
????????????????throw?beginErr;
????????????}
????????}
/**
?????*?Clean?up?after?completion,?clearing?synchronization?if?necessary,
?????*?and?invoking?doCleanupAfterCompletion.
?????*?@param?status?object?representing?the?transaction
?????*?@see?#doCleanupAfterCompletion
?????*/
????private?void?cleanupAfterCompletion(DefaultTransactionStatus?status)?{
????????status.setCompleted();
????????if?(status.isNewSynchronization())?{
????????????TransactionSynchronizationManager.clear();
????????}
????????if?(status.isNewTransaction())?{
????????????doCleanupAfterCompletion(status.getTransaction());
????????}
????????if?(status.getSuspendedResources()?!=?null)?{
????????????if?(status.isDebug())?{
????????????????logger.debug("Resuming?suspended?transaction?after?completion?of?inner?transaction");
????????????}
????????????resume(status.getTransaction(),?(SuspendedResourcesHolder)?status.getSuspendedResources());
????????}
????}
Spring的事務(wù)是通過(guò)AOP代理類中的一個(gè)Advice(TransactionInterceptor)進(jìn)行生效的,而傳播級(jí)別定義了事務(wù)與子事務(wù)獲取連接、事務(wù)提交、回滾的具體方式。
AOP(Aspect Oriented Programming),即面向切面編程。Spring AOP技術(shù)實(shí)現(xiàn)上其實(shí)就是代理類,具體可分為靜態(tài)代理和動(dòng)態(tài)代理兩大類,其中靜態(tài)代理是指使用 AOP 框架提供的命令進(jìn)行編譯,從而在編譯階段就可生成 AOP 代理類,因此也稱為編譯時(shí)增強(qiáng);(AspectJ);而動(dòng)態(tài)代理則在運(yùn)行時(shí)借助于 默寫類庫(kù)在內(nèi)存中“臨時(shí)”生成 AOP 動(dòng)態(tài)代理類,因此也被稱為運(yùn)行時(shí)增強(qiáng)。其中java是使用的動(dòng)態(tài)代理模式 (JDK+CGLIB)。
JDK動(dòng)態(tài)代理 JDK動(dòng)態(tài)代理主要涉及到j(luò)ava.lang.reflect包中的兩個(gè)類:Proxy和InvocationHandler。InvocationHandler是一個(gè)接口,通過(guò)實(shí)現(xiàn)該接口定義橫切邏輯,并通過(guò)反射機(jī)制調(diào)用目標(biāo)類的代碼,動(dòng)態(tài)將橫切邏輯和業(yè)務(wù)邏輯編制在一起。Proxy利用InvocationHandler動(dòng)態(tài)創(chuàng)建一個(gè)符合某一接口的實(shí)例,生成目標(biāo)類的代理對(duì)象。
CGLIB動(dòng)態(tài)代理 CGLIB全稱為Code Generation Library,是一個(gè)強(qiáng)大的高性能,高質(zhì)量的代碼生成類庫(kù),可以在運(yùn)行期擴(kuò)展Java類與實(shí)現(xiàn)Java接口,CGLIB封裝了asm,可以再運(yùn)行期動(dòng)態(tài)生成新的class。和JDK動(dòng)態(tài)代理相比較:JDK創(chuàng)建代理有一個(gè)限制,就是只能為接口創(chuàng)建代理實(shí)例,而對(duì)于沒(méi)有通過(guò)接口定義業(yè)務(wù)方法的類,則可以通過(guò)CGLIB創(chuàng)建動(dòng)態(tài)代理。
CGLIB 創(chuàng)建代理的速度比較慢,但創(chuàng)建代理后運(yùn)行的速度卻非??欤?JDK 動(dòng)態(tài)代理正好相反。如果在運(yùn)行的時(shí)候不斷地用 CGLIB 去創(chuàng)建代理,系統(tǒng)的性能會(huì)大打折扣。因此如果有接口,Spring默認(rèn)使用JDK 動(dòng)態(tài)代理,源代碼如下:
public?class?DefaultAopProxyFactory?implements?AopProxyFactory,?Serializable?{
????@Override
????public?AopProxy?createAopProxy(AdvisedSupport?config)?throws?AopConfigException?{
????????if?(config.isOptimize()?||?config.isProxyTargetClass()?||?hasNoUserSuppliedProxyInterfaces(config))?{
????????????Class>?targetClass?=?config.getTargetClass();
????????????if?(targetClass?==?null)?{
????????????????throw?new?AopConfigException("TargetSource?cannot?determine?target?class:?"?+
????????????????????????"Either?an?interface?or?a?target?is?required?for?proxy?creation.");
????????????}
????????????if?(targetClass.isInterface()?||?Proxy.isProxyClass(targetClass))?{
????????????????return?new?JdkDynamicAopProxy(config);
????????????}
????????????return?new?ObjenesisCGLIBAopProxy(config);
????????}???
????????else?{
????????????return?new?JdkDynamicAopProxy(config);
????????}
????}
}
在了解Spring代理的兩種特點(diǎn)后,我們也就知道在做事務(wù)切面配置時(shí)的一些注意事項(xiàng),例如JDK代理時(shí)方法必須是public,CGLIB代理時(shí)必須是public、protected,且類不能是final的;在依賴注入時(shí),如果屬性類型定義為實(shí)現(xiàn)類,JDK代理時(shí)會(huì)報(bào)如下注入異常:
org.springframework.beans.factory.UnsatisfiedDependencyException:?Error?creating?bean?with?name?'com.wwb.test.TxTestAop':?Unsatisfied?dependency?expressed?through?field?'service';?nested?exception?is?org.springframework.beans.factory.BeanNotOfRequiredTypeException:?Bean?named?'stockService'?is?expected?to?be?of?type?'com.wwb.service.StockProcessServiceImpl'?but?was?actually?of?type?'com.sun.proxy.$Proxy14'
但如果修改為CGLIB代理時(shí)則會(huì)成功注入,所以如果有接口,建議注入時(shí)該類屬性都定義為接口。另外事務(wù)切點(diǎn)都配置在實(shí)現(xiàn)類和接口都可以生效,但建議加在實(shí)現(xiàn)類上。
官網(wǎng)關(guān)于Spring AOP的詳細(xì)介紹
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html%23aop
三、Spring事務(wù)的那些坑
通過(guò)之前章節(jié),相信您已經(jīng)掌握了spring事務(wù)的使用方式與原理,不過(guò)還是要注意,因?yàn)橐徊恍⌒木涂赡芫偷艨?。首先看第一個(gè)坑:
3.1 事務(wù)不生效
測(cè)試代碼,事務(wù)AOP配置:
<tx:advice?id="txAdvice"?transaction-manager="myTxManager">
????????<tx:attributes>
????????????
????????????<tx:method?name="openAccount"?isolation="DEFAULT"?propagation="REQUIRED"/>
????????????<tx:method?name="openStock"?isolation="DEFAULT"?propagation="REQUIRED"/>
????????????<tx:method?name="openStockInAnotherDb"?isolation="DEFAULT"?propagation="REQUIRES_NEW"/>
????????????<tx:method?name="openTx"?isolation="DEFAULT"?propagation="REQUIRED"/>
????????????<tx:method?name="openWithoutTx"?isolation="DEFAULT"?propagation="NEVER"/>
????????????<tx:method?name="openWithMultiTx"?isolation="DEFAULT"?propagation="REQUIRED"/>
tx:advice>
public?class?StockProcessServiceImpl?implements?IStockProcessService{
@Autowired
?????private?IAccountDao?accountDao;
????@Autowired
?????private?IStockDao?stockDao;
????
????@Override
????public?void?openAccount(String?aname,?double?money)?{
????????accountDao.insertAccount(aname,?money);
????}
????@Override
????public?void?openStock(String?sname,?int?amount)?{
????????stockDao.insertStock(sname,?amount);
????}
????
????@Override
????public?void?openStockInAnotherDb(String?sname,?int?amount)?{
????????stockDao.insertStock(sname,?amount);
}
}
public?void?insertAccount(String?aname,?double?money)?{
????????String?sql?=?"insert?into?account(aname,?balance)?values(?,?)";
????????this.getJdbcTemplate().update(sql,?aname,?money);
????????DbUtils.printDBConnectionInfo("insertAccount",getDataSource());
}?
????public?void?insertStock(String?sname,?int?amount)?{
????????String?sql?=?"insert?into?stock(sname,?count)?values?(?,?)";
????????this.getJdbcTemplate().update(sql?,?sname,?amount);
????????DbUtils.printDBConnectionInfo("insertStock",getDataSource());
}
????public?static?void?printDBConnectionInfo(String?methodName,DataSource?ds)?{
????????Connection?connection?=?DataSourceUtils.getConnection(ds);
????????System.out.println(methodName+"?connection?hashcode="+connection.hashCode());
????}
//調(diào)用同類方法,外圍配置事務(wù)
????public?void?openTx(String?aname,?double?money)?{
????????????openAccount(aname,money);
????????????openStock(aname,11);
????}
1.運(yùn)行輸出:
insertAccount connection hashcode=319558327
insertStock connection hashcode=319558327
//調(diào)用同類方法,外圍未配置事務(wù)
????public?void?openWithoutTx(String?aname,?double?money)?{
????????openAccount(aname,money);
????????????openStock(aname,11);
????}
2.運(yùn)行輸出:
insertAccount connection hashcode=1333810223
insertStock connection hashcode=1623009085
//通過(guò)AopContext.currentProxy()方法獲取代理
@Override
public?void?openWithMultiTx(String?aname,?double?money)?{
openAccount(aname,money);??
openStockInAnotherDb(aname,?11);//傳播級(jí)別為REQUIRES_NEW
}
3.運(yùn)行輸出:
insertAccount connection hashcode=303240439
insertStock connection hashcode=303240439
可以看到2、3測(cè)試方法跟我們事務(wù)預(yù)期并一樣,結(jié)論:調(diào)用方法未配置事務(wù)、本類方法直接調(diào)用,事務(wù)都不生效!
究其原因,還是因?yàn)镾pring的事務(wù)本質(zhì)上是個(gè)代理類,而本類方法直接調(diào)用時(shí)其對(duì)象本身并不是織入事務(wù)的代理,所以事務(wù)切面并未生效。具體可以參見(jiàn)#Spring事務(wù)實(shí)現(xiàn)機(jī)制#章節(jié)。
Spring也提供了判斷是否為代理的方法:
public?static?void?printProxyInfo(Object?bean)?{
????????System.out.println("isAopProxy"+AopUtils.isAopProxy(bean));
????????System.out.println("isCGLIBProxy="+AopUtils.isCGLIBProxy(bean));
????????System.out.println("isJdkProxy="+AopUtils.isJdkDynamicProxy(bean));
????}
那如何修改為代理類調(diào)用呢?最直接的想法是注入自身,代碼如下:
????@Autowired
????private?IStockProcessService?stockProcessService;
//注入自身類,循環(huán)依賴,親測(cè)可以?
????public?void?openTx(String?aname,?double?money)?{
????????????stockProcessService.openAccount(aname,money);
????????????stockProcessService.openStockInAnotherDb?(aname,11);
????}
當(dāng)然Spring提供了獲取當(dāng)前代理的方法:代碼如下:
//通過(guò)AopContext.currentProxy()方法獲取代理
????@Override
????public?void?openWithMultiTx(String?aname,?double?money)?{
((IStockProcessService)AopContext.currentProxy()).openAccount(aname,money);
((IStockProcessService)AopContext.currentProxy()).openStockInAnotherDb(aname,?11);
????}
另外Spring是通過(guò)TransactionSynchronizationManager類中線程變量來(lái)獲取事務(wù)中數(shù)據(jù)庫(kù)連接,所以如果是多線程調(diào)用或者繞過(guò)Spring獲取數(shù)據(jù)庫(kù)連接,都會(huì)導(dǎo)致Spring事務(wù)配置失效。
最后Spring事務(wù)配置失效的場(chǎng)景:
事務(wù)切面未配置正確
本類方法調(diào)用
多線程調(diào)用
繞開(kāi)Spring獲取數(shù)據(jù)庫(kù)連接
接下來(lái)我們看下Spring的事務(wù)的另外一個(gè)坑:
3.2 事務(wù)不回滾
測(cè)試代碼:
<tx:advice?id="txAdvice"?transaction-manager="myTxManager">
????????<tx:attributes>
????????????
????????????<tx:method?name="buyStock"?isolation="DEFAULT"?propagation="REQUIRED"/>
????????tx:attributes>
tx:advice>
public?void?buyStock(String?aname,?double?money,?String?sname,?int?amount)?throws?StockException?{
????????boolean?isBuy?=?true;
????????accountDao.updateAccount(aname,?money,?isBuy);
????????//?故意拋出異常
????????if?(true)?{
????????????throw?new?StockException("購(gòu)買股票異常");
????????}
????????stockDao.updateStock(sname,?amount,?isBuy);
????}
????@Test
????public?void?testBuyStock()?{
????????try?{
????????????service.openAccount("dcbs",?10000);
????????????service.buyStock("dcbs",?2000,?"dap",?5);
????????}?catch?(StockException?e)?{
????????????e.printStackTrace();
????????}
????????double?accountBalance?=?service.queryAccountBalance("dcbs");
????????System.out.println("account?balance?is?"?+?accountBalance);
????}
輸出結(jié)果:
insertAccount connection hashcode=656479172
updateAccount connection hashcode=517355658
account balance is 8000.0
應(yīng)用拋出異常,但accountDao.updateAccount卻進(jìn)行了提交。究其原因,直接看Spring源代碼:
TransactionAspectSupport
protected?void?completeTransactionAfterThrowing(TransactionInfo?txInfo,?Throwable?ex)?{
????????if?(txInfo?!=?null?&&?txInfo.hasTransaction())?{
????????????if?(logger.isTraceEnabled())?{
????????????????logger.trace("Completing?transaction?for?["?+?txInfo.getJoinpointIdentification()?+
????????????????????????"]?after?exception:?"?+?ex);
????????????}
????????????if?(txInfo.transactionAttribute.rollbackOn(ex))?{
????????????????try?{
????????????????????txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
????????????????}
????????????????catch?(TransactionSystemException?ex2)?{
????????????????????logger.error("Application?exception?overridden?by?rollback?exception",?ex);
????????????????????ex2.initApplicationException(ex);
????????????????????throw?ex2;
????????????????}
????????????????…
}
public?class?DefaultTransactionAttribute?extends?DefaultTransactionDefinition?implements?TransactionAttribute?{
@Override
????public?boolean?rollbackOn(Throwable?ex)?{
????????return?(ex?instanceof?RuntimeException?||?ex?instanceof?Error);
????}
…
}
由代碼可見(jiàn),Spring事務(wù)默認(rèn)只對(duì)RuntimeException和Error進(jìn)行回滾,如果應(yīng)用需要對(duì)指定的異常類進(jìn)行回滾,可配置rollback-for=屬性,例如:
????
????<tx:advice?id="txAdvice"?transaction-manager="myTxManager">
????????<tx:attributes>
????????????
????????????<tx:method?name="buyStock"?isolation="DEFAULT"?propagation="REQUIRED"?rollback-for="StockException"/>
????????tx:attributes>
????tx:advice>
事務(wù)不回滾的原因:
事務(wù)配置切面未生效
應(yīng)用方法中將異常捕獲
拋出的異常不屬于運(yùn)行時(shí)異常(例如IOException),
rollback-for屬性配置不正確
接下來(lái)我們看下Spring事務(wù)的第三個(gè)坑:
3.3 事務(wù)超時(shí)不生效
測(cè)試代碼:
????<tx:advice?id="txAdvice"?transaction-manager="myTxManager">
????????<tx:attributes>
?????????????<tx:method?name="openAccountForLongTime"?isolation="DEFAULT"?propagation="REQUIRED"?timeout="3"/>
????????tx:attributes>
????tx:advice>
@Override
????public?void?openAccountForLongTime(String?aname,?double?money)?{
????????accountDao.insertAccount(aname,?money);
????????try?{
????????????Thread.sleep(5000L);//在數(shù)據(jù)庫(kù)操作之后超時(shí)
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????}
????@Test
????public?void?testTimeout()?{
????????service.openAccountForLongTime("dcbs",?10000);
????}
正常運(yùn)行,事務(wù)超時(shí)未生效
public?void?openAccountForLongTime(String?aname,?double?money)?{
????????try?{
????????????Thread.sleep(5000L);?//在數(shù)據(jù)庫(kù)操作之前超時(shí)
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????accountDao.insertAccount(aname,?money);
????}
拋出事務(wù)超時(shí)異常,超時(shí)生效
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Nov 23 17:03:02 CST 2018
at org.springframework.transaction.support.ResourceHolderSupport.checkTransactionTimeout(ResourceHolderSupport.java:141)
…
通過(guò)源碼看看Spring事務(wù)超時(shí)的判斷機(jī)制:
ResourceHolderSupport
/**
?????*?Return?the?time?to?live?for?this?object?in?milliseconds.
?????*?@return?number?of?millseconds?until?expiration
?????*?@throws?TransactionTimedOutException?if?the?deadline?has?already?been?reached
?????*/
????public?long?getTimeToLiveInMillis()?throws?TransactionTimedOutException{
????????if?(this.deadline?==?null)?{
????????????throw?new?IllegalStateException("No?timeout?specified?for?this?resource?holder");
????????}
????????long?timeToLive?=?this.deadline.getTime()?-?System.currentTimeMillis();
????????checkTransactionTimeout(timeToLive?<=?0);
????????return?timeToLive;
????}
????/**
?????*?Set?the?transaction?rollback-only?if?the?deadline?has?been?reached,
?????*?and?throw?a?TransactionTimedOutException.
?????*/
????private?void?checkTransactionTimeout(boolean?deadlineReached)?throws?TransactionTimedOutException?{
????????if?(deadlineReached)?{
????????????setRollbackOnly();
????????????throw?new?TransactionTimedOutException("Transaction?timed?out:?deadline?was?"?+?this.deadline);
????????}
????}
通過(guò)查看getTimeToLiveInMillis方法的Call Hierarchy,可以看到被DataSourceUtils的applyTimeout所調(diào)用, 繼續(xù)看applyTimeout的Call Hierarchy,可以看到有兩處調(diào)用,一個(gè)是JdbcTemplate,一個(gè)是TransactionAwareInvocationHandler類,后者是只有TransactionAwareDataSourceProxy類調(diào)用,該類為DataSource的事務(wù)代理類,我們一般并不會(huì)用到。難道超時(shí)只能在這調(diào)用JdbcTemplate中生效?寫代碼親測(cè):
????
????<tx:advice?id="txAdvice"?transaction-manager="myTxManager">
????????<tx:attributes>
????????????<tx:method?name="openAccountForLongTimeWithoutJdbcTemplate"?isolation="DEFAULT"?propagation="REQUIRED"?timeout="3"/>
????????tx:attributes>
????tx:advice>
????public?void?openAccountForLongTimeWithoutJdbcTemplate(String?aname,?double?money)?{
????????try?{
????????????Thread.sleep(5000L);
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????accountDao.queryAccountBalanceWithoutJdbcTemplate(aname);
????}
????public?double?queryAccountBalanceWithoutJdbcTemplate(String?aname)?{
???????????String?sql?=?"select?balance?from?account?where?aname?=??";
???????????PreparedStatement?prepareStatement;
????????try?{
????????????prepareStatement?=?this.getConnection().prepareStatement(sql);
???????????????prepareStatement.setString(1,?aname);
???????????????ResultSet?executeQuery?=?prepareStatement.executeQuery();
???????????????while(executeQuery.next())?{
???????????????????return?executeQuery.getDouble(1);
???????????????}
????????}?catch?(CannotGetJdbcConnectionException?|?SQLException?e)?{
????????????//?TODO?Auto-generated?catch?block
????????????e.printStackTrace();
????????}
????????return?0;
????}
運(yùn)行正常,事務(wù)超時(shí)失效
由上可見(jiàn):Spring事務(wù)超時(shí)判斷在通過(guò)JdbcTemplate的數(shù)據(jù)庫(kù)操作時(shí),所以如果超時(shí)后未有JdbcTemplate方法調(diào)用,則無(wú)法準(zhǔn)確判斷超時(shí)。另外也可以得知,如果通過(guò)Mybatis等操作數(shù)據(jù)庫(kù),Spring的事務(wù)超時(shí)是無(wú)效的。鑒于此,Spring的事務(wù)超時(shí)謹(jǐn)慎使用。
四、 總結(jié)
JDBC規(guī)范中Connection 的setAutoCommit是原生控制手動(dòng)事務(wù)的方法,但傳播行為、異?;貪L、連接管理等很多技術(shù)問(wèn)題都需要開(kāi)發(fā)者自己處理,而Spring事務(wù)通過(guò)AOP方式非常優(yōu)雅的屏蔽了這些技術(shù)復(fù)雜度,使得事務(wù)管理變的異常簡(jiǎn)單。
但凡事有利弊,如果對(duì)實(shí)現(xiàn)機(jī)制理解不透徹,很容易掉坑里。最后總結(jié)下Spring事務(wù)的可能踩的坑:
1. ?Spring事務(wù)未生效
調(diào)用方法本身未正確配置事務(wù) 本類方法直接調(diào)用 數(shù)據(jù)庫(kù)操作未通過(guò)Spring的DataSourceUtils獲取Connection 多線程調(diào)用
2. ?Spring事務(wù)回滾失效
未準(zhǔn)確配置rollback-for屬性 異常類不屬于RuntimeException與Error 應(yīng)用捕獲了異常未拋出
3. ?Spring事務(wù)超時(shí)不準(zhǔn)確或失效
超時(shí)發(fā)生在最后一次JdbcTemplate操作之后 通過(guò)非JdbcTemplate操作數(shù)據(jù)庫(kù),例如Mybatis
更多精彩推薦
??外包程序員入職螞蟻金服被質(zhì)疑,網(wǎng)友:人生污點(diǎn)??前后端分離三連問(wèn):為何分離?如何分離?分離后的接口規(guī)范???如何設(shè)計(jì)一個(gè)通用的權(quán)限管理系統(tǒng)??去一家小公司從0到1搭建后端架構(gòu),做個(gè)總結(jié)!??這應(yīng)該是全網(wǎng)最全的Git分支開(kāi)發(fā)規(guī)范手冊(cè)~
最后,推薦給大家一個(gè)有趣有料的公眾號(hào):寫代碼的渣渣鵬,7年老程序員教你寫bug,回復(fù) 面試或資源 送一你整套開(kāi)發(fā)筆記 有驚喜哦

