昨天有朋友在面试时被问到 @Transactional 注解在哪些情况下会失效,一时没能答上来。这确实是日常开发中一个容易踩坑的点。今天,我们就来系统地梳理一下 @Transactional 相关的知识点,并重点分析导致其失效的几种常见场景。
@Transactional 注解大家应该都不陌生,它是 Spring 框架中用于声明式事务管理的关键注解,能确保方法内多个数据库操作具备原子性——要么全部成功,要么全部回滚。然而,如果不了解其工作原理和使用细节,你很可能会发现事务并没有按照预期工作。接下来,我们先从事务的基础概念讲起。
一、Spring 事务管理简介
在系统开发中,事务管理至关重要。Spring 提供了完善的事务管理机制,主要分为编程式事务和声明式事务两种。
- 编程式事务:指在代码中手动管理事务的提交、回滚等操作。这种方式代码侵入性强。
try {
//TODO something
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw new InvoiceApplyException(“异常失败”);
}
- 声明式事务:基于
AOP(面向切面编程)实现,将具体业务逻辑与事务处理代码解耦,代码侵入性低,因此在实际开发中更为常用。声明式事务有两种配置方式:一是基于 XML 配置文件,另一种就是使用 @Transactional 注解。
@Transactional
@GetMapping(“/test”)
public String test() {
int insert = cityInfoDictMapper.insert(cityInfoDict);
}
二、@Transactional 注解详解
1. 注解作用域
@Transactional 可以作用于接口、类和类方法。
- 作用于类:表示该类的所有
public 方法都使用相同的事务属性配置。
- 作用于方法:如果类和方法都配置了
@Transactional,方法上的配置会覆盖类上的配置。
- 作用于接口:不推荐。如果将注解标注在接口上,并且 Spring AOP 使用了 CGLib 动态代理(例如,代理类没有实现接口),将导致
@Transactional 注解失效。
下面是一个同时使用类级别和方法级别注解的例子:
@Transactional
@RestController
@RequestMapping
public class MybatisPlusController {
@Autowired
private CityInfoDictMapper cityInfoDictMapper;
@Transactional(rollbackFor = Exception.class)
@GetMapping(“/test”)
public String test() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setParentCityId(2);
cityInfoDict.setCityName(“2”);
cityInfoDict.setCityLevel(“2”);
cityInfoDict.setCityCode(“2”);
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert + “”;
}
}
2. 注解核心属性
-
propagation(传播行为):默认值为 Propagation.REQUIRED。
REQUIRED:如果当前存在事务,则加入该事务;否则新建一个事务。
SUPPORTS:如果当前存在事务,则加入;否则以非事务方式运行。
MANDATORY:必须在一个已有的事务中运行,否则抛异常。
REQUIRES_NEW:总是新建一个事务,如果当前存在事务,则将其挂起。
NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则将其挂起。
NEVER:以非事务方式运行,如果当前存在事务,则抛异常。
NESTED:如果当前存在事务,则在嵌套事务内执行;否则行为同 REQUIRED。
-
isolation(隔离级别):默认值为 Isolation.DEFAULT,即使用底层数据库的默认隔离级别。其他选项包括 READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。
-
timeout(超时时间):单位为秒,默认值为 -1(永不超时)。超过时间限制事务未完成则自动回滚。
-
readOnly(是否只读):默认 false。设为 true 可优化读取操作。
-
rollbackFor/noRollbackFor(回滚/不回滚的异常类型):用于指定触发回滚或不触发回滚的异常类数组。
三、@Transactional 失效的六大场景分析
了解了基础知识后,我们进入正题,结合代码分析 @Transactional 注解为何会“莫名其妙”地失效。
场景一:注解应用于非 public 方法
如果 @Transactional 注解应用在 protected、private 或包级可见的方法上,事务将不会生效。

失效原因在于 Spring AOP 代理的机制。如上图所示,TransactionInterceptor(事务拦截器)在调用目标方法前后进行拦截。在决定是否启用事务时,会检查目标方法的修饰符。相关源码片段如下:
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don‘t allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
此方法会检查目标方法是否为 public,如果不是则直接返回 null,导致无法获取事务配置信息。
注意:此时事务虽然无效,但程序不会抛出任何异常,这很容易在不知不觉中导致数据不一致问题。
场景二:propagation 属性设置不当
如果错误地配置了以下三种事务传播行为,事务将不会在异常时回滚:
TransactionDefinition.PROPAGATION_SUPPORTS:支持当前事务,不存在则以非事务运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式执行,挂起当前事务。
TransactionDefinition.PROPAGATION_NEVER:以非事务方式执行,存在事务则抛异常。
这属于配置错误导致的失效,需要根据业务逻辑谨慎选择传播行为。合理使用事务传播机制是构建健壮后端 & 架构的关键之一。
场景三:rollbackFor 属性设置错误
Spring 事务默认只对未检查异常(unchecked exceptions,即 RuntimeException 及其子类)和 Error 进行回滚。对于已检查异常(checked exceptions,即 Exception 的子类但非 RuntimeException 子类),默认不会触发回滚。

如果你在事务方法中抛出了一个自定义的已检查异常,并期望事务回滚,就必须通过 rollbackFor 属性来指定。
// 希望自定义的异常可以进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class)
Spring 会判断抛出的异常是否为 rollbackFor 指定异常或其子类,从而决定是否回滚。
场景四:同一类内的方法调用
这是非常高频的一个错误场景。考虑以下情况:类中有方法 A 和方法 B,方法 A 未声明事务,方法 B 声明了 @Transactional。当方法 A 内部直接调用方法 B 时,方法 B 的事务不会生效。
//@Transactional // 注意:A方法本身没有事务注解
@GetMapping(“/test”)
private Integer A() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName(“2”);
/**
* B 插入字段为 3的数据
*/
this.insertB(); // 内部调用
/**
* A 插入字段为 2的数据
*/
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert;
}
@Transactional()
public Integer insertB() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName(“3”);
cityInfoDict.setParentCityId(3);
return cityInfoDictMapper.insert(cityInfoDict);
}
原因:Spring 的事务管理是通过 AOP 代理实现的。只有被代理对象调用的方法,事务拦截器才能介入。当通过 this.insertB() 进行内部调用时,调用的是目标对象自身的方法,而非代理对象的方法,因此事务增强逻辑被绕过。
场景五:异常被 catch 捕获并“吞掉”
这是导致事务失效的最常见原因之一。
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName(“2”);
cityInfoDict.setParentCityId(2);
/**
* A 插入字段为 2的数据
*/
insert = cityInfoDictMapper.insert(cityInfoDict);
/**
* B 插入字段为 3的数据
*/
b.insertB(); // 假设B方法内部抛出了异常
} catch (Exception e) {
e.printStackTrace(); // 仅仅打印,未重新抛出!
}
return insert;
}
如果 b.insertB() 抛出了异常,但被 A() 方法内的 try-catch 捕获并处理(没有重新抛出),那么事务将不会回滚。这是因为 Spring 事务的回滚触发机制依赖于异常是否传播到事务拦截器。
更棘手的情况是,你可能看到 UnexpectedRollbackException 异常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
这通常发生在嵌套方法调用且传播行为为 REQUIRED 时。内层方法(如insertB)抛异常,标记事务需要回滚(rollback-only)。但外层方法(A)捕获了该异常,并试图正常提交事务。这种“一个要回滚,一个要提交”的矛盾状态导致了 UnexpectedRollbackException。
最佳实践:在事务方法中,除非有特殊需求,否则不要轻易捕获异常。如果必须捕获,请确保在 catch 块中抛出 RuntimeException 或通过 @Transactional(rollbackFor=Exception.class) 指定的异常。
场景六:数据库引擎不支持事务
这种情况相对少见,但需知晓。事务能否生效,底层数据库引擎的支持是根本前提。以常用的 MySQL 为例,其默认的 InnoDB 引擎是支持事务的。但如果表使用了 MyISAM 引擎,那么即便代码层面配置了 @Transactional,事务也完全不会生效,因为 MyISAM 引擎本身不支持事务。
总结
@Transactional 注解使用简单,但背后的机制和细节不少。希望通过本文对6种失效场景的剖析,能帮助你更深刻地理解 Java 中声明式事务的工作方式,在开发中有效避坑。事务一致性是数据准确性的基石,务必重视。
在实际工作中遇到其他疑难杂症,也欢迎到云栈社区与更多开发者交流探讨。