@Transactional是 Spring Boot 中处理事务最核心的注解。它的作用是保证一系列数据库操作要么全部成功,要么全部失败(回滚)。
在实际复杂的业务中(比如 Service A 调用 Service B),我们需要控制这两个 Service 的事务是“合二为一”还是“各自独立”,这就是事务传播机制 (Propagation) 解决的问题。
一、@Transactional的基础用法
1、基础语法
通常加在 Service 层的方法上,也可以加在类 上(表示该类所有 public 方法都开启事务)。
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
// 推荐写法:明确指定回滚异常类型
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO order) {
// 1. 扣减库存
inventoryService.deduct(order.getProductId());
// 2. 保存订单
orderRepository.save(order);
// 3. 扣减余额
accountService.pay(order.getUserId(), order.getAmount());
}
}
2、需要注意的坑
规则1:必须是 public 方法。Spring AOP 默认无法拦截 private/protected 方法
规则2:加上 rollbackFor = Exception.class
默认情况:Spring 只会在遇到 RuntimeException (运行时异常) 或 Error 时回滚。如果你的代码抛出了 Checked Exception (比如 IOException 或自定义的非运行时异常),事务不会回滚!
最佳实践:养成习惯,永远写成 @Transactional(rollbackFor = Exception.class)。
二、 事务传播机制 (Propagation) 详解
当 Service A(外层)调用 Service B(内层)时,Service B 是加入 A 的事务,还是自己新开一个?这由 propagation 属性决定。
虽然 Spring 有 7 种传播行为,但日常开发中 95% 只会用到以下三种:
场景设定
假设我们有两个服务:
MainService (外层):主业务。
SubService (内层):子业务,被外层调用
// 外层调用伪代码
@Transactional
public void mainMethod() {
// 做一些事...
subService.subMethod(); // 调用内层
// 做另一些事...
}
package org.springframework.transaction.annotation;
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
1. REQUIRED (默认值) —— “同生共死”
如果外层有事务,内层就加入;如果外层没事务,内层就自己开一个。这是最常用的。
逻辑:外层和内层属于同一个物理事务。
结果:任何一方报错,全部回滚。
代码示例:
// 外层:处理订单
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder() {
orderMapper.insertOrder(); // 步骤1
// 调用内层:扣减库存
stockService.deductStock();
// 假设这里报错:int i = 1/0; -> 步骤1和内层的扣减库存都会回滚
}
// 内层:扣减库存
@Transactional(propagation = Propagation.REQUIRED)
public void deductStock() {
stockMapper.updateStock();
// 假设这里报错 -> 外层的 createOrder 也会感知到异常,全部回滚
}
2. REQUIRES_NEW —— “各自为政”
不管外层有没有事务,内层都开启一个全新的事务。如果外层有事务,会把外层事务挂起,等内层搞定提交了,再恢复外层。
逻辑:两个独立的物理事务(两个数据库连接)。
核心用途:记录日志、发送记录。不管主业务是成功还是失败,日志必须保存下来,不能因为主业务回滚把日志也回滚了。
代码示例:
// 外层:主业务
@Transactional(propagation = Propagation.REQUIRED)
public void buyItem() {
// 1. 买东西
productMapper.reduceStock();
// 2. 记录日志 (不管买没买成,我都要记录这一次操作)
try {
logService.saveLog("用户尝试购买");
} catch (Exception e) {
// 如果日志记失败了,捕获异常,不影响买东西的主流程
}
// 3. 模拟异常
throw new RuntimeException("购买失败,余额不足");
}
// 内层:日志服务
@Transactional(propagation = Propagation.REQUIRES_NEW) // <--- 关键点
public void saveLog(String msg) {
logMapper.insert(msg);
}
结果分析:
buyItem 抛出异常,reduceStock 会回滚(没买成)。
但是!saveLog 因为是 REQUIRES_NEW,它已经独立提交了。所以日志会保留在数据库中
3. NESTED —— “留有后路” (嵌套事务)
如果外层有事务,内层就在这个事务里做一个Savepoint (保存点)。
如果内层报错:只回滚到保存点(内层刚才做的操作没了),外层可以选择捕获异常,继续执行其他逻辑(外层不一定回滚)。
如果外层报错:内层和外层统统回滚。
逻辑:同一个物理事务,但利用了 JDBC 的 Savepoint 功能。
核心用途:重试机制、备选方案。比如:尝试用积分支付,如果失败了(回滚积分扣减),catch 住异常,自动改用余额支付。
代码示例:
// 外层:支付流程
@Transactional(propagation = Propagation.REQUIRED)
public void pay() {
orderMapper.updateStatus("PAYING");
try {
// 尝试用积分支付 (内层)
pointService.payWithPoints();
} catch (Exception e) {
// 捕获了内层的异常!
// 因为是 NESTED,外层不会被标记为 rollback-only,我们可以走 Plan B
System.out.println("积分支付失败,转用余额支付");
balanceService.payWithBalance();
}
}
// 内层:积分服务
@Transactional(propagation = Propagation.NESTED) // <--- 关键点
public void payWithPoints() {
pointMapper.deduct();
throw new RuntimeException("积分不够!"); // 抛出异常
}
结果分析:
payWithPoints 失败回滚,但只回滚了“扣积分”这一步。
pay 方法捕获了异常,updateStatus("PAYING") 不会回滚,且 payWithBalance 会继续执行并提交。
注意点:
如果内层(NESTED)抛出异常,且外层没有捕获(try-catch)这个异常,那么外层和内层都会回滚。数据就像什么都没发生过一样,全军覆没。
1、底层逻辑
虽然 NESTED 是通过数据库的 Savepoint(保存点)实现的,但异常的 传递遵循 Java 的基本规则。
流程如下:
内层报错:内层方法执行失败,数据库操作回滚到“保存点”(也就是说,内层做的修改先撤销了)。
异常冒泡:因为外层没有写 try-catch,这个异常会继续向上抛出,传给外层方法。
外层崩溃:外层方法接收到了异常,也随之执行中断(报错)。
最终判定:Spring 的事务管理器监控到外层事务也抛出了异常,于是触发外层事务的默认回滚机制——把整个事务全部回滚。
所以,最终结果看起来和 REQUIRED 是一模一样的:所有数据都回滚了。2、代码示例
@Service
public class MainService {
@Autowired
private SubService subService;
@Autowired
private UserMapper userMapper;
// 外层事务
@Transactional(propagation = Propagation.REQUIRED)
public void mainMethod() {
// 1. 外层做了一些操作
userMapper.updateName("外层张三");
// 2. 调用内层 NESTED 方法
// 【关键点】:这里没有 try-catch!
subService.nestedMethod();
// 3. 后面的代码不会执行
System.out.println("这就话永远打印不出来");
}
}
@Service
public class SubService {
@Autowired
private UserMapper userMapper;
// 内层事务 (NESTED)
@Transactional(propagation = Propagation.NESTED)
public void nestedMethod() {
// 4. 内层做了一些操作
userMapper.updateAge(18);
// 5. 抛出异常
throw new RuntimeException("内层炸了!");
}
}
结果分析:
内层:updateAge(18) 会回滚(因为它回滚到了 Savepoint)。
外层:由于异常冒泡到了 mainMethod 导致 mainMethod 也是异常结束,所以 updateName("外层张三") 也会回滚。
数据库:名字没变,年龄也没变。
3. 既然不捕获就是全部回滚,那 NESTED 和 REQUIRED 有什么区别?
如果在“不捕获异常”的情况下,它们确实表现一致。
NESTED 存在的唯一意义就在于:外层拥有“选择权”。
REQUIRED:不管外层捕不捕获,只要内层炸了,外层必须陪葬(因为 Spring 标记了 rollback-only)。即使外层 try-catch 了,提交时也会报错 Transaction rolled back because it has been marked as rollback-only。
NESTED:内层炸了,外层可以选择捕获异常并继续执行其他逻辑(比如走备用方案)。
总结一句话: 用 NESTED 时,如果你不写 try-catch,那你就纯属“浪费感情”,效果和默认的 REQUIRED 一模一样,还多了一个创建 Savepoint 的性能开销。所以用 NESTED 必须配合 try-catch 使用。
三、 总结对比表
传播属性 行为描述 谁报错回滚谁? 常用场景
REQUIRED (默认) 加入当前事务,没有则新建 只要有一处报错,所有操作全回滚 普通的业务逻辑调用
REQUIRES_NEW 挂起当前事务,新建独立事务 内层报错回滚内层;外层报错回滚外层。互不干扰 记录操作日志、流水记录
NESTED 在当前事务中嵌套 (Savepoint) 内层报错只回滚内层(若被捕获);外层报错回滚全部 复杂的子业务尝试、备选方案
四、不常用(四种)
坦白说:非常不常用。
在 99% 的实际开发工作中,你只需要掌握前三种(REQUIRED, REQUIRES_NEW, NESTED)。剩下的四种属于**“特定场景下的特殊手段”**。
第一类:随波逐流型 (可有可无)
4. SUPPORTS (佛系)
含义:如果外层有事务,我就加入;如果外层没事务,我就以非事务方式运行。
常用度:⭐ (极低)
场景:
通常用于只读查询。
你希望如果在一个大事务里调用它,它能读到事务内修改的数据(保持一致性);但如果只是单独调用它,没必要为了查个数据专门开个事务(为了性能)。
例子:
Java
@Transactional(propagation = Propagation.SUPPORTS)
public User findUserById(Long id) {
// 如果外层有事务,这里就在事务里查(能查到外层刚修改还没提交的数据)。
// 如果外层没事务,这里就普通查询。
return userMapper.selectById(id);
}
5. NOT_SUPPORTED (独善其身)
含义:我不支持事务。如果外层有事务,把它挂起(暂停),我以非事务方式运行完,再恢复外层事务。
常用度:⭐⭐ (偶尔用到,主要为了性能)
场景:
执行耗时操作(如发邮件、发短信、复杂的图像处理)。
原因:数据库连接是非常宝贵的资源。如果在事务里发邮件(耗时 5 秒),这意味着数据库连接被占用了 5 秒不释放,且可能持有锁。用 NOT_SUPPORTED 可以先把数据库事务挂起,发完邮件再回来,极大释放数据库压力。
例子:
@Service
public class OrderService {
@Transactional
public void createOrder() {
orderMapper.insert(); // 占用数据库连接
// 发送邮件(耗时操作),不应该占用数据库事务资源
emailService.sendEmail();
otherMapper.update(); // 恢复事务继续做
}
}
// 在 EmailService 中
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendEmail() {
// 这里没有事务,运行慢一点也不会锁住数据库表
Thread.sleep(5000);
}
第二类:严格管控型 (强制约束)
这两类更多是为了代码规范和防止误用。
6. MANDATORY (强依赖)
含义:我必须在事务中运行。如果外层没事务,我就直接抛异常。
常用度:⭐ (极低)
场景:
用于约束某些方法绝对不能独立调用,必须作为某个大业务流程的一部分。
比如“扣减库存”这个动作,你规定它必须依赖于“订单创建”或“支付”的事务,不允许任何人单独写个测试代码去裸调它。
例子:
Java
@Transactional(propagation = Propagation.MANDATORY)
public void deductStock() {
// 如果直接在 Controller 里调用这个方法(且没开事务),直接报错:
// IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
stockMapper.update();
}
7. NEVER (绝缘体)
含义:我绝不能在事务中运行。如果外层有事务,我就抛异常。
常用度:⭐ (几乎不用)
场景:
极少见。可能用于某些非线程安全的、或者涉及多线程并发操作的代码,为了防止混淆上下文,强制要求不能在 Spring 管理的事务上下文中执行。
例子:
Java
@Transactional(propagation = Propagation.NEVER)
public void selfInit() {
// 如果你在一个 @Transactional 方法里调用我,我就报错。
}


被折叠的 条评论
为什么被折叠?



