《后端程序猿 · Spring事务失效场景》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗

🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

写在前面的话

Spring 事务管理是通过 AOP(面向切面编程)实现的,提供了声明式事务管理的能力。尽管 Spring 提供了强大的事务管理功能,但在某些情况下,事务可能会失效。

推荐文章

《故障复盘 · 记一次事务用法错误导致的大量锁表问题》


Spring 事务失效场景

方法访问修饰符非public

场景描述:

Java 的访问权限主要是:private、default、protected、public,它们的权限则是依次变大。

如果事务方法的访问修饰符是 protected、private 或 default,Spring AOP 代理无法拦截这些方法。

逻辑分析:

AbstractFallbackTransactionAttributeSource类 的 computeTransactionAttribute 方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
	// Don't allow non-public methods, as configured.
	if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
		return null;
	}

    //省略部分代码
}

解决方案:

将事务方法的访问修饰符设置为 public。


方法使用 static

场景描述:

如果事务方法被声明为 static,则 Spring AOP 代理无法拦截该方法。

逻辑分析:

静态方法属于类本身,而不是类的实例。Spring AOP 代理是基于实例的,因此无法对静态方法进行代理。

解决方案:

避免将事务方法声明为 static。


方法使用 final

场景描述:

在 Java 中,final 关键字用于修饰类、方法和变量,表示这些元素不能被修改或重写。

如果事务方法被声明为 final,则 Spring AOP 代理无法拦截该方法。

逻辑分析:

final 方法在编译时会被优化,无法被子类重写。CGLIB 代理是通过子类化来实现的,因此无法代理 final 方法。

例如,CGLIB 代理的实现中,如果方法被标记为 final,则在生成代理类时会跳过这些方法:

public class Enhancer {
    protected void generateMethod(ClassGenerator gen, Method method) {
        // 跳过 final 方法
        if (Modifier.isFinal(method.getModifiers())) {
            return; 
        }
    }
}

解决方案:

避免将事务方法声明为 final。

Tips:上面几个错误很明显,IDEA也会给出相应提示,应该不容易会出现。


同类方法自调用(★)

场景描述:

当一个类中的方法 A 调用同一类中的方法 B 时,如果方法 B 上有事务注解(如 @Transactional),而方法 A 没有,事务可能会失效。

逻辑分析:

如事务注解 @Transactional 是基于动态代理实现的,Spring 采用动态代理(AOP)实现对 bean 的管理和切片,它为我们的每个 class 生成一个代理对象。只有在代理对象之间进行调用时,可以触发切面逻辑。而在同一个 class 中,方法B调用方法A,调用的是原对象的方法,而不通过代理对象,所以 Spring 无法切到这次调用,也就无法通过注解保证事务性了。

友情提示:@Aspectj、@Async,@Transational、@Cacheable 等注解都是基于AOP 实现的,AOP是基于动态代理实现,存在问题差不多。

原理补充:

由于 Spring AOP 采用了动态代理实现,在Spring 容器中的bean(也就是目标对象)会被代理对象代替,代理对象里加入了我们需要的增强逻辑,当调用代理对象的方法时,目标对象的方法就会被拦截。通过调用代理对象的A方法,在其内部会经过切面增强,然后方法被发射到目标对象,在目标对象上执行原有逻辑,如果在原有逻辑中(同类)嵌套调用了B方法,则此时B方法并没有被进行切面增强,因为此时它已经在目标对象内部。而解决方案很好地说明了,将嵌套方法发射到代理对象,这样就完成了切面增强。

解决方案1:

迁移方法,把业务逻辑抽离到另外一个Service,然后正常注入,调用。

这种方式最稳妥,但是改造量可能偏大。

解决方案2:

获取本对象的代理对象,再进行调用,有两种方式:

1、从Bean工厂获取,注入自身,或者通过getBean方式;

2、使用 AopContext.currentProxy() 方式;

Tips:注入自身的问题,由于这种写法基于Spring的三级缓存不会导致循环依赖的问题出现。

解决方案3:

使用编程式事务,自主控制事务的提交和回滚。


异步调用场景(★)

场景描述:

如果你将 @Async 和 @Transactional 注解放在同一个方法上,通常会导致事务失效。这是因为 @Async 注解会导致该方法在一个新的线程中执行,而 Spring 的事务管理是基于代理的,通常只在同一线程中有效。

逻辑分析:

1、代理机制:Spring 的事务管理是通过 AOP(面向切面编程)实现的,通常使用 JDK 动态代理或 CGLIB 代理。当你在一个方法上使用 @Transactional 注解时,Spring 会创建一个代理对象来管理事务。

2、异步执行:当你在同一个方法上使用 @Async 注解时,Spring 会将该方法的调用转发到一个新的线程中执行。由于事务是绑定到调用线程的,而不是代理对象的,因此在新的线程中,事务不会生效。

解决方案1:

将事务和异步调用分开:将 @Transactional 注解放在一个单独的方法上,然后在该方法中调用带有 @Async 注解的方法。

解决方案2:

如果一定要先用异步逻辑,那么可以在异步逻辑中使用编程式事务。

补充:为什么同时加 @Async 和 @Transactional 时,异步生效而不是事务生效?

这主要是因为 Spring 的事务管理机制 和 异步执行机制 的工作原理不同。

1、事务管理机制:Spring 的事务管理是基于 代理 的。当一个方法被 @Transactional 注解标记时,Spring 会为该方法生成一个代理对象。这个代理对象会拦截方法调用,并在方法执行前后进行事务管理操作(例如,开始事务、提交事务、回滚事务)。

2、异步执行机制:Spring 的异步执行机制是基于 线程池 的。当一个方法被 @Async 注解标记时,Spring 会将该方法提交到一个线程池中执行。线程池会创建一个新的线程来执行该方法,而这个新的线程与当前线程是独立的。

当 @Async 和 @Transactional 同时存在时,Spring 会优先处理 @Async 注解。这意味着方法会被提交到线程池中执行,而不是被代理对象拦截。由于事务管理是基于代理的,因此在异步线程中,事务管理机制无法生效。简单来说,事务管理需要在同一个线程中进行,而异步执行会创建新的线程,导致事务管理无法生效。

总之,@Async 注解会将方法提交到线程池中执行,而 @Transactional 注解会为方法生成代理对象。Spring 会优先处理 @Async 注解,因此事务管理机制无法在异步线程中生效。


没有被 Spring 管理

场景描述:

如果一个被 @Transactional 注解的方法被一个非 Spring 管理的类调用,事务也会失效。

逻辑分析:

这个好像不用多说了,SpringAOP都不会触发。

直接检查一下是不是Bean扫描路径不对等问题。


异常类型不符合

场景描述:

默认情况下,Spring 只会对未检查异常(RuntimeException)进行回滚。如果抛出的是检查异常(Exception),事务不会自动回滚,除非在 @Transactional 注解中指定。

逻辑分析:

@Transactional
public void createUser() {
    // 业务逻辑
    throw new IOException(); // 事务不会回滚
}

@Override
public boolean rollbackOn(Throwable ex) {
	return (ex instanceof RuntimeException || ex instanceof Error);
}

解决方案:

按需调整,例如:@Transactional(rollbackFor = Exception.class)


传播行为不当

场景描述:

事务的传播行为决定了方法调用时如何处理事务。如果传播行为设置不当,可能导致事务失效。例如,使用 Propagation.REQUIRES_NEW 会导致新事务的创建,而不是在现有事务中执行。

逻辑分析:

这种情况不能说事务失效,只能说要按自己的需要处理。


其他场景

1、数据库不支持事务(较少);

2、项目没有开启事务能力(较少);

3、开发捕获了异常导致没回滚(常见);

4、未完待续。。。


扩展 · 部分源码分析

模拟一个事务方法调用,当该方法被调用时,Spring AOP 会拦截这个调用,进入下面的流程。

Step1、事务拦截器触发

参考:TransactionInterceptor#invoke

说明:TransactionInterceptor 会调用 invoke 方法,检查方法上是否有 @Transactional 注解。

Step2、获取事务属性

参考:AbstractFallbackTransactionAttributeSource#getTransactionAttribute

说明:getTransactionAttribute 方法来获取事务属性,这里还涉及一个缓存机制,先不管。

Step3、开启事务

参考:TransactionAspectSupport#invokeWithinTransaction

说明:通过 PlatformTransactionManager 的 getTransaction 方法开始一个新事务,这会创建一个 TransactionStatus 对象,表示当前事务的状态。

Step4、执行目标方法

参考:retVal = invocation.proceedWithInvocation()

说明:执行目标方法,操作数据库。

Step5、提交或回滚事务

如果目标方法执行成功,TransactionInterceptor 会调用 commitTransactionAfterReturning/commit 方法提交事务。

如果在执行过程中抛出异常,TransactionInterceptor 会调用 completeTransactionAfterThrowing/rollback 方法回滚事务。

Step6、结束事务

参考:cleanupTransactionInfo

说明:TransactionInterceptor 会清理事务状态,结束事务。


总结陈词

此篇文章介绍了 Spring 事务的常见失效场景,仅供学习参考。

通过本篇文章的分析,可以看到,Spring事务失效的原因,大半部分和SpringAOP原理有关系,如果某些因素导致AOP无法生效或代理类无法操作,则事务随之失效了。从源码分析过程中,也能找到部分事务失效场景对应的代码。

Spring 的事务能讨论的知识点还很多,后续有机会再进行专题补充,

💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战神刘玉栋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值