您好,登錄后才能下訂單哦!
這篇文章主要講解了“Spring事務有哪些坑”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Spring事務有哪些坑”吧!
一、Spring事務管理的幾種方式:
Spring事務在具體使用方式上可分為兩大類:
1. 聲明式
基于 TransactionProxyFactoryBean的聲明式事務管理
基于 <tx> 和 <aop> 命名空間的事務管理
基于 @Transactional 的聲明式事務管理
2. 編程式
基于事務管理器API 的編程式事務管理
基于TransactionTemplate 的編程式事務管理
目前大部分項目使用的是聲明式的后兩種:
基于 <tx> 和 <aop> 命名空間的聲明式事務管理可以充分利用切點表達式的強大支持,使得管理事務更加靈活。
基于 @Transactional 的方式需要實施事務管理的方法或者類上使用 @Transactional 指定事務規則即可實現事務管理,在Spring Boot中通常也建議使用這種注解方式來標記事務。
二、Spring事務實現機制
接下來我們詳細看下Spring事務的源代碼,進而了解其工作原理。我們從<tx>標簽的解析類開始:
@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事務的核心實現類TransactionInterceptor及其父類TransactionAspectSupport,其實現了事務的開啟、數據庫操作、事務提交、回滾等。我們平時在開發時如果想確定是否在事務中,也可以在該方法進行斷點調試。
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; } }
至此我們了解事務的整個調用流程,但還有一個重要的機制沒分析到,那就是Spring 事務針對不同的傳播級別控制當前獲取的數據庫連接。接下來我們看下Spring獲取連接的工具類DataSourceUtils,JdbcTemplate、Mybatis-Spring也都是通過該類獲取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也是一個事務同步管理的核心類,它實現了事務同步管理的職能,包括記錄當前連接持有connection holder。
TransactionSynchronizationManager
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<Map<Object, Object>>("Transactional resources"); … public static Object getResource(Object key) { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); Object value = doGetResource(actualKey); if (value != null && logger.isTraceEnabled()) { logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); } return value; } /** * Actually check the value of the resource that is bound for the given key. */ private static Object doGetResource(Object actualKey) { Map<Object, Object> map = resources.get(); if (map == null) { return null; } Object value = map.get(actualKey); // Transparently remove ResourceHolder that was marked as void... if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { map.remove(actualKey); // Remove entire ThreadLocal if empty... if (map.isEmpty()) { resources.remove(); } value = null; } return value; }
在事務管理器類AbstractPlatformTransactionManager中,getTransaction獲取事務時,會處理不同的事務傳播行為,例如當前存在事務,但調用方法事務傳播級別為REQUIRES_NEW、PROPAGATION_NOT_SUPPORTED時,對當前事務進行掛起、恢復等操作,以此保證了當前數據庫操作獲取正確的Connection。
具體是在子事務提交的最后會將掛起的事務恢復,恢復時重新調用TransactionSynchronizationManager. bindResource設置之前的connection holder,這樣再獲取的連接就是被恢復的數據庫連接, TransactionSynchronizationManager當前激活的連接只能是一個。
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 suspendsuspendedResources = 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的事務是通過AOP代理類中的一個Advice(TransactionInterceptor)進行生效的,而傳播級別定義了事務與子事務獲取連接、事務提交、回滾的具體方式。
AOP(Aspect Oriented Programming),即面向切面編程。Spring AOP技術實現上其實就是代理類,具體可分為靜態代理和動態代理兩大類,其中靜態代理是指使用 AOP 框架提供的命令進行編譯,從而在編譯階段就可生成 AOP 代理類,因此也稱為編譯時增強;(AspectJ);而動態代理則在運行時借助于 默寫類庫在內存中“臨時”生成 AOP 動態代理類,因此也被稱為運行時增強。其中java是使用的動態代理模式 (JDK+CGLIB)。
JDK動態代理 JDK動態代理主要涉及到java.lang.reflect包中的兩個類:Proxy和InvocationHandler。InvocationHandler是一個接口,通過實現該接口定義橫切邏輯,并通過反射機制調用目標類的代碼,動態將橫切邏輯和業務邏輯編制在一起。Proxy利用InvocationHandler動態創建一個符合某一接口的實例,生成目標類的代理對象。
CGLIB動態代理 CGLIB全稱為Code Generation Library,是一個強大的高性能,高質量的代碼生成類庫,可以在運行期擴展Java類與實現Java接口,CGLIB封裝了asm,可以再運行期動態生成新的class。和JDK動態代理相比較:JDK創建代理有一個限制,就是只能為接口創建代理實例,而對于沒有通過接口定義業務方法的類,則可以通過CGLIB創建動態代理。
CGLIB 創建代理的速度比較慢,但創建代理后運行的速度卻非常快,而 JDK 動態代理正好相反。如果在運行的時候不斷地用 CGLIB 去創建代理,系統的性能會大打折扣。因此如果有接口,Spring默認使用JDK 動態代理,源代碼如下:
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代理的兩種特點后,我們也就知道在做事務切面配置時的一些注意事項,例如JDK代理時方法必須是public,CGLIB代理時必須是public、protected,且類不能是final的;在依賴注入時,如果屬性類型定義為實現類,JDK代理時會報如下注入異常:
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代理時則會成功注入,所以如果有接口,建議注入時該類屬性都定義為接口。另外事務切點都配置在實現類和接口都可以生效,但建議加在實現類上。
官網關于Spring AOP的詳細介紹:https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html%23aop
三、Spring事務的那些坑
通過之前章節,相信您已經掌握了spring事務的使用方式與原理,不過還是要注意,因為一不小心就可能就掉坑。首先看第一個坑:
3.1 事務不生效
測試代碼,事務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()); }
//調用同類方法,外圍配置事務 public void openTx(String aname, double money) { openAccount(aname,money); openStock(aname,11); }
1.運行輸出:
insertAccount connection hashcode=319558327
insertStock connection hashcode=319558327
//調用同類方法,外圍未配置事務 public void openWithoutTx(String aname, double money) { openAccount(aname,money); openStock(aname,11); }
2.運行輸出:
insertAccount connection hashcode=1333810223
insertStock connection hashcode=1623009085
//通過AopContext.currentProxy()方法獲取代理 @Override public void openWithMultiTx(String aname, double money) { openAccount(aname,money); openStockInAnotherDb(aname, 11);//傳播級別為REQUIRES_NEW }
3.運行輸出:
insertAccount connection hashcode=303240439
insertStock connection hashcode=303240439
可以看到2、3測試方法跟我們事務預期并一樣,結論:調用方法未配置事務、本類方法直接調用,事務都不生效!
究其原因,還是因為Spring的事務本質上是個代理類,而本類方法直接調用時其對象本身并不是織入事務的代理,所以事務切面并未生效。具體可以參見#Spring事務實現機制#章節。
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)); }
那如何修改為代理類調用呢?最直接的想法是注入自身,代碼如下:
@Autowired private IStockProcessService stockProcessService; //注入自身類,循環依賴,親測可以 public void openTx(String aname, double money) { stockProcessService.openAccount(aname,money); stockProcessService.openStockInAnotherDb (aname,11); }
當然Spring提供了獲取當前代理的方法:代碼如下:
//通過AopContext.currentProxy()方法獲取代理 @Override public void openWithMultiTx(String aname, double money) { ((IStockProcessService)AopContext.currentProxy()).openAccount(aname,money); ((IStockProcessService)AopContext.currentProxy()).openStockInAnotherDb(aname, 11); }
另外Spring是通過TransactionSynchronizationManager類中線程變量來獲取事務中數據庫連接,所以如果是多線程調用或者繞過Spring獲取數據庫連接,都會導致Spring事務配置失效。
最后Spring事務配置失效的場景:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
事務切面未配置正確
本類方法調用
多線程調用
繞開Spring獲取數據庫連接
接下來我們看下Spring的事務的另外一個坑:
3.2 事務不回滾
測試代碼:
<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("購買股票異常"); } 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); }
輸出結果:
insertAccount connection hashcode=656479172
updateAccount connection hashcode=517355658
account balance is 8000.0
應用拋出異常,但accountDao.updateAccount卻進行了提交。究其原因,直接看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); } … }
由代碼可見,Spring事務默認只對RuntimeException和Error進行回滾,如果應用需要對指定的異常類進行回滾,可配置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>
事務不回滾的原因:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
事務配置切面未生效
應用方法中將異常捕獲
拋出的異常不屬于運行時異常(例如IOException),
rollback-for屬性配置不正確
接下來我們看下Spring事務的第三個坑:
3.3 事務超時不生效
測試代碼:
<!-- 注冊事務通知 --> <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);//在數據庫操作之后超時 } catch (InterruptedException e) { e.printStackTrace(); } }
@Test public void testTimeout() { service.openAccountForLongTime("dcbs", 10000); }
正常運行,事務超時未生效
public void openAccountForLongTime(String aname, double money) { try { Thread.sleep(5000L); //在數據庫操作之前超時 } catch (InterruptedException e) { e.printStackTrace(); } accountDao.insertAccount(aname, money); }
拋出事務超時異常,超時生效
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)
…
通過源碼看看Spring事務超時的判斷機制:
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); } }
通過查看getTimeToLiveInMillis方法的Call Hierarchy,可以看到被DataSourceUtils的applyTimeout所調用, 繼續看applyTimeout的Call Hierarchy,可以看到有兩處調用,一個是JdbcTemplate,一個是TransactionAwareInvocationHandler類,后者是只有TransactionAwareDataSourceProxy類調用,該類為DataSource的事務代理類,我們一般并不會用到。難道超時只能在這調用JdbcTemplate中生效?寫代碼親測:
<!-- 注冊事務通知 --> <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; }
運行正常,事務超時失效
由上可見:Spring事務超時判斷在通過JdbcTemplate的數據庫操作時,所以如果超時后未有JdbcTemplate方法調用,則無法準確判斷超時。另外也可以得知,如果通過Mybatis等操作數據庫,Spring的事務超時是無效的。鑒于此,Spring的事務超時謹慎使用。
四、 總結
JDBC規范中Connection 的setAutoCommit是原生控制手動事務的方法,但傳播行為、異常回滾、連接管理等很多技術問題都需要開發者自己處理,而Spring事務通過AOP方式非常優雅的屏蔽了這些技術復雜度,使得事務管理變的異常簡單。
但凡事有利弊,如果對實現機制理解不透徹,很容易掉坑里。最后總結下Spring事務的可能踩的坑:
1. Spring事務未生效
調用方法本身未正確配置事務
本類方法直接調用
數據庫操作未通過Spring的DataSourceUtils獲取Connection
多線程調用
2. Spring事務回滾失效
未準確配置rollback-for屬性
異常類不屬于RuntimeException與Error
應用捕獲了異常未拋出
3. Spring事務超時不準確或失效
超時發生在最后一次JdbcTemplate操作之后
通過非JdbcTemplate操作數據庫,例如Mybatis
感謝各位的閱讀,以上就是“Spring事務有哪些坑”的內容了,經過本文的學習后,相信大家對Spring事務有哪些坑這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。