在 Spring 应用中,@Transactional 是事务管理的核心注解。它通过 AOP 在方法执行前后织入事务控制代码,让开发者可以用一行注解替代数十行事务模板代码。然而,真正用好这个注解,必须对其每一个参数的含义与适用场景有清晰的认识。本节将逐一详解 @Transactional 的所有关键属性,并结合实际案例给出使用建议。
10.1.1 注解概览
@Transactional 可以标注在类或方法上。标注在类上时,对该类中所有 public 方法生效;标注在方法上时,方法级配置覆盖类级配置。其完整签名如下(省略部分元注解):
public @interface Transactional {
String value() default "";
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
下面按实际使用频率和重要性分别解析。
10.1.2 事务传播行为(propagation)
传播行为定义了当前方法被调用时,如果已经存在一个事务,应该如何处理。它由 Propagation 枚举指定,共七种取值。
1. REQUIRED(默认)
- 含义:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。
- 适用场景:绝大多数增删改操作。例如订单服务调用库存服务,两者都标注 REQUIRED,当订单服务发起调用时,库存操作会无缝参与到同一事务中,任何一步失败都导致整体回滚。
2. REQUIRES_NEW
- 含义:无论当前是否存在事务,都创建一个新事务。如果当前有事务,则将原事务挂起。
- 适用场景:需要独立于外部事务提交的业务,如记录操作日志。即使主业务回滚,日志记录也应保留,因此需要独立事务。使用时注意,由于内外事务互不相干,可能引发数据不一致(例如日志记录成功但主业务失败),需结合业务场景评估。
3. SUPPORTS
- 含义:如果当前存在事务,则加入;如果没有事务,则以非事务方式执行。
- 适用场景:纯查询方法。查询在只读事务中可获得一致性快照,但无事务时也能正常运行,不会因缺少事务而报错。
4. NOT_SUPPORTED
- 含义:以非事务方式执行,如果当前存在事务,则将事务挂起。
- 适用场景:某段逻辑明确不需要事务,且不希望受到外层事务影响,例如调用第三方接口发送短信(避免因数据库事务未提交而不发送)。
5. MANDATORY
- 含义:必须在一个已存在的事务中运行,否则抛出异常。
- 适用场景:强制要求调用者必须提供事务上下文,例如某些核心业务操作,不允许无事务执行。
6. NEVER
- 含义:必须以非事务方式执行,如果当前存在事务则抛出异常。
- 适用场景:极少数需要排除事务干扰的场景,很少使用。
7. NESTED
- 含义:如果当前存在事务,则在嵌套事务内执行;如果没有事务,则创建事务(效果同 REQUIRED)。嵌套事务有一个重要特性:可以单独回滚到某个保存点而不影响外部事务。
- 适用场景:需要部分回滚的场景,例如批量处理中某一条记录失败不影响整体。注意,NESTED 需要底层数据库支持保存点(Savepoint),例如 MySQL 的 InnoDB 引擎。
10.1.3 事务隔离级别(isolation)
隔离级别定义了事务之间的可见性和数据冲突解决策略,直接影响数据一致性与并发性能。Isolation 枚举取值如下:
1. DEFAULT
- 含义:使用底层数据库默认的隔离级别。MySQL InnoDB 默认为
REPEATABLE_READ,Oracle/PostgreSQL 默认为READ_COMMITTED。 - 建议:除非有明确理由,一般保持 DEFAULT,让数据库决定。
2. READ_UNCOMMITTED
- 含义:最低级别,允许读取未提交的数据(脏读)。性能最高,但数据一致性最差。
- 适用场景:几乎不在事务场景中使用,仅用于对一致性无要求、极度追求性能的历史统计等。
3. READ_COMMITTED
- 含义:禁止脏读,但允许不可重复读和幻读。即同一事务内多次读取同一行数据,可能因其他事务提交修改而出现不同结果。
- 适用场景:大多数需要一定并发性能且对不可重复读不敏感的业务,如 Oracle 默认使用的该级别。
4. REPEATABLE_READ
- 含义:禁止脏读和不可重复读,但可能出现幻读(InnoDB 通过间隙锁在一定程度上解决了幻读)。同一事务内多次读取同一行数据,结果始终一致。
- 适用场景:对数据一致性要求较高,如金融对账、库存扣减等。MySQL 默认级别。
5. SERIALIZABLE
- 含义:最高隔离级别,完全避免脏读、不可重复读和幻读。事务串行执行,并发性能急剧下降。
- 适用场景:极严格的场景,如银行转账一致性要求,且并发量不大。
实战中,多数场景在 READ_COMMITTED 和 REPEATABLE_READ 之间选择,很少显式指定 SERIALIZABLE。
10.1.4 超时时间(timeout)
- 含义:事务允许执行的最长时间,单位为秒。一旦超时,事务会强制回滚并抛出
TransactionTimedOutException。 - 默认值:
TransactionDefinition.TIMEOUT_DEFAULT即 -1,表示使用底层数据库或事务管理器的默认超时(通常为永不超时)。 - 使用建议:对于可能长时间执行的批量操作或外部调用,应设置一个合理的时间限制,防止数据库锁被长期占用,影响其他业务。例如:
@Transactional(timeout = 10)
public void batchProcess() {
// 限制 10 秒内完成
}
10.1.5 只读标志(readOnly)
- 含义:标记事务是否为只读。设置为
true时,可以提示底层数据库进行性能优化(如 MySQL 的只读事务不记录回滚段),同时对持久化上下文的 flush 模式也有影响(Hibernate 中 readOnly 会禁用脏检查)。 - 适用场景:纯查询方法、报表生成等。注意:如果将
readOnly = true标注在含有 update/insert 的方法上,部分数据库会直接报错,或导致不可预期的行为。
10.1.6 回滚策略(rollbackFor / noRollbackFor)
这是 Spring 事务中最容易产生误解的参数,直接关系到事务在什么情况下回滚。
1. rollbackFor 和 rollbackForClassName
- 含义:指定哪些异常(及其子类)必须触发事务回滚。
- 默认行为:Spring 对 运行时异常(RuntimeException) 和 未检查异常(Error) 自动回滚,但对 受检异常(checked exception) 默认不回滚。这正是许多初学者遇到“抛出异常但事务没回滚”的根源。
示例:
@Transactional(rollbackFor = Exception.class)
public void createOrder() throws OrderException {
// 任何异常(包括受检异常)都会回滚
}
如果业务中需要将受检异常也纳入回滚范围,应显式设置 rollbackFor = Exception.class。这是一种常见且安全的做法,将所有异常都视为回滚信号。
2. noRollbackFor 和 noRollbackForClassName
- 含义:指定哪些异常不触发回滚,即使它们属于默认回滚的类型。
- 场景:某些业务逻辑中,抛出的异常虽然继承自 RuntimeException,但不希望事务回滚。例如:
@Transactional(noRollbackFor = {BusinessWarning.class})
public void process() {
// BusinessWarning 抛出时仍然提交事务
}
10.1.7 事务管理器指定(value / transactionManager)
当应用配置了多个事务管理器(例如同时使用 JDBC 和 JTA,或同时操作两个不同数据源)时,需要指明当前注解使用哪一个。
@Transactional("orderTransactionManager")
public void updateOrder() { ... }
在单一数据源场景下,Spring Boot 自动配置的 TransactionManager 无需额外指定。此参数在多数据源项目中至关重要,为每个数据源定义独立的事务管理器,并在方法上精确标注。
10.1.8 实践中的注意事项与常见陷阱
- 仅对 public 方法生效
@Transactional 通过 Spring AOP 代理实现,静态代理和动态代理都只对 public 方法增强。在 private 或 protected 方法上标注不会生效。如需对非 public 方法应用事务,应通过 AspectJ 编译时织入,或重构代码结构。
- 同类方法调用事务失效
同一个类内部,一个没有 @Transactional 的方法直接调用另一个有 @Transactional 的方法,不会触发代理,因此事务不会生效。这是因为调用时 this 指向目标对象而非代理对象。解决方案:将事务方法抽取到另一个 Bean 中,或通过 AopContext.currentProxy() 获取代理后调用,或使用 @EnableAspectJAutoProxy(exposeProxy = true)。
- 正确理解 rollbackFor 默认值
务必记住:受检异常默认不回滚。建议企业在编码规范中约定,所有 @Transactional 一律添加 rollbackFor = Exception.class,除非有特殊原因需要受检异常时提交。
- 只读事务的性能收益
在 Spring Data JPA 中,readOnly = true 会关闭持久化上下文的脏检查,减少内存消耗。对纯查询的 Controller 或 Service 方法,建议标注以优化性能。
- 与隔离级别的配合
对于高并发下的库存扣减、账户扣款,不能仅依赖事务隔离级别,还需结合乐观锁(@Version)或数据库悲观锁(select ... for update)。事务仅仅保证一系列操作的原子性,不解决所有并发问题。
- 嵌套事务与保存点
使用 NESTED 时,确保底层 PlatformTransactionManager 支持嵌套事务(如 DataSourceTransactionManager)。JPA 的 JpaTransactionManager 默认也支持嵌套,但底层数据库必须支持保存点。
10.1.9 总结
@Transactional 将原本复杂的事务控制简化为几个可组合的参数,但“简洁”不等于“简单”。实际开发中,事务失效、回滚不符合预期、性能瓶颈等大多源自对这些参数的理解偏差。建议读者将本节作为查手册的资料收藏,每次编写事务代码时,并不仅仅写上 @Transactional 了事,而是有意识地思考:这个操作的传播行为是否合适?是否需要独立的日志事务?哪些异常必须回滚?是否需要只读优化?只有将每个参数都放到业务场景下权衡,才能真正驾驭 Spring 的事务管理机制。