一.什么时候会发生事务失效?
Spring的@Transactional
注解是实现声明式事务管理的强大工具,但在某些情况下,开发者可能会遇到事务注解失效的问题,导致预期中的事务管理机制不起作用。理解为什么会发生事务注解失效是避免这一问题的关键。以下是事务注解失效的一些常见原因:
1.方法访问级别不当
Spring的事务管理默认要求事务方法必须是public。如果你将一个使用@Transactional
注解的方法设置为private、protected或是package-private,事务代理将无法正常工作,导致事务注解失效。
2.事务方法在同一个类中调用
Spring事务管理是基于代理的。当一个事务方法直接从同一个类的另一个方法内部调用时,由于代理是基于方法调用的外部拦截,这种"自调用"情况会导致事务失效。
@Service
public class MyService {
public void methodA() {
// 调用本类的事务方法
methodB();
}
@Transactional
public void methodB() {
// 执行一些数据库操作
}
}
这种情况下,methodB()
是通过 普通的 Java 方法调用 执行的,而不是通过代理对象调用。也就是说,Spring 的事务拦截器(TransactionInterceptor)无法拦截到这次调用,所以 @Transactional
注解就不会生效,事务就不会被真正开启。
3.异常处理不当
@Transactional
注解默认只对运行时异常(RuntimeException
及其子类)进行回滚。如果方法内部抛出的是检查型异常(Exception
的直接子类),而不是运行时异常,且没有通过@Transactional
的rollbackFor
属性明确指定异常类型,事务将不会回滚,导致事务失效。
❌ 默认行为下,不回滚
@Transactional
public void doSomething() throws Exception {
// 执行一些数据库操作
if (true) {
throw new Exception("受检异常"); // 不会触发事务回滚
}
}
✅ 显式指定回滚异常类型
@Transactional(rollbackFor = Exception.class)
public void doSomething() throws Exception {
// 执行一些数据库操作
if (true) {
throw new Exception("受检异常"); // 事务将回滚
}
}
4. 事务管理器配置错误
在Spring配置中,如果未正确配置事务管理器,或者在多事务管理器的情况下未指定正确的事务管理器,也可能导致@Transactional
注解失效。
5.数据源或持久化框架配置不正确
正确配置数据源和持久化框架(如Hibernate或JPA)对于事务管理至关重要。如果数据源未配置为支持事务的数据源,或者持久化框架的配置不支持当前的事务管理方式,都可能导致事务失效。
6. Spring Bean的错误创建或注入
如果使用@Transactional
注解的类没有被Spring容器管理,即该类的实例不是通过Spring创建的Bean,而是通过new
关键字直接实例化的,那么@Transactional
注解将不会生效,因为Spring无法对这样的实例应用代理和事务管理。
public class OrderService {
@Transactional
public void createOrder() {
// 1. 保存订单
// 2. 抛出异常
}
}
public class Test {
public static void main(String[] args) {
OrderService service = new OrderService(); // 手动new出来
service.createOrder(); // @Transactional 无效,不会开启事务
}
}
7. 事务传播行为配置不当
事务的传播行为指的是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。Spring提供了多种的事务传播行为。
-
REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是常规的传播行为,也是默认的,适用大多数情况。
-
REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起。适用于需要独立事务执行的场景,不受外部事务影响。
-
SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式执行。适用于不需要强制事务的场景,可以在方法执行期间暂停禁用事务。
-
NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则将当前事务挂起。适用于不需要事务支持的场景,可以在方法执行期间暂停禁用事务。
-
MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。适用于必须在事务支持的场景,如果没有事务则会抛出异常。
-
NESTED:如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则创建一个新的事务。嵌套事务是外部事务的一部分,可以独立提交或者回滚。适用于需要在嵌套事务中执行的场景。
-
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。适用于不允许在事务中执行的场景,如果存在事务则会抛出异常。
通过 @Transactional
注解的 propagation 属性来指定事务传播行为。例如,如果一个事务方法被另一个已经在运行中的事务方法调用,并且事务传播行为设置为REQUIRES_NEW,那么原有事务将被挂起,新的事务开始执行。如果对这些行为的理解不正确,可能会导致事务管理复杂化,甚至失效。
二.案例分析
1.案例1
@Transactional
public int insert(Test test) {
testMapper.insert(test);
insert2(test);
return 1;
}
@Transactional(propagation = Propagation.NEVER)
public int insert2(Test test){
return testMapper.insert(test);
}
Propagation.NEVER
的作用是不在事务中执行,如果之前已经存在事务的话,那么直接抛出异常不再执行。上述代码中,我们期望的结果是执行到insert2方法时会抛出已经存在事务的错误,然而实际上是什么错误也没有抛出,这两个insert操作都成功执行了。
问题本质:自调用导致事务失效
在 insert() 中调用 insert2(),这是 “类内部方法之间的调用”(自调用)。这是事务不生效的核心原因。
分析:
其实事务也利用了AOP原理,Spring通过代理对象来实现事务管理。当你标注了 @Transactional注解
,Spring会给你的类创建一个代理类(默认是JDK动态代理或者CGLIB代理)。这个代理类包裹了真实的业务方法,他在调用方法前会先判断是否要开启事务,在方法后提交或回滚事务。
-
方法执行时,其实是执行了事务代理对象的方法
-
事务代理对象的方法中首先会开启事务,获取数据源连接
-
然后再执行代理对象中的
target
也就是普通对象的方法,这里就是执行真正我们的业务 -
然后事务代理对象会判断上述执行过程中有没有出现异常,进而判断是
commit
还是rollback
要让 Propagation.NEVER
生效,你必须通过代理对象调用 insert2() 方法。有两种解决方式:
-
将调用交由 Spring 自己的代理对象执行
-
把 insert2 拆到另一个类中
2.案例2
@RequestMapping("/insert/{id}")
public Integer insert(@PathVariable Long id){
Test test = new Test();
test.setId(id);
test.setColumn1("test1-" + id);
test.setColumn2("test2-" + id);
test.setColumn3("test3-" + id);
test.setColumn4("test4-" + id);
test.setColumn5("test5-" + id);
test.setColumn6("test6-" + id);
test.setNumber(id);
int result = 0;
try {
result = testService.insert(test);
}catch (Exception e) {
log.error("出现异常",e);
}
return result;
}
public int insert(Test test) {
testMapper.insert(test);
int i = 1 / 0;
return 1;
}
大部分人会觉得service的方法已经被try住了,肯定不会被回滚了,然后事实并非如此,在这个案例中依旧能够被回滚。
执行流程:
-
从开启事务获得数据库的connect -> 执行添加操作 -> 根据是否出现异常进行提交或回滚这整个逻辑都在代理对象中的invokeWithinTransaction方法内的
-
try包裹的是service的方法,也就是说try包裹的范围是在invokeWithinTransaction方法的外面,异常依旧能被感知到还是能回滚的
修改后的版本:
@Transactional
public int insert(Test test) {
testMapper.insert(test);
try {
int i = 1 / 0;
} catch (Exception e) {
// 吞掉异常
// 什么也不做
}
return 1;
}
这时候就不会回滚,因为异常被吞了,Spring就感知不到任何异常发生,默认会进行 commit。
三.总结
Spring 的事务机制基于 AOP 代理实现,虽然使用上非常简洁,但也隐藏了许多容易被忽略的陷阱。事务失效的根本原因大多源于对 Spring 代理机制的不了解 或 使用方式不当。本文通过多种典型场景详细剖析了事务失效的常见原因,包括方法访问级别、自调用、异常类型、配置错误等问题,并结合具体案例阐明了事务的执行流程与传播行为。
开发者在使用 @Transactional
时应特别注意:
-
必须通过 Spring容器管理的代理对象 调用事务方法;
-
事务方法应为
public
; -
要避免在事务方法中手动捕获并吞掉异常;
-
在需要对非运行时异常回滚时,使用
rollbackFor
明确指定; -
熟悉事务传播行为,合理设计业务调用关系。
只有深入理解 Spring 事务的原理与限制,才能在实际开发中写出健壮、可控的事务逻辑,避免潜在的数据一致性风险。