📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍
文章目录
- 写在前面的话
- Spring 事务失效场景
- 方法访问修饰符非public
- 方法使用 static
- 方法使用 final
- 同类方法自调用(★)
- 异步调用场景(★)
- 没有被 Spring 管理
- 异常类型不符合
- 传播行为不当
- 其他场景
- 扩展 · 部分源码分析
- 总结陈词
写在前面的话
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 的事务能讨论的知识点还很多,后续有机会再进行专题补充,
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。