事务传播机制

目录

[一、事务基础:先搞懂 "为什么需要它"](#一、事务基础:先搞懂 “为什么需要它”)

[1. 入门理解:用 "转账" 讲透事务](#1. 入门理解:用 “转账” 讲透事务)

[2. 专业解析:事务的 ACID 特性(必知)](#2. 专业解析:事务的 ACID 特性(必知))

[3. 实际场景:除了转账,还有这些地方需要事务](#3. 实际场景:除了转账,还有这些地方需要事务)

[二、Spring 事务的两种实现:编程式 vs 声明式](#二、Spring 事务的两种实现:编程式 vs 声明式)

[1. 编程式事务:手动 "做饭",一步都不能少](#1. 编程式事务:手动 “做饭”,一步都不能少)

[专业代码示例(Spring Boot)](#专业代码示例(Spring Boot))

优缺点分析

[2.声明式事务:用 "自动炒菜机",一键搞定](#2.声明式事务:用 “自动炒菜机”,一键搞定)

[核心背景:Spring 事务默认回滚规则](#核心背景:Spring 事务默认回滚规则)

[例 1:捕获异常后,重新抛出运行时异常 → 事务回滚](#例 1:捕获异常后,重新抛出运行时异常 → 事务回滚)

逻辑分析

结果:事务回滚

[例 2:异常被捕获但不抛出 → 事务提交](#例 2:异常被捕获但不抛出 → 事务提交)

逻辑分析

结果:事务提交

[例 3:异常被捕获后,直接抛出(等价于不捕获)→ 事务回滚](#例 3:异常被捕获后,直接抛出(等价于不捕获)→ 事务回滚)

逻辑分析

结果:事务回滚

[例 4:手动调用回滚方法 → 事务回滚](#例 4:手动调用回滚方法 → 事务回滚)

逻辑分析

结果:事务回滚

[例 5:抛出受检异常(未配置 rollbackFor)→ 事务提交](#例 5:抛出受检异常(未配置 rollbackFor)→ 事务提交)

[关键概念:受检异常 vs 运行时异常](#关键概念:受检异常 vs 运行时异常)

逻辑分析

结果:事务提交

[例 6:用 @SneakyThrows 隐藏受检异常,实际转为 RuntimeException → 事务回滚](#例 6:用 @SneakyThrows 隐藏受检异常,实际转为 RuntimeException → 事务回滚)

逻辑分析

结果:事务回滚

[例 7:用 rollbackFor 指定 "受检异常也回滚" → 事务回滚](#例 7:用 rollbackFor 指定 “受检异常也回滚” → 事务回滚)

关键配置:rollbackFor

逻辑分析

结果:事务回滚

[小心坑:这 3 种情况事务会失效!](#小心坑:这 3 种情况事务会失效!)

[7 个例子总结](#7 个例子总结)

[三、@Transactional 注解 ------ 声明式事务的 "魔法开关"](#三、@Transactional 注解 —— 声明式事务的 “魔法开关”)

[1、@Transactional 能做什么?](#1、@Transactional 能做什么?)

[2、@Transactional 的核心属性](#2、@Transactional 的核心属性)

[3、用法示例:给方法 "插电",秒变事务方法](#3、用法示例:给方法 “插电”,秒变事务方法)

[4、注意:@Transactional 的 "生效规则"](#4、注意:@Transactional 的 “生效规则”)


一、事务基础:先搞懂 "为什么需要它"

在学 Spring 事务前,我们得先明确:事务到底是什么?解决了什么问题?

1. 入门理解:用 "转账" 讲透事务

假设你给朋友转 100 元,整个过程分两步:

  1. 你的账户扣 100 元(A-100);
  2. 朋友的账户加 100 元(B+100)。

如果没有事务:第一步成功了,但第二步突然报错(比如网络断了),结果就是 "你的钱少了,朋友的钱没多"------ 钱平白消失了。

有了事务后:这两步会被 "捆成一个整体",要么全成功(转账完成),要么全失败(你的钱不扣,朋友的钱不加),绝对不会出现 "一半成功一半失败" 的情况。

一句话总结:事务是 "一组不可分割的操作",要么全成,要么全败。

2. 专业解析:事务的 ACID 特性(必知)

对专业开发者而言,事务的核心是ACID 特性,这是判断事务是否可靠的标准:

  • 原子性(Atomicity):操作要么全执行,要么全回滚(如转账的两步不能拆分);
  • 一致性(Consistency):事务执行前后,数据总量守恒(如转账前 A+B=1000,执行后还是 1000);
  • 隔离性(Isolation):多个事务同时执行时,互不干扰(比如你转账的同时,朋友也在给别人转账,不会互相影响);
  • 持久性(Durability):事务提交后,数据永久保存在数据库(即使数据库崩溃,重启后数据也在)。

3. 实际场景:除了转账,还有这些地方需要事务

  • 秒杀系统:"下单" 和 "扣库存" 必须在一个事务里,避免 "超卖"(下单成功但库存没扣);
  • 用户注册:"插入用户数据" 和 "记录注册日志" 必须同时成功,否则用户存在但日志缺失,后续查问题找不到依据;
  • 订单支付:"扣余额""生成订单""减库存" 三步绑定,缺一步都不行。

二、Spring 事务的两种实现:编程式 vs 声明式

Spring 对事务的支持分两类,前者灵活但繁琐,后者简单且常用,我们分别来看。

1. 编程式事务:手动 "做饭",一步都不能少

编程式事务就像 "自己做饭":买菜、切菜、炒菜、洗碗,每一步都要手动操作。它的核心是通过DataSourceTransactionManager(事务管理器)手动控制事务的 "开启、提交、回滚"。

专业代码示例(Spring Boot)

复制代码
@RestController
@RequestMapping("/user")
public class UserController {
    // 1. 注入事务管理器(负责开启/提交/回滚事务)
    @Autowired
    private DataSourceTransactionManager transactionManager;
    // 2. 注入事务属性(定义事务的隔离级别、超时时间等)
    @Autowired
    private TransactionDefinition transactionDefinition;
    // 3. 注入业务层
    @Autowired
    private UserService userService;

    @RequestMapping("/registry")
    public String registry(String name, String password) {
        // 开启事务:获取事务状态(相当于"打开燃气灶")
        TransactionStatus status = transactionManager.getTransaction(transactionDefinition);
        try {
            // 执行核心业务(相当于"炒菜")
            userService.registryUser(name, password);
            // 提交事务(相当于"关火,完成做饭")
            transactionManager.commit(status);
            return "注册成功";
        } catch (Exception e) {
            // 回滚事务(如果炒菜糊了,"倒掉重做")
            transactionManager.rollback(status);
            return "注册失败";
        }
    }
}

优缺点分析

  • 优点:完全手动控制,灵活(比如可根据不同异常决定是否回滚);
  • 缺点:代码冗余(每个事务方法都要写开启 / 提交 / 回滚),不符合 "开闭原则";
  • 适用场景:极少用,仅在需要精细控制事务时(如多数据源切换场景)。

2.声明式事务:用 "自动炒菜机",一键搞定

在了解 @Transactional 基础用法后,"异常处理" 与 "事务回滚" 的配合是新手(甚至老手)最易踩坑的点。下面通过 7 个实战例子,结合「代码 + 结果(提交 / 回滚)+ 日志对比」,彻底讲清事务回滚规则。

核心背景:Spring 事务默认回滚规则

Spring 事务默认只对 RuntimeException(运行时异常)和 Error 自动回滚 ,对「受检异常」(如 IOExceptionSQLException 等编译器强制处理的异常)不回滚。若要让 "受检异常也能触发回滚",需用 @Transactional(rollbackFor = 异常类.class) 显式配置。

例 1:捕获异常后,重新抛出运行时异常 → 事务回滚

复制代码
@Transactional
@RequestMapping("/r1")
public Boolean r1(String userName, String password) {
    Integer result = userService.insert(userName, password);
    System.out.println("插入用户表,result:" + result);
    try {
        int a = 10 / 0; // 触发「算术异常」(属于RuntimeException)
    } catch (Exception e) {
        // 关键:捕获后,重新抛出RuntimeException
        throw new RuntimeException("发生算术异常,事务将回滚", e);
    }
    return true;
}
逻辑分析
  • int a = 10 / 0 会抛出 ArithmeticException(属于 RuntimeException);
  • 虽用 try-catch 捕获异常,但重新抛出了 RuntimeException
  • 符合 Spring 「默认对 RuntimeException 回滚」的规则。
结果:事务回滚

对应日志会显示 "回滚" 核心信息(类似如下行):

复制代码
Transaction synchronization deregistering SqlSession [...]
Transaction synchronization closing SqlSession [...]

例 2:异常被捕获但不抛出 → 事务提交

复制代码
@Transactional
@RequestMapping("/registry")
public Boolean registry(String userName, String password) {
    Integer result = userService.insert(userName, password);
    System.out.println("插入用户表,result:" + result);
    try {
        int a = 10 / 0; // 触发算术异常(RuntimeException)
    } catch (Exception e) {
        e.printStackTrace(); // 仅打印异常,不重新抛出
    }
    return true;
}
逻辑分析
  • 异常被 try-catch 捕获后,没有重新抛出任何异常
  • Spring 认为方法 "正常执行完毕",因此自动提交事务
结果:事务提交

对应日志会显示 **"提交"** 核心信息(类似如下行):

复制代码
Transaction synchronization committing SqlSession [...]
Transaction synchronization deregistering SqlSession [...]

例 3:异常被捕获后,直接抛出(等价于不捕获)→ 事务回滚

复制代码
@Transactional
@RequestMapping("/r2")
public Boolean r2(String userName, String password){
    Integer result = userService.insert(userName, password);
    System.out.println("插入用户表, result: "+ result);
    try {
        int a = 10/0; // 触发算术异常(RuntimeException)
    }catch (Exception e){
        throw e; // 直接抛出捕获的异常(RuntimeException)
    }
    return true;
}
逻辑分析
  • 虽用 try-catch 捕获异常,但直接把异常抛出去了
  • 抛出的 ArithmeticExceptionRuntimeException,符合 Spring 自动回滚规则。
结果:事务回滚

日志显示 "回滚" 相关信息,与「例子 1」的回滚日志一致。

例 4:手动调用回滚方法 → 事务回滚

复制代码
@Transactional
@RequestMapping("/r3")
public Boolean r3(String userName, String password){
    Integer result = userService.insert(userName, password);
    System.out.println("插入用户表, result: "+ result);
    try {
        int a = 10/0; // 触发算术异常(RuntimeException)
    }catch (Exception e){
        // 关键:手动标记"事务需要回滚"
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return true;
}
逻辑分析
  • 即使异常被捕获且不抛出,只要通过 TransactionAspectSupportsetRollbackOnly() 手动标记 "需要回滚",事务就会回滚。
结果:事务回滚

日志显示 "回滚" 相关信息,与「例子 1」的回滚日志一致。

例 5:抛出受检异常(未配置 rollbackFor)→ 事务提交

复制代码
@Transactional
@RequestMapping("/r4")
public Boolean r4(String userName, String password) throws IOException {
    Integer result = userService.insert(userName, password);
    System.out.println("插入用户表, result: "+ result);
    if (true){
        throw new IOException(); // 抛出「受检异常」(非RuntimeException)
    }
    return true;
}
关键概念:受检异常 vs 运行时异常
  • 受检异常 :编译器强制要求处理的异常(如 IOException),必须用 try-catchthrows 声明;
  • 运行时异常 :编译器不强制处理的异常(如 NullPointerException),继承自 RuntimeException
逻辑分析
  • IOException受检异常 ,不属于 Spring 默认回滚的 RuntimeException/Error
  • 方法仅加了 @Transactional(未配置 rollbackFor),因此 Spring 认为 "异常已被 throws 声明,方法正常执行",提交事务
结果:事务提交

日志显示 "提交" 相关信息,与「例子 2」的提交日志一致。

例 6:用 @SneakyThrows 隐藏受检异常,实际转为 RuntimeException → 事务回滚

复制代码
@SneakyThrows // Lombok注解,将受检异常包装为RuntimeException
@Transactional
@RequestMapping("/r5")
public Boolean r5(String userName, String password) {
    Integer result = userService.insert(userName, password);
    System.out.println("插入用户表, result: "+ result);
    if (true){
        throw new IOException(); // 受检异常
    }
    return true;
}
逻辑分析
  • @SneakyThrows 是 Lombok 的 "黑魔法":它会把受检异常(如 IOException偷偷包装成 RuntimeException 的子类抛出;
  • Spring 看到的是 RuntimeException,因此自动回滚事务
结果:事务回滚

日志显示 "回滚" 相关信息,与「例子 1」的回滚日志一致。

例 7:用 rollbackFor 指定 "受检异常也回滚" → 事务回滚

复制代码
@Transactional(rollbackFor = Exception.class, isolation = Isolation.DEFAULT)
@RequestMapping("/r7")
public Boolean r7(String userName, String password) throws IOException {
    Integer result = userService.insert(userName, password);
    System.out.println("插入用户表, result: "+ result);
    if (true){
        throw new IOException(); // 受检异常
    }
    return true;
}
关键配置:rollbackFor

@Transactional(rollbackFor = Exception.class) 表示:只要方法抛出 Exception 及其子类异常(包括受检异常),事务就回滚

逻辑分析
  • IOException 是受检异常,但通过 rollbackFor = Exception.class 显式配置了 "所有 Exception 都触发回滚";
  • 方法抛出 IOException 后,Spring 检测到符合回滚条件,回滚事务
结果:事务回滚

日志显示 "回滚" 相关信息,与「例子 1」的回滚日志一致。

小心坑:这 3 种情况事务会失效!

  1. 异常被手动捕获了 :如果用try-catch捕获了异常且不重新抛出,事务会认为方法执行成功,不会回滚;

    复制代码
    @Transactional
    public void registryUser(String name, String password) {
        try {
            userMapper.insert(name, password);
            int a = 10 / 0; // 异常被捕获,事务不回滚!
        } catch (Exception e) {
            e.printStackTrace(); // 仅打印异常,不抛出去
        }
    }

    解决:要么不捕获异常,要么捕获后重新抛出(throw e;),或手动回滚(TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();)。

  2. 修饰非 public 方法@Transactional仅对public方法生效(非 public 方法不会被 AOP 代理);

  3. 数据源没配置事务管理器 :如果 Spring 没扫描到DataSourceTransactionManager,事务注解会 "形同虚设"(Spring Boot 默认自动配置,手动配置时需注意)。

7 个例子总结

例子 核心场景 事务结果 关键原因
1 捕获异常后,重新抛 RuntimeException 回滚 Spring 默认对 RuntimeException 回滚
2 捕获异常后,不抛出任何异常 提交 Spring 认为方法 "正常执行完毕"
3 捕获后直接抛出 RuntimeException 回滚 等价于 "没捕获异常",触发 Spring 自动回滚
4 手动调用 setRollbackOnly 回滚 强制标记 "事务需要回滚"
5 抛出受检异常(无 rollbackFor 提交 Spring 默认仅对 RuntimeException/Error 回滚,受检异常不触发
6 @SneakyThrows 包装受检异常为 RuntimeException 回滚 Lombok 将受检异常转为 RuntimeException,触发默认回滚
7 rollbackFor 指定受检异常需回滚 回滚 显式配置 rollbackFor = Exception.class,让受检异常也能触发回滚

通过这 7 个例子,能清晰发现:事务是否回滚,核心取决于「异常类型」和「是否被 Spring 检测到需要回滚」


@Transactional 注解 ------ 声明式事务的 "魔法开关"

在 Spring 声明式事务中,@Transactional 是最核心的注解。它像一个 "智能开关",能让方法自动具备事务能力(无需手动写开启 / 提交 / 回滚代码),还能通过 "属性" 精细控制事务的隔离级别、传播机制、回滚规则等。

1、@Transactional 能做什么?

想象你要做一道 "事务菜"(如 "转账"):

  • 手动做(编程式事务):得自己买菜、切菜、炒菜、洗碗,每一步都要管;
  • @Transactional(声明式事务):把食材放进 "自动炒菜机",按下开关(加注解),机器自动完成炒菜(执行业务)、关火(提交事务)、翻车自动重做(异常回滚)

2、@Transactional 的核心属性

通过注解的 "属性",你能像 "调炒菜机参数" 一样,控制事务的行为:

属性名 作用 生活类比(炒菜机) 常用值示例
isolation 控制事务隔离级别(解决 "多事务同时炒同一道菜,互相干扰" 的问题) 控制 "是否允许别人在你炒菜时动锅" Isolation.READ_COMMITTED(读已提交)
propagation 控制事务传播机制(解决 "一个事务方法调用另一个事务方法,锅怎么共用" 的问题) 控制 "新菜用新锅还是旧锅" Propagation.REQUIRED(默认,有锅用旧锅,没锅买新锅)
rollbackFor 指定哪些异常会让事务回滚 (默认只有 RuntimeException 才会触发回滚) 控制 "炒糊到什么程度,会自动倒掉重做" rollbackFor = Exception.class(所有异常都重做)

3、用法示例:给方法 "插电",秒变事务方法

只需在public 方法 (或类)上添加 @Transactional,方法就会被 Spring "代理",自动具备事务能力。

复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockMapper stockMapper;

    // 给"创建订单"方法加事务:下单和扣库存必须同时成功/失败
    @Transactional(
        isolation = Isolation.READ_COMMITTED, // 隔离级别:读已提交
        propagation = Propagation.REQUIRED,   // 传播机制:默认(有事务则加入,无则新建)
        rollbackFor = Exception.class         // 所有Exception都触发回滚
    )
    public void createOrder(OrderDTO order) {
        orderMapper.insert(order);           // 步骤1:插入订单(核心业务)
        stockMapper.decrease(order.getGoodsId()); // 步骤2:扣减库存(核心业务)
        // 如果下面抛异常,上面两步会自动回滚
        // int a = 10 / 0; 
    }
}

4、注意:@Transactional 的 "生效规则"

用不好这个注解,容易出现 "事务看似加了,实际没生效" 的问题,记住这 3 点:

  1. 仅对 public 方法生效

    非 public 方法(如 private)加 @Transactional,事务不会生效 ------ 因为 Spring AOP 代理是基于 "public 方法调用" 实现的。

  2. 方法调用要走 "代理"

    如果在同一个类中 ,用 "普通方法" 调用 "加了 @Transactional 的方法",事务也不生效 ------ 因为绕过了 Spring 的代理逻辑。

    (解决:把被调用的事务方法放到另一个 Bean 中,通过 Bean 注入调用)

  3. 异常处理要匹配 rollbackFor

    如果方法捕获了异常但不抛出,或异常类型不匹配 rollbackFor,事务可能不会回滚(参考上一节 "7 个实战例子")。

相关推荐
_OP_CHEN2 小时前
【C++数据结构进阶】吃透 LRU Cache缓存算法:O (1) 效率缓存设计全解析
数据结构·数据库·c++·缓存·线程安全·内存优化·lru
Elastic 中国社区官方博客2 小时前
在 Elasticsearch 中通过乘法增强来影响 BM25 排名
大数据·数据库·elasticsearch·搜索引擎·全文检索
@淡 定2 小时前
MVCC(多版本并发控制)实现机制详解
java·服务器·数据库
消失的旧时光-19432 小时前
Repository 层如何无缝接入本地缓存 / 数据库
数据库·flutter·缓存
尋有緣2 小时前
力扣1225-报告系统状态的连续日期
数据库·sql·算法·leetcode·oracle
消失的旧时光-19432 小时前
用 Drift 实现 Repository 无缝接入本地缓存/数据库(SWR:先快后准)
数据库·flutter·缓存
Tony Bai2 小时前
【API 设计之道】08 流量与配额:构建基于 Redis 的分布式限流器
数据库·redis·分布式·缓存
无名-CODING2 小时前
MyBatis 动态 SQL 全攻略
数据库·sql·mybatis
枫叶丹43 小时前
【Qt开发】Qt事件(二)-> QKeyEvent 按键事件
c语言·开发语言·数据库·c++·qt·microsoft