Spring 九大事务失效场景

2023-11-17 16:52:01 浏览数 (1)

1、九大事务失效场景

首先来看看事务的具体执行过程

1.1、数据库不支持事务

像 MySQL 数据库中的 MyISAM 引擎就不支持事务。

1.3、被代理类没有被 Spring 所管理

从源码的位置 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization

代码语言:less复制
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,这个连初始化的机会都没有。

比如在后置处理器中依赖注入:

代码语言:java复制
@Component
public class TestBeanPostProcessor implements BeanPostProcessor, Ordered {
 
    @Autowired
    private IAiExtractService aiExtractService;
 
    @Override
    public int getOrder() {
        return 1;
    }
}

注意:这个自定义后置处理器要在 AbstractAutoProxyCreator 后置处理器前执行才会出现这个问题,而 AbstractAutoProxyCreator 就只实现了 BeanPostProcessor,执行的顺序算是靠后了。

为什么在 AbstractAutoProxyCreator 之前依赖注入就会代理失效呢?因为 Spring 是先将后置处理器注册后再初始化剩余的 Bean 的,而在注册后置处理器的过程中依赖注入会初始化 Bean,而初始化会调用容器中现存的后置处理器,还没注册后置处理器自然就不会被代理了。

1.5、标注事务注解的起点不在抛出异常的范围内

可能有些人会有这个疑问,为什么事务的注解一般标注在 service 层而不是 controller 层,难道标注在控制层会事务失效?

no no no,因为一般业务逻辑都是放置在 service 层的,而从方法的调用开始,标注事务注解就是事务有效范围的开始,而因为大部分逻辑都在 service 层,controller 基本上就一行代码,一般不会抛异常,自然注解标注在 controller 和 service 就差不多了。

最最重要的是 service 层的代码可能会提供公共调用,如果我的上级没有事务注解,而我的注解又在 controller 层,service 层抛出的异常就没办法回滚了。

1.6、事务的起点是被 this 调用的,没有真正的去调用代理类

假设有段代码是这样的

代码语言:c#复制
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 这个方法,这个是执行事务的具体代码。

代码语言:scss复制
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、抛出的异常与事务能够处理的异常不匹配

接着上面的代码:

代码语言:typescript复制
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、未配置事务管理器

还是一种就是不正确的使用传播行为导致的事务失效,我们在下一节具体分析。

最后再补充一个小点点,大家觉得以下的事务会失效吗?

代码语言:less复制
 @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 的事务所处理。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞