Spring 事务管理深度解析
深入解析Spring声明式事务,核心围绕
@Transactional注解。涵盖其AOP原理、传播行为、隔离级别,并总结了方法自调用等常见失效场景及最佳实践。
我们来全面、深入地聊一下 Spring 的事务管理。这可以说是 Spring 框架最核心、最强大的功能之一。
我会从以下几个方面来为你详细解析:
- 什么是事务?(基本概念)
- Spring 事务的核心优势
- Spring 事务的实现方式:声明式 vs 编程式
@Transactional注解详解(核心中的核心)- 事务的传播行为(Propagation Behavior)
- 事务的隔离级别(Isolation Level)
- Spring 事务背后的原理(AOP)
- 常见"坑"与最佳实践
1. 什么是事务? (基本概念)
事务(Transaction)是一系列操作的集合,这些操作要么全部成功,要么全部失败。它是一个不可分割的工作单元。
最经典的例子就是银行转账:账户 A 向账户 B 转账 100 元。这个操作包含两个步骤:
- 从账户 A 扣除 100 元。
- 向账户 B增加 100 元。
这两个步骤必须在一个事务中完成。如果第一步成功后,系统崩溃,第二步没能执行,那么 A 的钱少了,B 的钱没多,钱就凭空消失了。事务就是为了保证这种情况不会发生。
事务具有四个基本特性,通常被称为 ACID:
- A (Atomicity) 原子性:事务中的所有操作,要么全部完成,要么全部不完成。
- C (Consistency) 一致性:事务执行前后,数据库从一个一致性状态转移到另一个一致性状态。
- I (Isolation) 隔离性:一个事务的执行不应被其他事务干扰。多个并发事务之间是相互隔离的。
- D (Durability) 持久性:一个事务一旦提交,它对数据库中数据的改变就是永久性的。
2. Spring 事务的核心优势
在没有 Spring 的时代,开发者需要手动管理数据库连接、开启事务(conn.setAutoCommit(false))、提交(conn.commit())、回滚(conn.rollback()),并处理复杂的 try-catch-finally 逻辑,代码非常冗余且容易出错。
Spring 事务管理带来了巨大的便利:
- 简化编码:通过注解或 XML 配置,将业务代码与事务管理代码解耦。
- 统一的抽象:Spring 提供了一个
PlatformTransactionManager接口,无论是用的 JDBC、Hibernate、JPA 还是 JTA,上层 API 都是一致的,更换底层数据访问技术时,事务管理代码几乎不用改变。 - 声明式事务:这是最强大的功能,让你能够像“声明”一样定义事务,而无需编写任何事务管理代码。
3. Spring 事务的实现方式
Spring 支持两种事务管理方式:
编程式事务 (Programmatic Transaction Management)
通过TransactionTemplate或者直接使用PlatformTransactionManagerAPI 来手动管理事务。这种方式非常灵活,可以进行细粒度的控制,但侵入性强,会将事务管理代码和业务代码耦合在一起。现在已经很少使用。java@Autowired private TransactionTemplate transactionTemplate; public void transferMoney() { transactionTemplate.execute(status -> { try { // ... 执行数据库操作1 // ... 执行数据库操作2 } catch (Exception e) { // 手动标记事务为回滚 status.setRollbackOnly(); throw e; // 抛出异常 } return null; }); }声明式事务 (Declarative Transaction Management)
这是 最推荐、最常用 的方式。它基于 AOP(面向切面编程)实现,通过在方法上添加@Transactional注解,就可以让 Spring 自动为该方法开启、提交或回滚事务。优点:非侵入性,将业务逻辑与事务逻辑完全解耦,代码极其简洁。
java@Service public class BankServiceImpl implements BankService { @Autowired private AccountDao accountDao; @Transactional @Override public void transfer(String from, String to, double amount) { accountDao.withdraw(from, amount); // 扣钱 // 假设这里发生异常 // int i = 1 / 0; accountDao.deposit(to, amount); // 加钱 } }在上面的例子中,只要在
transfer方法上加上@Transactional,Spring 就会自动处理事务。如果方法正常执行完毕,事务提交;如果方法中途抛出运行时异常(RuntimeException),事务自动回滚。
4. @Transactional 注解详解
@Transactional 是声明式事务的核心,它可以作用于类或方法上。
- 作用于类:表示该类中所有
public方法都应用相同的事务配置。 - 作用于方法:当类和方法上都有此注解时,方法上的配置会覆盖类上的配置。
它有许多重要的属性,用于精细化控制事务行为:
propagation:事务的传播行为(下面详述)。isolation:事务的隔离级别(下面详述)。readOnly:是否为只读事务。默认为false。如果设置为true,数据库会进行一些优化,但此事务中不能执行写操作。timeout:事务超时时间(秒)。超过设定时间后,事务会自动回滚。rollbackFor:指定哪些异常类型会触发事务回滚。noRollbackFor:指定哪些异常类型不会触发事务回滚。
特别注意 rollbackFor 的默认行为:
默认情况下,只有 RuntimeException 和 Error 类型的异常才会触发事务回滚。对于受检异常(Checked Exception,如 IOException),Spring 默认不回滚!
如果你希望遇到任何异常都回滚,需要这样配置:@Transactional(rollbackFor = Exception.class)
5. 事务的传播行为 (Propagation Behavior)
事务传播行为定义了当一个已存在事务的方法调用另一个需要事务的方法时,事务应该如何表现。这是面试中的高频考点。
通过 @Transactional(propagation = Propagation.XXX) 设置。
| 传播行为 | 描述 | 场景 |
|---|---|---|
REQUIRED (默认) |
需要事务。如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务。 | 绝大多数场景,如常规的增删改查。 |
SUPPORTS |
支持事务。如果当前存在事务,则加入该事务;如果不存在,则以非事务方式执行。 | 查询操作,事务有无皆可。 |
MANDATORY |
强制事务。必须在一个已有的事务中执行,否则抛出异常。 | 用于确保方法必须在事务上下文中被调用。 |
REQUIRES_NEW |
需要新事务。总是创建一个新的事务。如果当前存在事务,则将当前事务挂起。 | 外部事务与内部事务需独立,如记录日志。 |
NOT_SUPPORTED |
不支持事务。以非事务方式执行。如果当前存在事务,则将当前事务挂起。 | 调用一个不需要事务的、耗时长的非核心方法。 |
NEVER |
绝不需要事务。以非事务方式执行,如果当前存在事务,则抛出异常。 | 明确方法不能在事务中执行。 |
NESTED |
嵌套事务。如果当前存在事务,则创建一个嵌套事务(保存点)。如果不存在,则行为同 REQUIRED。嵌套事务的回滚不影响外部事务,但外部事务回滚会影响嵌套事务。 |
复杂的业务,希望子业务回滚不影响主业务。 |
REQUIRED vs REQUIRES_NEW 的区别:
REQUIRED:大家都在同一个事务里,一荣俱荣,一损俱损。只要有一个环节出错,整个事务都回滚。REQUIRES_NEW:内部方法会创建一个完全独立的事务。内部事务的回滚不会影响外部事务的提交。
6. 事务的隔离级别 (Isolation Level)
隔离级别定义了一个事务可能受其他并发事务影响的程度。设置不当可能导致脏读、不可重复读、幻读等问题。
通过 @Transactional(isolation = Isolation.XXX) 设置。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 描述 |
|---|---|---|---|---|
READ_UNCOMMITTED |
会 | 会 | 会 | 读未提交。一个事务可以读取到另一个事务未提交的数据。性能最好,但数据一致性最差。 |
READ_COMMITTED |
不会 | 会 | 会 | 读已提交。只能读取到其他事务已提交的数据。(大多数数据库的默认级别,如 Oracle, SQL Server) |
REPEATABLE_READ |
不会 | 不会 | 会 | 可重复读。一个事务中多次读取同一数据,结果总是一致的。(MySQL 的默认级别) |
SERIALIZABLE |
不会 | 不会 | 不会 | 串行化。事务串行执行,完全避免并发问题。性能最差,但数据一致性最好。 |
通常使用数据库的默认隔离级别即可,Spring 只是将其委托给底层数据库去实现。
7. Spring 事务背后的原理 (AOP)
声明式事务的核心是 AOP (Aspect-Oriented Programming),即面向切面编程。
- 代理创建:当 Spring 容器启动时,如果发现一个 Bean 的
public方法上带有@Transactional注解,Spring 不会直接返回这个 Bean 的实例,而是为它创建一个代理对象(Proxy)。 - 方法拦截:当你调用这个带有
@Transactional注解的方法时,实际上调用的是代理对象的方法。 - 事务增强:代理对象的方法在执行你的业务逻辑之前,会先执行一段“增强(Advice)”代码。这段代码会:
- 检查是否存在当前事务。
- 根据传播行为决定是创建新事务、加入当前事务还是挂起当前事务。
- 开启事务 (
try块开始)。
- 业务逻辑执行:调用你写的实际业务代码。
- 提交或回滚:
- 如果业务代码正常执行完毕,代理对象会在
try块的最后提交事务。 - 如果业务代码抛出异常,代理对象会捕获它(在
catch块中),然后回滚事务。
- 如果业务代码正常执行完毕,代理对象会在
这个过程就像一个 try-catch-finally 代码块,由 Spring 自动帮你加上了。
// Spring 代理对象为你做的伪代码
TransactionManager txManager = ...;
TransactionStatus status = txManager.getTransaction(definition); // 1. 开启事务
try {
// 2. 调用你的业务方法
target.yourBusinessMethod();
// 3. 如果没有异常,提交事务
txManager.commit(status);
} catch (RuntimeException | Error ex) {
// 4. 如果有运行时异常或错误,回滚事务
txManager.rollback(status);
throw ex;
} catch (SomeCheckedException ex) {
// 5. 如果是受检异常,默认不回滚,直接提交
txManager.commit(status);
throw ex;
}
8. 常见"坑"与最佳实践
@Transactional注解失效的场景- 方法不是
public的:Spring AOP 默认只代理public方法。 - 方法是
final或static的:AOP 基于动态代理,无法重写final或static方法。 - 方法内部调用(自调用):java
@Service public class MyService { public void methodA() { // ... this.methodB(); // 这里的调用不会触发事务 } @Transactional public void methodB() { // ... } }methodA调用methodB是通过this关键字,直接调用了目标对象的原始方法,绕过了代理对象,所以methodB的事务切面不会生效。解决方法:注入自己代理对象,或者将methodB移到另一个 Bean 中。
- 方法不是
不正确的异常处理
如果你在事务方法内部catch了异常却没有重新抛出,Spring 会认为方法正常执行,从而提交事务,导致数据不一致。java@Transactional public void wrongWay() { try { // ... database update throw new RuntimeException("Something wrong"); } catch (Exception e) { // 异常被"吃掉"了,Spring 不知道出错了,事务会提交! log.error("An error occurred", e); } }正确做法:要么向上抛出异常,要么手动设置事务回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();。数据库引擎不支持事务
确保你的数据库表使用的是支持事务的存储引擎,例如 MySQL 的 InnoDB,而不是 MyISAM。
总结
Spring 的事务管理是一个设计精良、功能强大的模块。在日常开发中,我们绝大多数时间都在使用声明式事务 @Transactional。理解它的核心原理(AOP)、传播行为和常见陷阱,可以帮助你编写出更健壮、更可靠的业务代码。