Spring 事务的那些坑,都在这里了!
2021-03-11 18:29
标签:empty over jdk动态代理 rup upd while dde 特点 web 基于 TransactionProxyFactoryBean的声明式事务管理 基于 基于 @Transactional 的声明式事务管理 基于事务管理器API 的编程式事务管理 基于TransactionTemplate 的编程式事务管理 目前大部分项目使用的是声明式的后两种: 基于 基于 @Transactional 的方式需要实施事务管理的方法或者类上使用 @Transactional 指定事务规则即可实现事务管理,在Spring Boot中通常也建议使用这种注解方式来标记事务。 二、Spring事务实现机制 github.com/javastacks/… AbstractPlatformTransactionManager docs.spring.io/spring/docs… 三、Spring事务的那些坑 推荐阅读:Spring事务失效的 8 大原因 首先看第一个坑: 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); } } insertAccount connection hashcode=319558327 insertStock connection hashcode=319558327 //调用同类方法,外围未配置事务public void openWithoutTx(String aname, double money) { openAccount(aname,money); openStock(aname,11);} insertAccount connection hashcode=1333810223 insertStock connection hashcode=1623009085 //通过AopContext.currentProxy()方法获取代理@Overridepublic void openWithMultiTx(String aname, double money) { openAccount(aname,money); openStockInAnotherDb(aname, 11);//传播级别为REQUIRES_NEW} insertAccount connection hashcode=303240439 insertStock connection hashcode=303240439 可以看到2、3测试方法跟我们事务预期并一样,结论:调用方法未配置事务、本类方法直接调用,事务都不生效! 事务切面未配置正确 本类方法调用 多线程调用 绕开Spring获取数据库连接 接下来我们看下Spring的事务的另外一个坑: insertAccount connection hashcode=656479172 updateAccount connection hashcode=517355658 account balance is 8000.0 应用抛出异常,但accountDao.updateAccount却进行了提交。究其原因,直接看Spring源代码: 事务配置切面未生效 应用方法中将异常捕获 抛出的异常不属于运行时异常(例如IOException), rollback-for属性配置不正确 接下来我们看下Spring事务的第三个坑: @Overridepublic void openAccountForLongTime(String aname, double money) { accountDao.insertAccount(aname, money); try { Thread.sleep(5000L);//在数据库操作之后超时 } catch (InterruptedException e) { e.printStackTrace(); }}@Testpublic 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事务超时的判断机制: 运行正常,事务超时失效 由上可见:Spring事务超时判断在通过JdbcTemplate的数据库操作时,所以如果超时后未有JdbcTemplate方法调用,则无法准确判断超时。 调用方法本身未正确配置事务 本类方法直接调用 数据库操作未通过Spring的DataSourceUtils获取Connection 多线程调用 未准确配置rollback-for属性 异常类不属于RuntimeException与Error 应用捕获了异常未抛出 超时发生在最后一次JdbcTemplate操作之后 通过非JdbcTemplate操作数据库,例如Mybatis 总结了一些2020年的面试题,这份面试题的包含的模块分为19个模块,分别是: Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM 。 Spring 事务的那些坑,都在这里了! 标签:empty over jdk动态代理 rup upd while dde 特点 web 原文地址:https://blog.51cto.com/15000150/2563024
为了更透彻的说明这些坑,本文分四部分展开阐述:
第一部分简单介绍下Spring事务集成的几种方式;
第二部分结合Spring源代码说明Spring事务的实现原理;
第三部分通过实际测试代码介绍关于Spring事务的坑;
第四部分是对本文的总结。
一、Spring事务管理的几种方式:
Spring事务在具体使用方式上可分为两大类:
接下来我们详细看下Spring事务的源代码,进而了解其工作原理。我们从
@Overridepublic 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
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的事务是通过AOP代理类中的一个Advice(TransactionInterceptor)进行生效的,而传播级别定义了事务与子事务获取连接、事务提交、回滚的具体方式。详解 Java 中的三种代理模式,这篇推荐看下。
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的详细介绍
通过之前章节,相信您已经掌握了spring事务的使用方式与原理,不过还是要注意,因为一不小心就可能就掉坑。
3.1 事务不生效
测试代码,事务AOP配置:
复制代码
1.运行输出:
复制代码
2.运行输出:
复制代码
3.运行输出:
究其原因,还是因为Spring的事务本质上是个代理类,而本类方法直接调用时其对象本身并不是织入事务的代理,所以事务切面并未生效。具体可以关注公众号Java技术栈,在后台回复 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));}
复制代码
那如何修改为代理类调用呢?最直接的想法是注入自身,代码如下:
@Autowiredprivate IStockProcessService stockProcessService;//注入自身类,循环依赖,亲测可以 public void openTx(String aname, double money) { stockProcessService.openAccount(aname,money); stockProcessService.openStockInAnotherDb (aname,11);}
复制代码
当然Spring提供了获取当前代理的方法:代码如下:
//通过AopContext.currentProxy()方法获取代理@Overridepublic void openWithMultiTx(String aname, double money) {((IStockProcessService)AopContext.currentProxy()).openAccount(aname,money);((IStockProcessService)AopContext.currentProxy()).openStockInAnotherDb(aname, 11);}
复制代码
另外Spring是通过TransactionSynchronizationManager类中线程变量来获取事务中数据库连接,所以如果是多线程调用或者绕过Spring获取数据库连接,都会导致Spring事务配置失效。
最后Spring事务配置失效的场景:
3.2 事务不回滚
测试代码:
复制代码
输出结果:
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=属性,例如:
复制代码
事务不回滚的原因:
3.3 事务超时不生效
测试代码:
复制代码
复制代码
抛出事务超时异常,超时生效
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 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中生效?写代码亲测:
复制代码
另外也可以得知,如果通过Mybatis等操作数据库,Spring的事务超时是无效的。鉴于此,Spring的事务超时谨慎使用。
四、 总结
JDBC规范中Connection 的setAutoCommit是原生控制手动事务的方法,但传播行为、异常回滚、连接管理等很多技术问题都需要开发者自己处理,而Spring事务通过AOP方式非常优雅的屏蔽了这些技术复杂度,使得事务管理变的异常简单。
但凡事有利弊,如果对实现机制理解不透彻,很容易掉坑里。最后总结下Spring事务的可能踩的坑:
获取资料以上资料:关注公众号:有故事的程序员,获取学习资料。
记得点个关注+评论哦~
文章标题:Spring 事务的那些坑,都在这里了!
文章链接:http://soscw.com/index.php/essay/63316.html