Spring 九大事务失效场景分析
1、九大事务失效场景
1.1、数据库不支持事务
像 MySQL 数据库中的 MyISAM 引擎就不支持事务。
1.3、被代理类没有被 Spring 所管理
从源码的位置 org.springframework.aop.framework.***.Abstract***Creator#postProcessAfterInitialization
<pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; }
可以看出 Spring 创建代理是需要获取容器中的 Bean 的,如果没有被 Spring 所管理,那自然就创建不了代理,事务自然也就不会生效了。
1.4、被代理的类过早实例化
如果 Bean 过早的进行实例化,那么在执行初始化的过程可能就不能被代理后置处理器处理。比如在 BeanDefinitionRegistryPostProcessor 实现类中使用 beanFactory.registerSingleton("test",new Test()); 注册 Bean,这个连初始化的机会都没有。
比如在后置处理器中依赖注入:
<pre class="prettyprint hljs java" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">@Component public class TestBeanPostProcessor implements BeanPostProcessor, Ordered { @Autowired private IAiExtractService aiExtractService; @Override public int getOrder() { return 1; } }
注意:这个自定义后置处理器要在 Abstract***Creator 后置处理器前执行才会出现这个问题,而 Abstract***Creator 就只实现了 BeanPostProcessor,执行的顺序算是靠后了。
为什么在 Abstract***Creator 之前依赖注入就会代理失效呢?因为 Spring 是先将后置处理器注册后再初始化剩余的 Bean 的,而在注册后置处理器的过程中依赖注入会初始化 Bean,而初始化会调用容器中现存的后置处理器,还没注册后置处理器自然就不会被代理了。
1.5、标注事务注解的起点不在抛出异常的范围内
可能有些人会有这个疑问,为什么事务的注解一般标注在 service 层而不是 controller 层,难道标注在控制层会事务失效?
no no no,因为一般业务逻辑都是放置在 service 层的,而从方法的调用开始,标注事务注解就是事务有效范围的开始,而因为大部分逻辑都在 service 层,controller 基本上就一行代码,一般不会抛异常,自然注解标注在 controller 和 service 就差不多了。
最最重要的是 service 层的代码可能会提供公共调用,如果我的上级没有事务注解,而我的注解又在 controller 层,service 层抛出的异常就没办法回滚了。
1.6、事务的起点是被 this 调用的,没有真正的去调用代理类
假设有段代码是这样的
<pre class="prettyprint hljs cpp" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">public void test1(){ test2(); } @Transactional(rollbackFor = Exception.class) public void test2(){ throw new RuntimeException(); }
正常的事务代理是生成的动态代理类中调用目标方法的外层是有 try catch 包裹着的,如果出现异常会执行回滚操作。但是这里在事务的起点是通过 this 去调用目标方法的,也就是使用真实的类去调用目标方法,目标方法出现异常,自然就不能回滚操作了。需要使用注入的方式注入当前对象,然后使用代理类来调用目标方法。
1.7、方法抛出的异常在方法内捕获,没有被事务拦截器所拦截
可以找到 org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction 这个方法,这个是执行事务的具体代码。
<pre class="prettyprint hljs scala" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">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);
这行就是调用目标方法的代码 retVal = invocation.proceedWithInvocation(); 。如果你异常都捕获了,肯定是直接走到 commitTransactionAfterReturning 提交事务了啊。
1.8、抛出的异常与事务能够处理的异常不匹配
接着上面的代码:
<pre class="prettyprint hljs less" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { if (txInfo != null && txInfo.getTransactionStatus() != null) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex); } if (txInfo.transactionAttribute != null && 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; } catch (RuntimeException | Error ex2) { logger.error("Application exception overridden by rollback exception", ex); throw ex2; } } else { // We don't roll back on this exception. // Will still roll back if TransactionStatus.isRollbackOnly() is true. try { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { logger.error("Application exception overridden by commit exception", ex); ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { logger.error("Application exception overridden by commit exception", ex); throw ex2; } } } } public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); }
我们发现它只判断是不是 RuntimeException 或者 Error 的子类,否则的话就直接提交事务了,我们知道除了 RuntimeException 还有平级的异常有 SQLException、IOException,如果是他们的子类异常自然事务就不会回滚了。
所以在阿里的代码规范中就要求必须指名 Exception 的异常回滚类型,因为指定了指定的异常,如果在指定异常的层级下就会回滚.
1.9、未配置事务管理器
还是一种就是不正确的使用传播行为导致的事务失效,我们在下一节具体分析。
最后再补充一个小点点,大家觉得以下的事务会失效吗?
<pre class="prettyprint hljs less" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">@Autowired private ICallbackService callbackService; @Override @Transactional(rollbackFor = Exception.class) public void test1() { callbackService.test2(); } @Override public void test2(){ // 正常插入数据 baseMapper.insert(new CallbackEntity().setContent("").setAccountId(1)); // 插入失败抛出异常 baseMapper.insert(new CallbackEntity()); }
实际上是不会失效的,上面说了,从调用开始 test1 是事务有效的起点,test2 发生异常然后抛出异常,因为是 RuntimeException,异常会不断的往上抛,最终被 test1 的事务所处理。
#java#