【每日一技·Java】——@Transactional
@Transactional 是 Spring 框架中声明式事务管理的核心注解。它的作用可以用一句话概括:让你只需要通过一个注解,就能让一个方法或类中的所有数据库操作,自动在一个事务边界内执行,无需手动编写事务开启、提交、回滚的代码。
下面我将从最基础到最深入的细节,为你全面拆解这个注解。
核心原理:AOP 的经典应用
@Transactional 之所以能“自动”工作,完全依赖于 Spring 的AOP(面向切面编程)。简单来说,Spring 会在运行时为带有 @Transactional 注解的类动态创建一个代理对象。
这个代理对象的工作流程就像一个标准的数据库事务模板:
// Spring AOP 代理为你自动生成的伪代码逻辑
public void 代理方法() {
try {
// 1. 开启事务 (BEGIN TRANSACTION)
开启事务();
// 2. 调用你原本的业务方法 (target.method())
你的业务方法();
// 3. 如果业务方法正常执行完毕,提交事务 (COMMIT)
提交事务();
} catch (Exception e) {
// 4. 如果捕获到异常,执行回滚 (ROLLBACK)
回滚事务();
// 5. 最后将异常重新抛出,让调用方知道
throw e;
}
}
核心属性详解:精准控制事务行为
你可以通过注解的参数来精细控制事务的方方面面:
|
属性 |
作用 |
示例 |
|
|
传播行为:决定当一个事务方法被另一个事务方法调用时,它自己该如何“自处”。 |
|
|
|
隔离级别:定义事务与事务之间关于数据可见性的隔离程度。 |
|
|
|
回滚规则:指定遇到哪些异常时必须回滚。 |
|
|
|
不回滚规则:指定遇到哪些异常时,即使抛出也不回滚(提交事务)。 |
|
|
|
超时时间:设置事务允许执行的最大秒数,超时则自动回滚。 |
|
|
|
只读事务:标记事务是否只读,是数据库连接池和数据库优化的一个提示。 |
|
核心属性深度解析
1. propagation 传播行为
propagation(传播行为)是 @Transactional 注解中最核心、也最容易让人困惑的一个属性。它解决的核心问题是:当一个事务方法调用另一个事务方法时,第二个方法应该如何处理事务?是复用当前的事务,还是挂起当前事务并开启一个新事务?它解决了“方法调用链”中的事务边界问题。
|
传播行为 |
行为描述 |
典型场景 |
|
|
如果当前没有事务,就新建一个;如果已经存在一个事务,就加入到这个事务中。 |
绝大多数业务方法。比如:用户下单、更新库存等操作,它们天生应该属于同一个大事务。 |
|
|
无论当前是否存在事务,都会新建一个事务。如果当前有事务,则将当前事务挂起。 |
独立的记录日志、审计、发消息。比如在一个下单的大事务中,记录一条操作日志,这条日志的记录不应该因为下单失败而丢失。 |
|
|
如果当前存在事务,则在嵌套事务内执行;如果没有,则行为同 |
批量处理。比如批量导入用户,其中一条失败,只回滚这一条,而不影响整个批次。 |
|
|
如果当前有事务,则支持;如果没有,则以非事务方式执行。 |
查询方法,有事务也行,没有也行。 |
|
|
必须在一个已有的事务中运行,否则抛出异常。 |
强制要求调用方已开启事务的方法。 |
|
|
始终以非事务方式执行。如果当前有事务,则挂起它。 |
不需要事务,且不希望被当前事务影响的非关键操作。 |
|
|
必须在非事务中执行,否则抛出异常。 |
绝对不允许在事务中运行的方法。 |
2. isolation 隔离级别
这主要解决的是数据库并发问题,Spring 定义了以下几种隔离级别:
|
隔离级别 |
脏读 |
不可重复读 |
幻读 |
说明 |
|
|
✅可能 |
✅可能 |
✅可能 |
读未提交,性能最高,但安全性最低。 |
|
|
❌不会 |
✅可能 |
✅可能 |
读已提交,Oracle等数据库默认,可防止脏读。 |
|
|
❌不会 |
❌不会 |
✅可能 |
可重复读,MySQL默认,可防止脏读和不可重复读。 |
|
|
❌不会 |
❌不会 |
❌不会 |
串行化,最高隔离级别,但性能极低,相当于所有事务排队执行。 |
3. rollbackFor / noRollbackFor
-
默认行为:Spring 事务的默认回滚策略是只对非检查型异常(
RuntimeException及其子类)和Error进行回滚,而检查型异常(Exception的子类但非RuntimeException)不会触发回滚。 -
为什么:这是 Spring 的设计哲学,认为检查型异常代表一种“可预期的业务结果”,而非系统故障。
-
如何修改:使用
rollbackFor让检查型异常也回滚:@Transactional(rollbackFor = Exception.class);使用noRollbackFor让某些运行时异常不回滚:@Transactional(noRollbackFor = NullPointerException.class)。
五大常见“坑”与最佳实践
坑1:同一个类内部的方法调用,事务失效
现象:类中的 methodA() 调用 methodB(),只有 methodB() 加了 @Transactional,但事务不生效。
原因:代理机制。外部调用走代理,内部调用直接走 this 引用,绕过了代理。
解决方案:
-
拆分到不同类(推荐):将
methodB()移到另一个@Service中,然后注入调用。 -
自注入:在类中注入自己的代理对象,用
self.methodB()调用。 -
修改为
public方法(基础要求):@Transactional只在public方法上生效,private/protected会被忽略。
坑2:异常被你“吃”掉了,事务不回滚
现象:方法内 try-catch 了异常,但没有重新抛出,事务正常提交。
原因:代理捕获不到异常,认为一切正常。
解决方案:在 catch 块中手动回滚或重新抛出。
// 错误示例
try { ... } catch (Exception e) {
log.error(...); // 只记日志,事务提交!
}
// 正确示例1:重新抛出
try { ... } catch (Exception e) {
log.error(...);
throw e;
}
// 正确示例2:手动回滚
try { ... } catch (Exception e) {
log.error(...);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
坑3:方法权限不正确
现象:注解加在 private 或 protected 方法上,无任何效果。
原因:Spring 的 AOP 代理(无论是 JDK (实现接口)还是 CGLIB(继承))只能通过 public 方法入口增强。
解决方案:始终将 @Transactional 标注在 public 方法上。
坑4:错误的数据库引擎(MySQL)
现象:事务配置一切正确,但发生异常后数据依然被修改。
原因:你的 MySQL 表可能使用了 MyISAM 引擎,它不支持事务。
解决方案:将表引擎改为 InnoDB:ALTER TABLE 表名 ENGINE=InnoDB;
坑5:多数据源配置错误
现象:配置了多个数据源,但 @Transactional 不知道管哪个。
原因:未明确指定事务管理器。
解决方案:通过 value 或 transactionManager 属性指定。
@Transactional(value = "postgresTransactionManager")
public void updateUser() { ... }
总结:核心记忆点
-
原理:
@Transactional的本质是 AOP 动态代理,它帮你自动完成BEGIN、COMMIT、ROLLBACK。 -
默认规则:
-
传播行为:
REQUIRED(加入或新建)。 -
回滚策略:只回滚
RuntimeException和Error,不回滚Exception。 -
生效范围:只能作用于
public方法。
-
-
常见失效:同类内部调用、异常被吞、非
public方法,这三大原因占了 90% 的事务失效问题。 -
最佳实践:
-
在
Service层的public方法上使用。 -
明确指定
rollbackFor = Exception.class可有效防坑。 -
保持事务粒度尽可能小,避免在长事务中进行耗时的非DB操作(如RPC调用、文件处理)。
-
更多推荐
所有评论(0)