接手新的代码仓库时,最让人头疼的往往是那些看似简单却埋着深坑的基础组件使用。就比如 @Transactional 这个注解,在很多项目中其用法可谓是五花八门,相当一部分配置甚至直接失效,完全起不到回滚的作用。
下意识地在涉及数据库操作的方法上添加 @Transactional 是个好习惯。但问题在于,很多开发者只是“加上去了事”,只要功能正常跑通,很少有人会专门去验证在异常情况下事务是否真的能正确回滚。这个注解用起来简单,却总能在你意想不到的地方“摆你一道”。本文将这些问题归纳为三类:不必要、不生效和不回滚,并通过具体的代码示例逐一剖析。

不必要
1. 无需事务的业务
在纯查询或仅进行HTTP请求等无写操作的方法上使用 @Transactional 注解,虽然通常不会引发错误,但从编码规范和技术严谨性的角度看,属于画蛇添足,建议移除。
@Transactional
public String testQuery() {
standardBak2Service.getById(1L);
return "testB";
}
2. 事务范围过大
有些开发者为了省事,直接将 @Transactional 注解标注在类或抽象类上。这会导致一个后果:该类(或其子类)中的所有方法都将被事务管理。这不仅增加了不必要的性能开销,也让代码逻辑变得不清晰。最佳实践是按需使用,只为真正需要事务管理的方法添加注解。
@Transactional
public abstract class BaseService {
}
@Slf4j
@Service
public class TestMergeService extends BaseService{
private final TestAService testAService;
public String testMerge() {
testAService.testA();
return "ok";
}
}
需要注意的是,方法级别的事务配置会覆盖类级别的配置。例如,即使在类上配置了只读事务,方法上的 @Transactional 注解也会启用读写事务。
@Transactional(readOnly = true)
public class TestMergeService {
private final TestBService testBService;
private final TestAService testAService;
@Transactional
public String testMerge() {
testAService.testA();
testBService.testB();
return "ok";
}
}
不生效
3. 方法权限问题
切记,不要将 @Transactional 注解用于 private 方法!
@Transactional 的实现依赖于 Spring AOP 的动态代理机制。而 private 方法无法被代理,因此基于代理的AOP增强对其无效,事务自然也不会生效。
@Transactional
private String testMerge() {
testAService.testA();
testBService.testB();
return "ok";
}
那么,如果在一个已开启事务的 public 方法内部调用一个 private 方法,事务会生效吗?答案是:会生效。因为事务的边界是那个 public 方法,其内部的调用都在同一个事务上下文中。
@Transactional
public String testMerge() throws Exception {
ccc();
return "ok";
}
private void ccc() {
testAService.testA();
testBService.testB();
}
4. 被 final、static 修饰的方法
原因与上述类似,被 final 或 static 修饰的方法上使用 @Transactional 同样不会生效。
static 方法属于类而非实例,代理机制无法拦截静态方法。
final 方法无法被子类重写,事务逻辑无法插入。
@Transactional
public static void b() {
}
@Transactional
public final void b() {
}
5. 同类内部方法调用问题
注意,这是 @Transactional 失效的重灾区!
网上常见的说法是:同类内部方法调用不会经过代理,因此事务不生效。这个说法比较片面,需要分情况讨论。
情况一: 外层方法 testMerge() 开启事务,调用同类的非事务方法 a() 和 b()。此时若 b() 抛异常,根据事务传播特性,a() 和 b() 的操作都在 testMerge() 的事务管理下,会一同回滚。
@Transactional
public String testMerge() {
a();
b();
return "ok";
}
public void a() {
standardBakService.save(testAService.buildEntity());
}
public void b() {
standardBak2Service.save(testBService.buildEntity2());
throw new RuntimeException("b error");
}
情况二(常出问题!): testMerge() 方法未开启事务,它调用了非事务方法 a() 和标注了 @Transactional 的方法 b()。当 b() 抛出异常时,a() 和 b() 的事务都不会生效。因为这种调用是通过 this 引用直接进行的,并未经过代理。
public String testMerge() {
a();
b();
return "ok";
}
public void a() {
standardBakService.save(testAService.buildEntity());
}
@Transactional
public void b() {
standardBak2Service.save(testBService.buildEntity2());
throw new RuntimeException("b error");
}
5.1 独立的 Service 类
最直接的解决方案是将需要事务的方法(如 b())剥离到另一个独立的 Service 类中,通过 Spring 注入使用。这是最清晰、最推荐的方式,但可能会增加类的数量。
@Slf4j
@Service
public class TestBService {
@Transactional
public void b() {
standardBak2Service.save(testBService.buildEntity2());
throw new RuntimeException("b error");
}
}
5.2 自注入方式
通过注入自身的代理对象来调用。虽然 Spring 支持这种方式,但它破坏了清晰的依赖关系,可能引发循环依赖问题,需谨慎使用。
@Slf4j
@Service
public class TestMergeService {
@Autowired
private TestMergeService testMergeService;
public String testMerge() {
a();
testMergeService.b(); // 通过注入的代理调用
return "ok";
}
public void a() {
standardBakService.save(testAService.buildEntity());
}
@Transactional
public void b() {
standardBak2Service.save(testBService.buildEntity2());
throw new RuntimeException("b error");
}
}
5.3 手动获取代理对象
使用 AopContext.currentProxy() 手动获取当前类的代理对象,然后通过代理调用方法。这种方式同样能解决问题,但代码略显突兀。
@Slf4j
@Service
public class TestMergeService {
public String testMerge() {
a();
((TestMergeService) AopContext.currentProxy()).b(); // 手动获取代理
return "ok";
}
public void a() {
standardBakService.save(testAService.buildEntity());
}
@Transactional
public void b() {
standardBak2Service.save(testBService.buildEntity2());
throw new RuntimeException("b error");
}
}
6. Bean 未被 Spring 管理
@Transactional 作为 Spring AOP 的一部分,其生效的前提是该类的实例必须由 Spring 容器管理。务必确保类上标注了 @Service、@Component、@Controller 等注解。
@Service // 必须有这个注解
public class TestBService {
@Transactional
public String testB() {
standardBak2Service.save(entity2);
return "testB";
}
}
7. 异步线程调用
在多线程环境下,事务的传播需要特别注意。由于 Spring 的事务上下文是基于 ThreadLocal 实现的,不同线程拥有独立的事务上下文。
看这个例子:testMerge() 开启事务并调用 testA(),同时在新线程中调用 testB(),testB() 中抛异常。
@Transactional
public String testMerge() {
testAService.testA();
new Thread(() -> {
try {
testBService.testB();
} catch (Exception e) {
throw new RuntimeException();
}
}).start();
return "ok";
}
@Transactional
public String testB() {
DeepzeroStandardBak2 entity2 = buildEntity2();
dataImportJob2Service.save(entity2);
throw new RuntimeException("test2");
}
@Transactional
public String testA() {
DeepzeroStandardBak entity = buildEntity();
standardBakService.save(entity);
return "ok";
}
结果: testA() 不回滚,testB() 回滚。
testA() 不回滚:因为主线程未捕获到新线程中抛出的异常。
testB() 回滚:它在新线程中独立运行,拥有自己的事务,异常导致其自身事务回滚。
8. 不支持事务的引擎
这属于基础设施层面的问题。我们常用的 MySQL,其 InnoDB 引擎支持事务,而 MyISAM 引擎则不支持。现代开发中已很少使用 MyISAM,因为其读性能优势已被 Redis、MongoDB 等专业中间件替代。
不回滚
9. 用错传播属性
@Transactional 的 propagation 属性控制事务的传播行为,配置错误是导致不回滚的常见原因。它支持7种传播特性:
| 传播特性 |
说明 |
| REQUIRED |
默认。当前有事务则加入,没有则新建。 |
| MANDATORY |
必须存在当前事务,否则抛异常。 |
| NEVER |
必须在非事务环境下运行,否则抛异常。 |
| REQUIRES_NEW |
新建事务,挂起当前事务(如果存在)。 |
| NESTED |
嵌套事务,依赖于当前事务。 |
| SUPPORTS |
有事务则加入,没有则以非事务运行。 |
| NOT_SUPPORTED |
以非事务运行,挂起当前事务(如果存在)。 |
下面通过案例解析几种关键特性:
REQUIRES_NEW
无论是否存在外部事务,总是新建一个独立事务。外部事务被挂起,两个事务互不影响。
@Transactional
public String testMerge() {
testAService.testA(); // 在外部事务中
testBService.testB(); // 开启一个全新的独立事务
return "ok";
}
// TestBService中
@Transactional(propagation = Propagation.REQUIRES_NEW)
public String testB() {
saveEntity2();
throw new RuntimeException("testB"); // 仅回滚testB自己的事务
}
结果:testB() 的事务回滚,testMerge() 和 testA() 的事务不受影响。
NESTED
在当前事务内创建一个嵌套的“子事务”。子事务是外部事务的一部分,其提交依赖于外部事务。外部事务回滚会导致子事务回滚;但子事务可以单独回滚而不影响外部事务(通过保存点机制)。
@Transactional
public String testMerge() {
testAService.testA();
testBService.testB(); // 嵌套事务
throw new RuntimeException("testMerge"); // 外部事务异常
return "ok";
}
// TestBService中
@Transactional(propagation = Propagation.NESTED)
public String testB() {
saveEntity2();
// throw new RuntimeException("testB"); // 如果这里抛异常,仅回滚子事务
return "ok";
}
结果:由于外部事务 (testMerge) 异常,导致 testA() 和 testB()(即使它正常执行)都回滚。
10. 自己吞了异常
这是导致事务不回滚的最常见原因!在业务代码中使用 try...catch 捕获了异常,却没有重新抛出,Spring 事务管理器就无法感知到异常,从而无法触发回滚。
@Transactional
public String testMerge() {
try {
testAService.testA();
testBService.testB(); // 假设这里抛RuntimeException
} catch (Exception e) {
log.error("testMerge error:{}", e);
// 没有 throw,异常在此被“吞掉”
}
return "ok";
}
解决方案: 在 catch 块中重新抛出 Spring 事务能够处理的异常(如 RuntimeException)。
@Transactional
public String testMerge() {
try {
testAService.testA();
testBService.testB();
} catch (Exception e) {
log.error("testMerge error:{}", e);
throw new RuntimeException(e); // 重新抛出
}
return "ok";
}
注意: 这并非绝对。如果被调用的方法(如 testB())自身也标注了 @Transactional,且传播特性为默认的 REQUIRED,那么它的异常会在其自身方法边界被事务机制捕获。此时,即使外层 catch 不抛出,整个事务(因为 testB() 加入了外层事务)也可能因 testB() 的失败而回滚。但这依赖于正确的事务配置。
11. 事务无法捕获的异常
Spring 事务默认只回滚 RuntimeException 和 Error。对于检查型异常(Checked Exception),如 Exception、SQLException、IOException 等,默认是不回滚的。
@Transactional
public String testMerge() throws Exception {
try {
testAService.testA();
testBService.testB();
} catch (Exception e) {
log.error("testMerge error:{}", e);
throw new Exception(e); // 抛出检查型异常,默认不会导致回滚!
}
return "ok";
}
解决方案: 使用 @Transactional 的 rollbackFor 属性明确指定需要回滚的异常类型。
@Transactional(rollbackFor = Exception.class) // 指定Exception也回滚
public String testMerge() throws Exception {
try {
testAService.testA();
testBService.testB();
} catch (Exception e) {
log.error("testMerge error:{}", e);
throw new Exception(e);
}
return "ok";
}
为了避免因异常类型记忆不清导致的bug,一个稳妥的做法是始终显式设置 rollbackFor = Exception.class 或 Throwable.class。
12. 自定义异常范围问题
业务中常自定义异常,并习惯让其继承 RuntimeException。在使用时,如果 rollbackFor 只指定了自定义异常,而实际抛出的却是其他类型异常,同样会导致不回滚。
@Transactional(rollbackFor = CustomException.class)
public String testMerge() throws Exception {
try {
testAService.testA();
testBService.testB();
} catch (Exception e) {
log.error("testMerge error:{}", e);
throw new Exception(e); // 抛出的是Exception,不是CustomException
}
return "ok";
}
解决方案: 在 catch 块中统一抛出你定义的自定义异常。
@Transactional(rollbackFor = CustomException.class)
public String testMerge() throws Exception {
try {
testAService.testA();
testBService.testB();
} catch (Exception e) {
log.error("testMerge error:{}", e);
throw new CustomException(e); // 统一转换为自定义异常
}
return "ok";
}
13. 嵌套事务中的异常处理
有时,我们希望调用一个可能失败的事务方法,但即使它失败了,也不影响主流程的继续执行。这时就需要在主方法中单独捕获并处理那个子事务的异常,防止其传播。
@Transactional
public String testMerge() {
testAService.testA(); // 主事务操作
try {
testBService.testB(); // 一个可能失败的独立事务操作
} catch (Exception e) {
log.error("子操作B失败,但不影响主流程:{}", e);
// 仅记录日志,不重新抛出异常
}
return "ok"; // testA()的操作被提交,testB()的操作因其自身事务回滚而撤销
}
// TestBService
@Service
public class TestBService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // 关键:使用REQUIRES_NEW
public String testB() {
saveEntity2();
throw new RuntimeException("test2"); // 仅回滚testB自己的事务
}
}
关键点: 要实现这种隔离,被调用的方法(testB())的事务传播属性通常需要设置为 Propagation.REQUIRES_NEW,确保它在一个全新且独立的事务中运行。
总结
@Transactional 注解是 Spring 框架中非常强大且常用的特性,但其生效条件和使用细节也相当多。从 Spring AOP 的代理机制到不同的事务传播行为,再到异常处理,每一个环节都可能成为陷阱。希望本文梳理的这些常见场景能帮助你更透彻地理解 Spring 事务管理,在未来的开发中有效避坑,写出更加健壮可靠的数据访问代码。如果你在实践中遇到了其他诡异的事务问题,欢迎在 云栈社区 的 Java 板块与大家交流讨论。