第一章:事务管理的演进之路
要理解 @Transactional 的优雅,我们先要看看它解决了什么“痛苦”。
1.1 编程式事务
在 Spring 早期,开发者需要手动控制事务的每一个环节,这被称为编程式事务。它主要依赖 PlatformTransactionManager 和 TransactionTemplate 等API。
方式一:使用 PlatformTransactionManager
这是一种非常底层的实现,代码与业务逻辑高度耦合,事务的生命周期从getTransaction()
开始,到commit()
或rollback()
结束。充满了模板代码:
@Test
public void transactionManagerTest() {
// 1. 定义事务属性(隔离级别、传播行为等)
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 2. 获取事务状态
TransactionStatus status = transactionManager.getTransaction(def);
try {
log.info("开始执行业务操作...");
// 核心业务逻辑
User user = new User().setName("张三");
userService.insertUser(user);
// 模拟异常
int i = 1 / 0;
// 3. 如果一切正常,提交事务
transactionManager.commit(status);
log.info("事务成功提交");
} catch (Exception e) {
// 4. 如果出现异常,回滚事务
transactionManager.rollback(status);
log.error("业务异常,事务已回滚", e);
}
}
痛点分析:
-
代码冗余:每个需要事务的方法都得写一套 try-catch-finally 的模板。
-
高度耦合:事务管理代码(开启、提交、回滚)和核心业务代码(insertUser)混杂在一起,难以维护。
方式二:使用 TransactionTemplate (稍作改进)
Spring 提供了 TransactionTemplate 来简化上述过程,它利用了回调模式,但本质上仍是编程式的。
@Test
public void transactionTemplateTest() {
transactionTemplate.execute(status -> {
try {
log.info("开始执行业务操作...");
User user = new User().setName("张三");
userService.insertUser(user);
// 模拟异常
int i = 1 / 0;
return "成功";
} catch (Exception e) {
log.error("业务异常,手动标记事务为回滚", e);
// 出现异常时,必须手动标记事务状态为“仅回滚”
status.setRollbackOnly();
return "失败";
}
});
}
虽然代码稍微简洁了一些,但业务逻辑依然被包裹在事务模板中,耦合问题没有根本解决。
1.2 优雅的方案:声明式事务 (@Transactional)
为了将开发者从繁琐的事务管理中解放出来,Spring 引入了基于 AOP 的声明式事务。你只需要一个注解,就能优雅地完成所有工作。
Generated java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
// 只需一个注解,Spring 会在方法前后自动处理事务
@Override
@Transactional(rollbackFor = Exception.class)
public void createUserAndThrowException(User user) {
log.info("开始执行业务操作...");
// 核心业务逻辑
userDao.insert(user);
// 模拟异常
int i = 1 / 0;
}
}
优势对比:
-
代码极简:没有一行多余的事务管理代码,专注于业务本身。
-
关注点分离:事务管理(横切关注点)与业务逻辑(核心关注点)完全解耦,符合 AOP 思想,代码更清晰、更易于维护。
现在,你已经直观地感受到了 @Transactional 的强大。接下来,我们深入其内部,看看魔法是如何发生的。
第二章:深入 @Transactional 核心
2.1 工作原理:AOP 与动态代理
@Transactional 的实现原理是 Spring AOP 和 动态代理。
-
代理创建:当 Spring 容器启动时,它会扫描到带有 @Transactional 注解的 Bean。Spring 不会直接返回这个 Bean 的实例,而是为它创建一个代理对象。
-
方法拦截:当外部代码调用这个 Bean 的 public 方法时,实际上是调用了代理对象的方法。
-
事务增强:代理对象就像一个“保安”,它会在调用真实方法前执行“前置操作”(开启事务),在调用真实方法后执行“后置操作”(提交或回滚事务)。
执行流程图:
理解这个代理机制是掌握所有“失效场景”的关键!
2.2 注解的适用范围
-
类 (Class):当注解在类上时,表示该类所有 public 方法都应用相同的事务配置。
-
方法 (Method):当方法和类上都有注解时,方法级别的注解会覆盖类级别的配置。这是更精细的控制方式。
-
接口 (Interface):不推荐使用。虽然技术上可行(如果使用 JDK 动态代理),但这是一种糟糕的实践。因为它会将事务这种实现细节耦合到接口定义中。如果切换到 CGLIB 代理(基于类的代理),接口上的注解会直接失效。
最佳实践:始终将 @Transactional 注解在具体的实现类或其方法上。
2.3 核心属性精讲
@Transactional 提供了丰富的属性来精细控制事务行为。
属性 | 说明 | 常用值/重要说明 |
propagation | 事务传播行为 (最重要)。定义当一个事务方法调用另一个事务方法时,事务如何交互。 | REQUIRED (默认), REQUIRES_NEW, NESTED |
isolation | 事务隔离级别。定义事务并发问题的处理能力(脏读、不可重复读、幻读)。 | READ_COMMITTED (常用), REPEATABLE_READ (MySQL默认) |
readOnly | 只读事务。设置为 true 可进行性能优化,数据库会跳过一些锁定和日志记录。 | true, false (默认) |
timeout | 事务超时 (秒)。超过指定时间未完成,事务将自动回滚。 | 整数值,如 30 |
rollbackFor | 指定回滚的异常。定义哪些异常类型会触发回滚。 | Exception.class (强烈推荐) |
noRollbackFor | 指定不回滚的异常。定义哪些异常类型不会触发回滚。 | 业务自定义的非关键异常 |
propagation (传播行为) 深度解析:
-
REQUIRED (默认):如果当前有事务,就加入;如果没有,就新建一个。这是最常见的场景。
-
REQUIRES_NEW:总是创建一个全新的、独立的事务。如果外部已有事务,会将其挂起。常用于需要独立提交的日志记录等场景。即使内部事务回滚,也不会影响外部事务。
-
NESTED:(注意:与REQUIRED不同!) 如果当前有事务,则创建一个嵌套事务(保存点 Savepoint)。嵌套事务是外部事务的一部分,它回滚不会影响外部事务。但如果外部事务回滚,嵌套事务也必须回滚。这需要数据库驱动支持保存点特性。
rollbackFor (回滚规则) 的关键点:
注意! Spring 默认只在捕获到 RuntimeException (未检查异常) 和 Error 时才会回滚事务。对于 Exception 的子类 (受检异常,如 IOException),默认是不会回滚的!
这是一个巨大的“坑”。为了避免意外,养成良好习惯:
@Transactional(rollbackFor = Exception.class)
这样可以确保任何异常都会触发回滚。
第三章:@Transactional 失效的6大场景与对策
由于其代理原理,@Transactional 在某些情况下会“失效”。
1. 失效场景一:应用在非 public 方法上
-
场景:@Transactional 写在了 private, protected 或 default 权限的方法上。
-
原因:Spring AOP 的动态代理(无论是 JDK 还是 CGLIB)主要是为 public 方法设计的。在事务拦截器的源码中,会检查方法是否为 public,如果不是,则直接跳过,不会应用事务增强。
-
解决方案:始终将 @Transactional 应用于 public 方法。
2. 失效场景二:方法内部调用 (this 调用)
这是最常见也最隐蔽的失效场景。
-
场景:
@Service public class MyService { public void methodA() { // this 调用,绕过了代理对象,导致 methodB 的事务失效! this.methodB(); } @Transactional public void methodB() { // ... 数据库操作 ... } }
-
原因:this 关键字引用的是原始对象,而不是 Spring 创建的代理对象。调用 this.methodB() 直接执行了原始对象的代码,完全绕过了代理的事务拦截逻辑。
-
解决方案 (按推荐度排序):
-
最佳方案:拆分到另一个 Service:将 methodB 移到另一个 Service (如 ServiceB) 中,然后在 MyService 中 @Autowired ServiceB,通过 serviceB.methodB() 调用。这符合单一职责原则,是架构上最清晰的做法。
-
推荐方案:注入自身代理:通过 AopContext.currentProxy() 获取当前方法的代理对象来调用。
// 启动类或配置类上需要加 @EnableAspectJAutoProxy(exposeProxy = true) @Service public class MyService { public void methodA() { // 获取代理对象,通过代理对象调用 ((MyService) AopContext.currentProxy()).methodB(); } @Transactional public void methodB() { /* ... */ } }
-
可行但不推荐:注入自身:@Autowired 注入自己。
@Service public class MyService { @Autowired private MyService self; // 注入自身的代理对象 public void methodA() { self.methodB(); // 通过代理调用 } @Transactional public void methodB() { /* ... */ } }
注意:这可能导致循环依赖问题,尽管 Spring 能解决大部分场景。
-
3. 失效场景三:异常被 try-catch 吞没
-
场景:
Generated java@Transactional(rollbackFor = Exception.class) public void transactionalTest() { try { // ... 数据库操作 ... int i = 1 / 0; } catch (Exception e) { log.error("执行事务异常,但异常被捕获了", e); // 异常被“吃掉”,没有重新抛出,代理对象认为方法正常结束,会提交事务! } }
-
原因:代理对象只能通过捕获从方法中抛出的异常来决定是否回滚。如果异常在方法内部被 catch 块完全处理(吞没),代理就无法感知到任何问题,会按正常流程提交事务。
-
解决方案:在 catch 块中处理完必要逻辑(如记录日志)后,必须将异常重新抛出。
catch (Exception e) { log.error("执行事务异常,需要回滚", e); // 将异常重新抛出,让事务代理可以感知到 throw e; // 或者抛出自定义的运行时异常 // throw new MyBusinessException("操作失败", e); }
4. 失效场景四:rollbackFor 属性设置错误
-
场景:方法中抛出了一个受检异常(如 IOException),但 @Transactional 没有指定 rollbackFor。
-
原因:如前所述,Spring 默认只对 RuntimeException 和 Error 回滚。
-
解决方案:明确指定 rollbackFor = Exception.class 或你期望回滚的具体异常类型。
5. 失效场景五:propagation 属性设置错误
-
场景:希望开启事务,却错误地配置了 propagation。
-
原因:如果传播行为被设置为 NOT_SUPPORTED (以非事务方式运行,若有事务则挂起)、NEVER (以非事务方式运行,若有事务则抛异常) 或 SUPPORTS (有事务就加入,没事务就算了),在外部没有事务的情况下,这些方法本身是不会主动开启事务的。
-
解决方案:确保需要事务的方法使用了正确的传播行为,通常是默认的 REQUIRED。
6. 失效场景六:数据库引擎不支持事务
-
场景:代码完全正确,但事务就是不回滚。
-
原因:事务的最终执行者是数据库。如果你的 MySQL 表使用了 MyISAM 存储引擎,它天生就不支持事务。
-
解决方案:检查你的表结构,确保使用了支持事务的存储引擎,如 InnoDB。
第四章:总结
@Transactional 是一个强大的工具,使用时请遵循以下最佳实践:
-
优先使用声明式事务:它让你的代码更简洁、更专注。
-
注解于具体实现类:将 @Transactional 放在 Service 实现类或其 public 方法上,而非接口。
-
明确回滚规则:为避免意外,建议总是设置 rollbackFor = Exception.class。
-
善用只读事务:对于所有查询操作,添加 @Transactional(readOnly = true) 可以有效提升性能。
-
警惕内部调用:深刻理解代理原理,避免 this 调用导致事务失效,优先选择重构代码。
-
保持事务粒度小:事务方法应只包含必要的数据库操作,避免将耗时的、非事务性的操作(如RPC、文件IO)放入其中,以免长时间占用数据库连接。