@Transactional 传播机制

@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 方法里调用我,我就报错。
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值