Spring进阶 - Spring事务理论+实战,一文吃透事务

本文系统性介绍Spring事务,一贯坚持知识关联性的叙述风格。

首先要了解 Spring 事务在 Spring 架构中的定位:

可以看出来,Spring事务属于 Data Acess(数据访问模块) 的知识点。

Data Acess 用于在**应用程序(Spring程序)**中访问持久化数据存储(如关系型数据库、NoSQL、缓存)与业务/服务层之间进行解耦、简化样板代码、统一事务与异常处理的通用抽象层。形象理解 @Transactionnal 注解是无侵入的给业务代码加上了事务功能,这就是 Data Acess 模块带来的快乐之一。

文本的目标是介绍 Data Acess 的 Transactions 。

fency: 从图中可以看出来,DataAccess 在 Core 模块之上,其实 Data Access 将 Core 模块作为底层能力支撑,而 Spring 事务是依赖于 **spring-core****spring-beans** 等核心模块,并与 **spring-aop** 模块紧密协作来实现 Spring 事务

事务

事务介绍

在计算机科学/数据管理系统中,一个"事务(transaction)"是指一系列操作组成的逻辑单元,它要么全部成功提交,要么全部失败回滚,从而保证系统状态从一个一致状态变到另一个一致状态。

举例:银行转账操作 ------ 扣款和入账两个子操作构成一个事务。若其中一个失败,则两个操作都应回滚。

事务的四大特性(ACID):原子性、一致性、隔离性、持久性

  • 原子性( Atomicity ): 事务是一个不可分割的整体。事务中的所有操作要么全部成功,要么全部失败,不能只成功部分。若事务执行过程中某个操作失败,则要将之前已做的操作回滚,如同未执行过。
  • 隔离性 (Isolation):当多个事务并发执行时(尤其在高并发系统中),一个事务不能看到其他事务未提交的中间状态;不同事务之间的执行效果应该与串行执行一致。这样可防止脏读、幻读、不可重复读等问题。
  • 持久性 (Durability):一旦事务提交,其变更就应永久保存在系统中,即便系统崩溃、断电也不能丢失。通常通过日志、磁盘持久化机制实现。
  • 一致性(Consistency):事务执行结束后,数据必须保持一致性状态。在事务执行期间,数据库中的数据可以处于中间状态,但在事务完成时必须保证数据的一致性。

落地技术:

原子性:由 MySql 的 undolog 实现,undolog 可以在 发生错误/死锁中断 、 用户执行 ROLLBACK 时,将已做修改撤销,回滚操作就是由 undolog 实现的,因此可以保证要么全成功,要么全失败

持久性:由 MySql 的 redolog 实现,注意 mysql 也是有数据缓冲区的,插入的数据放到数据缓冲区,定时或内存满了才会刷盘持久化(性能提升),如果还没持久化断电了,此时 redolog 就会发挥作用,redolog 记录了所有插入、更新、修改的操作,并在事务结束前刷盘 redolog , 确保redolog 必须持久化,从而保证了 MySQL 数据的持久化。

隔离性: 由 MySQL 的 MVCC 版本控制实现,有四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,隔离级别决定了事务中间数据的脏读、不可重复读、幻读等问题。MySQL 默认隔离级别是 MVCC + 间隙锁能够解决幻读。

**一致性:**因为实现了以上三种特性,所以一定满足一致性,这是一种因果关系,数据一致性是事务的终极目标。

事务类型:本地事务 VS 分布式事务

我们讨论的经常是本地事务,也就是单个数据源内执行的事务,这种事务由数据库自行管理,为开发者提供事务能力。

分布式事务涉及多个数据源(包括数据库、消息队列等),需要跨多个资源管理器(如数据库)进行协调,实现起来比较复杂,通常需要应用服务器(如Spring Cloud Gateway)或者JTA(Java Transcation API)支持

事务的概念不仅仅应用于 MySQL 数据,在以下系统和组件都有应用:

系统/组件 事务支持 说明
关系型数据库 完全支持 ACID 特性。 如 MySQL、PostgreSQL、Oracle 等,是事务最经典的应用场景。
消息队列 部分支持(如 RocketMQ 的事务消息)。 保证消息发送与本地事务的原子性,确保消息不丢失或重复消费。
Redis 提供有限的事务支持 (通过 **MULTI**/**EXEC**命令) 。 不保证原子性 (命令执行失败不会回滚)和隔离性(事务执行期间其他客户端命令可能插入)。通常用于简单批量操作,或结合 Lua 脚本实现原子操作juejin 。
Elasticsearch 不支持传统意义上的 ACID 事务。 更侧重于最终一致性和文档级别的原子操作。

事务传播行为的概念

事务传播行为: 一个事务调用另一个事务如何处理(加入 , 挂起,禁止加入,嵌套)

维基百科对事务的定义重点关注的是事务作为"独立单位"的概念,并没有牵扯事务传播行为,所以本质上事务传播行为不属于标准事务的知识领域,而是在应用层框架中运用的一种机制,在Spring中我们会看到" 一个事务方法调用另一个事务方法,该子方法应如何参与或者不参与父事务 "的情境。

注意区分,事务传播行为是 Spring 事务的独有概念,并不是数据库级事务定义的原生特性 。

Spring 事务

介绍

Spring 事务是 对底层事务 API(如 JDBC、JPA、Hibernate、JTA 等)的一层统一封装

通过它,开发者不需要关心底层数据库或框架的差异,只需用统一的编程方式来声明和管理事务。

名词解释:

JDBC: JDBC 是 Java 提供给关系数据库访问的一个标准 API,定义了如何建立连接、发送 SQL、检索结果,仅仅定义了抽象接口,实现交给具体的关系数据库。 Spring 的事务管理机制能帮你管理 JDBC 的事务的提交、回滚,不必手写这些细节。

JTA : JTA 是 Java 规范中用于"分布式事务/多资源管理器事务" 的 API。 在一个事务内可能涉及多个资源(比如多个数据库、消息队列等)时,JTA 提供一个标准接口让事务管理器协调这些资源 。

JPA : JPA 是 Java 的一个对象--关系映射(ORM)规范/API,定义如何将 Java 对象映射为数据库表、如何做持久化、查询、事务等。 开发者能通过实体类(POJO)操作数据库,正是JPA发挥了作用。Spring 的事务适配 JPA 提供的事务模型 , 让你用 @Transactional 等方式控制 JPA 操作的事务边界

Hibernate:Hibernate 是一个最著名的 ORM 框架之一,在 Java 世界中用于将 Java 对象映射到关系数据库,并处理所需的 CRUD、查询、缓存、事务整合等。 Spring 的事务抽象也支持 Hibernate 的事务管理

Spring 事务的优势

  • 跨不同事务API的一致编程模型:支持JTA、JDBC、Hibernate、JPA等 。
  • 支持声明式事务管理:通过AOP实现,极大简化事务管理 。
  • 简化的编程式事务管理API:比JTA等复杂API更易用 。
  • 与Spring数据访问抽象完美集成:无缝整合各种数据访问技术 。

Spring 事务的管理方式

Spring 提供了两种主要的事务管理方式:声明式事务管理,编程式事务管理。

1、声明式事务管理(注解,xml)

基于注解或 XML 配置实现,无需在代码中显式编写事务逻辑, XML 声明事务过时了。

常用注解:

java 复制代码
@Transactional
public void transferMoney() {
    accountDao.debit("A", 100);
    accountDao.credit("B", 100);
}
  • 默认是 运行时异常(RuntimeException)回滚
  • 可以通过属性定制行为:
java 复制代码
@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED,
    timeout = 30,
    rollbackFor = Exception.class
)

@Transactional 更多属性:

(按功能分类讲,表格后我还会带例子)

属性 作用 默认值 说明
propagation 事务传播行为 Propagation.REQUIRED 指定当前方法在事务中的传播方式(是否新建事务等)。
isolation 事务隔离级别 Isolation.DEFAULT 控制并发事务间的隔离强度。
timeout 超时时间(秒) -1(不超时) 超过时间未完成则回滚。
readOnly 是否只读事务 false 可用于优化查询性能(提示数据库不用加锁)。
rollbackFor 指定哪些异常触发回滚 无(仅对 RuntimeException/ Error回滚) 支持多个异常类,如 rollbackFor = {Exception.class}
2、编程式事务管理

通过 **TransactionTemplate**重点 )或 PlatformTransactionManager 手动控制事务。比声明式事务更灵活、可控,但是代码入侵高。

示例:

java 复制代码
@Autowired
private TransactionTemplate transactionTemplate;

public void transferMoney() {
    transactionTemplate.execute(status -> {
        accountDao.debit("A", 100);
        accountDao.credit("B", 100);
        return null;
    });
}

编程事务的 return 值一般不重要,用不到。

Spring 事务传播行为

事务传播行为定义了**当一个事务方法调用另一个事务方法时,事务如何在这些方法间传播,**传播行为是Spring独有的概念,并非事务通用概念。

示例:

以下面代码为例,createUserAndOrder() 方法本身有事务,方法内调用了另一个有事务的 OrderService() 方法,这就涉及到事务传播的知识范畴,事务传播有七种行为。

java 复制代码
    @Transactional(propagation = Propagation.REQUIRED)
    public void createUserAndOrder() {
        // 1. 插入用户
        insertUser();

        // 2. 调用 OrderService
        orderService.createOrder();
    }


    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder() {
        System.out.println("订单已创建");
        // 这里如果抛异常,则事务回滚
        // throw new RuntimeException("订单创建失败");
    }

propagation 是事务传播行为的属性,可以指定以下七个值,代表了七种行为。

主要要站在内层事务的角度(createOrder方法)思考这七种行为,我将事务传播分为了四个类别:内层事务加入外层事务(当前事务),挂起当前事务,禁止当前事务,嵌套事务。

分类 传播行为 存在事务时行为 无事务时行为 核心特点与典型场景
加入当前事务 **REQUIRED** 加入当前事务 创建新事务 默认选择。适用于绝大多数业务场景,保证操作在同一事务中。
**SUPPORTS** 加入当前事务 以非事务方式执行 可选支持。适用于查询方法,可根据调用方决定是否启用事务。
**MANDATORY** 加入当前事务 抛出异常 强制要求。确保方法必须在事务中运行,否则报错。用于内部核心方法,防止意外脱离事务。
挂起当前事务 **REQUIRES_NEW** 挂起当前事务,创建新事务 创建新事务 独立事务。新事务与外层事务完全独立,必须单独提交/回滚。用于日志记录、发送通知等需与主事务隔离的操作。
**NOT_SUPPORTED** 挂起当前事务,以非事务方式执行 以非事务方式执行 强制非事务。明确不需要事务的操作,如复杂查询,避免事务开销。
禁止当前事务 **NEVER** 抛出异常 以非事务方式执行 禁止事务。绝对不允许在事务中运行,否则报错。用于纯计算或外部调用等场景。
嵌套事务 **NESTED** 嵌套事务中执行 创建新事务 部分回滚。基于保存点机制,内层事务失败可单独回滚,不影响外层事务。适用于大操作中需独立回滚的部分步骤(如批量操作中的单条失败)。

面试官问到事务传播行为有哪些,你能说出四个种类来,也表现出你是理解的,每一个详细讲出来确实恶心。实际开发中,有意识的区分四个种类,也就够了。

Spring 事务隔离级别

事务隔离级别定义了并发事务之间的隔离程度,以防止数据不一致性问题(如脏读、不可重复读、幻读)Spring 支持以下 5 种隔离级别,与标准 JDBC 隔离级别对应:

隔离级别 说明 可能引发的问题 数据库默认示例
ISOLATION_DEFAULT 默认隔离级别 使用底层数据库的默认隔离级别 取决于数据库 MySQL: **REPEATABLE_READ**
ISOLATION_READ_UNCOMMITTED 读未提交 允许读取未提交的数据变更 。 脏读、不可重复读、幻读 较少使用
ISOLATION_READ_COMMITTED 读已提交 只允许读取已提交的数据变更 。 不可重复读、幻读 Oracle、SQL Server 默认
ISOLATION_REPEATABLE_READ 可重复读 确保同一事务中多次读取同一数据的结果一致 幻读 MySQL 默认,但MySQL不会有幻读问题
ISOLATION_SERIALIZABLE 串行化 最高的隔离级别,事务串行执行 无并发问题,但性能极低 金融等高一致性要求场景

当你在Spring应用层指定了事务的隔离级别,就相当于给数据库指定了隔离级别。

Spring 事务的原理

fency :对于程序技术而言,原理就是看程序的调用链路,看源码是怎么写的,分析背后的设计思想,不过这里看看调用链路,简单学一点原理就行了。

Spring 事务的原理核心是 AOP(面向切面编程)事务管理器 的协同工作。它通过代理模式在方法执行前后插入事务控制逻辑,从而实现声明式事务管理。下面我用一张图帮你直观理解其整体流程,然后再分步解析。

客户端:理解成前端发送请求,访问到后端的一个事务方法,开始调用事务方法,你看的是调用了一个带有 @Transitionnal 注解的方法,实际上呢?

Spring AOP 代理 :Spring 容器在启动时,会检查所有 Bean,如果发现某个类(或其方法)上带有 **@Transactional** 注解,Spring 会通过 AOP 框架 为其创建一个代理对象(基于 JDK 动态代理或 CGLIB),这个代理对象包装了原始的目标对象(方法)。 所以实际上程序调用了一个 AOP 代理。

TransactionManager : AOP 代理会调用Spring事务管理器(理解成一个类就行),帮你在背后对接数据库,帮你开启事务,告知 Spring 最新的数据库事务状态。

所以 Spring 事务的能力是取决于数据库,如果数据库使用 MyISAM 存储引擎,那么 Spring 将丧失事务能力。

Spring 事务实战

技术栈选用:Spring Boot 3.x + MyBatis plus + MySQL

场景是经典转账场景,周杰伦给昆凌转账

初始化项目

项目依赖:

xml 复制代码
<dependencies>
  <!-- Spring Boot Starter -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>

  <!-- MySQL Driver -->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
  </dependency>

  <!-- MyBatis-Plus Starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.14</version>
        </dependency>

  <!-- Lombok -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>

  <!-- Spring Boot Test -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

库表设计:

plsql 复制代码
CREATE DATABASE IF NOT EXISTS spring_tx_demo;
USE spring_tx_demo;

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL,
  balance DECIMAL(10,2) NOT NULL
);

INSERT INTO users(name, balance) VALUES ('周杰伦', 1000.00), ('昆凌', 1000.00);

配置文件:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring_tx_demo?useSSL=false&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # SQL 输出
  global-config:
    db-config:
      id-type: auto
      table-underline: true

MyBatis X 插件生成代码:

配置文件:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring_tx_demo?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # SQL 输出
  global-config:
    db-config:
      id-type: auto
      table-underline: true

实体类:

java 复制代码
@TableName(value ="users")
@Data
public class Users {
    /**
     * 
     */
    @TableId(type = IdType.AUTO)
    private Integer id;

    /**
     * 
     */
    private String name;

    /**
     * 
     */
    private BigDecimal balance;

}

事务基本功能示例

周杰伦给昆凌转账 , 我们模拟两次,一次是转账成功,一次是转账失败回滚,来验证Spring事务是正常工作的。 注意代码不用看的太仔细,一会跟我分析日志,日志是精髓,代码精髓就是一个 @Transactional ,代码逻辑无所谓。

Service 代码:

java 复制代码
@Service
public class UsersService {

    @Autowired
    private UsersMapper usersMapper;

    /**
     * 场景一:成功的转账
     * @param fromName 转出人
     * @param toName   转入人
     * @param amount   金额
     */
    @Transactional // <-- 关键!开启事务
    public void transferMoneySuccessfully(String fromName, String toName, BigDecimal amount) {
        System.out.println("--------- 开始成功转账 ---------");
        // 1. 查询转出用户
        Users fromUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", fromName)); // 使用 Users
        // 2. 查询转入用户
        Users toUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", toName)); // 使用 Users

        // 3. 转出用户扣款
        fromUser.setBalance(fromUser.getBalance().subtract(amount));
        usersMapper.updateById(fromUser);
        System.out.println(fromName + " 扣款 " + amount + " 成功,余额: " + fromUser.getBalance());

        // 4. 转入用户收款
        toUser.setBalance(toUser.getBalance().add(amount));
        usersMapper.updateById(toUser);
        System.out.println(toName + " 收款 " + amount + " 成功,余额: " + toUser.getBalance());

        System.out.println("--------- 成功转账完成 ---------");
    }

    /**
     * 场景二:失败的转账(模拟异常,触发回滚)
     * @param fromName 转出人
     * @param toName   转入人
     * @param amount   金额
     */
    @Transactional // <-- 关键!开启事务
    public void transferMoneyWithException(String fromName, String toName, BigDecimal amount) {
        System.out.println("--------- 开始模拟异常转账 ---------");
        // 1. 查询转出用户
        Users fromUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", fromName)); // 使用 Users
        // 2. 查询转入用户
        Users toUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", toName)); // 使用 Users

        // 3. 转出用户扣款
        fromUser.setBalance(fromUser.getBalance().subtract(amount));
        usersMapper.updateById(fromUser);
        System.out.println(fromName + " 扣款 " + amount + " 成功,余额: " + fromUser.getBalance());

        // 4. 模拟系统在转账过程中突然发生异常!
        System.out.println("系统发生异常,转账中断...");
        throw new RuntimeException("模拟转账过程中发生异常!");

        // 下面的代码永远不会被执行
        // toUser.setBalance(toUser.getBalance().add(amount));
        // usersMapper.updateById(toUser);
    }
}

在启动类中直接调用了,不用 web 的 controller 调用。

java 复制代码
@SpringBootApplication
@MapperScan("com.feng.springtransactiondemo.mapper")
public class SpringTransactionDemoApplication implements CommandLineRunner {

    @Autowired
    private UsersService usersService; // 注入 UsersService

    @Autowired
    private UsersMapper usersMapper; // 注入 UsersMapper,用于查询最终结果

    public static void main(String[] args) {
        SpringApplication.run(SpringTransactionDemoApplication.class, args);
    }


    @Override
    public void run(String... args) throws Exception {
        System.out.println("========================================");
        System.out.println("Spring 事务实战演示开始");
        System.out.println("========================================");

        // --- 场景一:演示成功的事务 ---
        System.out.println("\n--- 场景一:Alice 给 Bob 转账 200 元 ---");
        usersService.transferMoneySuccessfully("Alice", "Bob", new BigDecimal("200"));
        printAllUsersBalance();

        // --- 场景二:演示失败的事务(回滚)---
        System.out.println("\n--- 场景二:Alice 给 Bob 转账 300 元,但中途发生异常 ---");
        try {
            usersService.transferMoneyWithException("Alice", "Bob", new BigDecimal("300"));
        } catch (Exception e) {
            System.err.println("捕获到异常: " + e.getMessage());
        }
        printAllUsersBalance();

        System.out.println("\n========================================");
        System.out.println("Spring 事务实战演示结束");
        System.out.println("========================================");
    }

    private void printAllUsersBalance() {
        System.out.println("--- 查询当前所有用户余额 ---");
        List<Users> users = usersMapper.selectList(null); // 使用 Users
        users.forEach(user -> System.out.println(user.getName() + " 的余额是: " + user.getBalance()));
        System.out.println("---------------------------");
    }

}

最终得到日志就很有用了:

bash 复制代码
========================================
Spring 事务实战演示开始
========================================

--- 场景一:周杰伦 给 昆凌 转账 200 元 ---
2025-10-28T18:50:28.216+08:00  INFO 29736 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2025-10-28T18:50:28.344+08:00  INFO 29736 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@294f9d50
2025-10-28T18:50:28.345+08:00  INFO 29736 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
--------- 开始成功转账 ---------
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7]
JDBC Connection [HikariProxyConnection@718057154 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will be managed by Spring
==>  Preparing: SELECT id,name,balance FROM users WHERE (name = ?)
==> Parameters: 周杰伦(String)
<==    Columns: id, name, balance
<==        Row: 1, 周杰伦, 1000.00
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] from current transaction
==>  Preparing: SELECT id,name,balance FROM users WHERE (name = ?)
==> Parameters: 昆凌(String)
<==    Columns: id, name, balance
<==        Row: 2, 昆凌, 1000.00
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] from current transaction
==>  Preparing: UPDATE users SET name=?, balance=? WHERE id=?
==> Parameters: 周杰伦(String), 800.00(BigDecimal), 1(Integer)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7]
周杰伦 扣款 200 成功,余额: 800.00
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] from current transaction
==>  Preparing: UPDATE users SET name=?, balance=? WHERE id=?
==> Parameters: 昆凌(String), 1200.00(BigDecimal), 2(Integer)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7]
昆凌 收款 200 成功,余额: 1200.00
--------- 成功转账完成 ---------
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7]
--- 查询当前所有用户余额 ---
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f25bf88] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@2069678360 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will not be managed by Spring
==>  Preparing: SELECT id,name,balance FROM users
==> Parameters: 
<==    Columns: id, name, balance
<==        Row: 1, 周杰伦, 800.00
<==        Row: 2, 昆凌, 1200.00
<==      Total: 2
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f25bf88]
周杰伦 的余额是: 800.00
昆凌 的余额是: 1200.00
---------------------------

--- 场景二:周杰伦 给 昆凌 转账 300 元,但中途发生异常 ---
--------- 开始模拟异常转账 ---------
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4]
JDBC Connection [HikariProxyConnection@933293116 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will be managed by Spring
==>  Preparing: SELECT id,name,balance FROM users WHERE (name = ?)
==> Parameters: 周杰伦(String)
<==    Columns: id, name, balance
<==        Row: 1, 周杰伦, 800.00
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] from current transaction
==>  Preparing: SELECT id,name,balance FROM users WHERE (name = ?)
==> Parameters: 昆凌(String)
<==    Columns: id, name, balance
<==        Row: 2, 昆凌, 1200.00
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] from current transaction
==>  Preparing: UPDATE users SET name=?, balance=? WHERE id=?
==> Parameters: 周杰伦(String), 500.00(BigDecimal), 1(Integer)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4]
周杰伦 扣款 300 成功,余额: 500.00
系统发生异常,转账中断...
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4]
--- 查询当前所有用户余额 ---
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@456f7d9e] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1976788674 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will not be managed by Spring
==>  Preparing: SELECT id,name,balance FROM users
==> Parameters: 
<==    Columns: id, name, balance
<==        Row: 1, 周杰伦, 800.00
<==        Row: 2, 昆凌, 1200.00
<==      Total: 2
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@456f7d9e]
周杰伦 的余额是: 800.00
昆凌 的余额是: 1200.00
---------------------------

========================================
Spring 事务实战演示结束
========================================

日志看完了,发现我们打上的注解确实有用的,在场景二中,我们执行了两个更新金额的操作,根据所学知识,这需要开启事务,所以我们用Spring事务快速开启 MySQL 的事务,而不用编写 sql 语句,一个注解搞定。最终也确实回滚了,转账失败。

脏读示例

这里给出一个脏读示例,别的也是类似的。

在原本的service上加以下两个方法

上面的方法是默认的隔离级别(可重复读),下面的方法是isolation = Isolation.READ_UNCOMMITTED读未提交

java 复制代码
/**
     * 模拟周杰伦:增加余额,但最后会回滚(事务未成功提交)
     * @param name 用户名
     * @param amount 增加的金额
     */
@Transactional // 使用默认的隔离级别即可
public void addBalanceWithRollback(String name, BigDecimal amount) {
    System.out.println("\n[" + Thread.currentThread().getName() + "] " + name + " 开始事务,准备增加余额 " + amount);
    Users jay = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", name));
    jay.setBalance(jay.getBalance().add(amount));
    usersMapper.updateById(jay);
    System.out.println("[" + Thread.currentThread().getName() + "] " + name + " 余额已更新为: " + jay.getBalance() + ",但事务尚未提交!");

    try {
        // 模拟耗时操作,让其他事务有机会读取
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }

    System.out.println("[" + Thread.currentThread().getName() + "] " + name + " 的操作发生异常,事务即将回滚!");
    throw new RuntimeException("模拟系统异常,导致回滚");
}

/**
     * 模拟昆凌:使用 READ_UNCOMMITTED 隔离级别进行查询,可能发生脏读
     * @param name 要查询的用户名
     */
@Transactional(isolation = Isolation.READ_UNCOMMITTED) // 关键:设置隔离级别为读未提交
public void readWithDirtyRead(String name) {
System.out.println("\n[" + Thread.currentThread().getName() + "] 昆凌开始查询 " + name + " 的余额...");

try {
    // 稍微等待一下,确保周杰伦已经更新了数据但还没回滚
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Users jay = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", name));
System.out.println("[" + Thread.currentThread().getName() + "] 昆凌第一次读取到 " + name + " 的余额是: " + jay.getBalance() + " (这可能是脏数据!)");

try {
    // 等待周杰伦的事务回滚
    Thread.sleep(4000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

// 在同一个事务中再次读取
Users jayAgain = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", name));
System.out.println("[" + Thread.currentThread().getName() + "] 昆凌第二次读取到 " + name + " 的余额是: " + jayAgain.getBalance() + " (周杰伦的事务已回滚)");
}

启动类要注释刚刚的run方法相关代码,用以下的代码:

java 复制代码
    @Override
    public void run(String... args) throws Exception {
        System.out.println("========================================");
        System.out.println("Spring 事务并发问题演示:脏读");
        System.out.println("========================================");

        // 为了演示效果,先重置周杰伦的余额
        resetJayBalance();

        // 创建一个包含2个线程的线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        System.out.println("\n--- 场景:昆凌在周杰伦未提交的事务中读取了脏数据 ---");

        // 线程1:模拟周杰伦的操作(更新并回滚)
        executor.submit(() -> {
            try {
                usersService.addBalanceWithRollback("周杰伦", new BigDecimal("5000"));
            } catch (Exception e) {
                // 捕获异常,防止线程池报错
                System.err.println("[" + Thread.currentThread().getName() + "] 捕获到异常: " + e.getMessage());
            }
        });

        // 线程2:模拟昆凌的查询(脏读)
        executor.submit(() -> {
            usersService.readWithDirtyRead("周杰伦");
        });

        // 关闭线程池
        executor.shutdown();
    }

    private void resetJayBalance() {
        Users jay = usersMapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Users>().eq("name", "周杰伦"));
        if (jay != null) {
            jay.setBalance(new BigDecimal("1000.00"));
            usersMapper.updateById(jay);
            System.out.println("周杰伦的余额已重置为: " + jay.getBalance());
        }
    }

最终得到日志是这样的,这一次简化给大家看:

bash 复制代码
========================================
Spring 事务并发问题演示:脏读
========================================
周杰伦的余额已重置为: 1000.00

--- 场景:昆凌在周杰伦未提交的事务中读取了脏数据 ---
[pool-1-thread-1] 周杰伦 开始事务,准备增加余额 5000
[pool-1-thread-1] 周杰伦 余额已更新为: 6000.00,但事务尚未提交!
[pool-1-thread-2] 昆凌开始查询 周杰伦的余额...
[pool-1-thread-2] 昆凌第一次读取到 周杰伦 的余额是: 6000.00 (这可能是脏数据!)
[pool-1-thread-1] 周杰伦 的操作发生异常,事务即将回滚!
[pool-1-thread-1] 捕获到异常: 模拟系统异常,导致回滚
[pool-1-thread-2] 昆凌第二次读取到 周杰伦 的余额是: 1000.00 (周杰伦的事务已回滚)

系统给周杰伦转 5000 块钱,周杰伦的事务还没提交,昆凌就开始查账 ,查到了6000 , 然后系统发生异常,正常回滚,周杰伦的金额回到了 1000, 昆凌再查一次账变成了 1000! 昆凌在一次事务中查询同样的数据,结果却不一致,这就是脏读,如果昆凌基于读取到的 6000 余额做了某些业务决策(比如批准了一笔大额贷款),那么当周杰伦的事务回滚后,这个决策就建立在了一个不存在的"假数据"之上,可能导致严重的业务问题。

事务传播示例用法

我们演示两个传播行为:【默认事务传播行为】 和 【挂起当前事务并开启新事务】(REQUIRES_NEW)

场景周杰伦下单,下单包含多个步骤,每个步骤单独开启事务,便能演示一个事务调用另一个事务。

  1. 创建订单
  2. 扣减库存
  3. 扣减用户余额

我们需要增加产品表和订单表:

sql 复制代码
-- 商品表
CREATE TABLE product (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  stock INT NOT NULL
);

-- 订单表
CREATE TABLE orders (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  product_id INT NOT NULL,
  amount DECIMAL(10, 2) NOT NULL,
  create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 插入初始数据
INSERT INTO product(name, stock) VALUES ('iPhone 15', 10);

-- 确保"周杰伦"的账户余额足够
UPDATE users SET balance = 8000.00 WHERE name = '周杰伦';

实体类:

java 复制代码
@Data
@TableName("orders")
public class Order {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private Long productId;
    private BigDecimal amount;
    private LocalDateTime createTime;
}
@Data
@TableName("product")
@AllArgsConstructor
public class Product {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer stock;
}

mapper 类:

java 复制代码
@Mapper
public interface OrderMapper extends BaseMapper<Order> {}
@Mapper
public interface ProductMapper extends BaseMapper<Product> {}

对应的mapper.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feng.springtransactiondemo.mapper.OrderMapper">

  <resultMap id="BaseResultMap" type="com.feng.springtransactiondemo.domain.Order">
    <id property="id" column="id" jdbcType="INTEGER"/>
    <result property="userId" column="user_id" jdbcType="INTEGER"/>
    <result property="productId" column="product_id" jdbcType="INTEGER"/>
    <result property="amount" column="amount" jdbcType="DECIMAL"/>
    <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
  </resultMap>

  <sql id="Base_Column_List">
    id,user_id,product_id,
    amount,create_time
  </sql>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feng.springtransactiondemo.mapper.ProductMapper">

  <resultMap id="BaseResultMap" type="com.feng.springtransactiondemo.domain.Product">
    <id property="id" column="id" jdbcType="INTEGER"/>
    <result property="name" column="name" jdbcType="VARCHAR"/>
    <result property="stock" column="stock" jdbcType="INTEGER"/>
  </resultMap>

  <sql id="Base_Column_List">
    id,name,stock
  </sql>
</mapper>

Service 类

java 复制代码
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private UsersMapper usersMapper;

    // 用于记录日志的Mapper,我们直接用OrderMapper来模拟一个日志表
    @Autowired
    private OrderMapper logMapper; // 假设这是一个独立的日志Mapper

    /**
     * 场景一:REQUIRED (默认值)
     * 外部方法事务,内部方法加入外部事务。任何一个失败,整体回滚。
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void placeOrder_REQUIRED(Long userId, Long productId, BigDecimal amount) {
        System.out.println("\n--- 场景一:REQUIRED 传播 ---");
        System.out.println("[外部事务] 开始下单...");

        // 1. 创建订单
        createOrder(userId, productId, amount);

        // 2. 扣减库存
        reduceStock(productId);

        // 3. 扣减余额 (这里会模拟失败)
        deductBalance(userId, amount);

        System.out.println("[外部事务] 下单成功!");
    }

    /**
     * 场景二:REQUIRES_NEW
     * 内部方法开启一个新事务 REQUIRES_NEW,独立于外部事务。即使外部事务回滚,内部事务也提交。
     */
    @Transactional(propagation = Propagation.REQUIRED)  // 外部事务设置为 REQUIRED,表示如果当前存在事务,就加入该事务
    public void placeOrder_REQUIRES_NEW(Long userId, Long productId, BigDecimal amount) {
        System.out.println("\n--- 场景二:REQUIRES_NEW 传播 ---");
        System.out.println("[外部事务] 开始下单...");

        try {
            // 1. 创建订单,调用 createOrder 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务
            createOrder(userId, productId, amount);

            // 2. 扣减库存,调用 reduceStock 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务
            reduceStock(productId);

            // 3. 扣减余额 (这里会模拟失败)
            deductBalance(userId, amount);

            System.out.println("[外部事务] 下单成功!");
        } catch (Exception e) {
            System.err.println("[外部事务] 下单失败: " + e.getMessage());
            // 无论成功失败,都要记录一条日志
            recordLog(userId, productId, "下单失败");
        }
    }

    // ============== 内部私有方法 ==============

    private void createOrder(Long userId, Long productId, BigDecimal amount) {
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setAmount(amount);
        orderMapper.insert(order);
        System.out.println("[内部方法] 创建订单成功,订单ID: " + order.getId());
    }

    @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务
     void reduceStock(Long productId) {
        Product product = productMapper.selectById(productId);
        if (product.getStock() <= 0) {
            throw new RuntimeException("库存不足!");
        }
        product.setStock(product.getStock() - 1);
        productMapper.updateById(product);
        System.out.println("[内部方法] 扣减库存成功,剩余库存: " + product.getStock());
    }

    @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务
     void deductBalance(Long userId, BigDecimal amount) {
        Users user = usersMapper.selectById(userId);
        if (user.getBalance().compareTo(amount) < 0) {
            throw new RuntimeException("用户余额不足!");
        }
        user.setBalance(user.getBalance().subtract(amount));
        usersMapper.updateById(user);
        System.out.println("[内部方法] 扣减余额成功,用户余额: " + user.getBalance());
        
        // 模拟一个系统异常,导致事务回滚
        if (amount.compareTo(new BigDecimal("5000")) > 0) {
            throw new RuntimeException("模拟系统异常:大额订单需要人工审核!");
        }
    }

    /**
     * 记录日志,使用 REQUIRES_NEW 开启一个独立的事务
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordLog(Long userId, Long productId, String status) {
        System.out.println("[独立事务] 开始记录日志...");
        Order logEntry = new Order(); // 借用Order表做日志
        logEntry.setUserId(userId);
        logEntry.setProductId(productId);
        // 在订单表中金额 -1 表示日志,执行一次你会在订单表中看到两条记录,一条是订单,另一条是日志
        logEntry.setAmount(new BigDecimal("-1"));
        // 这里可以设置一个状态字段,为了简化,我们就不加了
        logMapper.insert(logEntry);
        System.out.println("[独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。");
    }
}

启动类:

java 复制代码
package com.feng.springtransactiondemo.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

import com.feng.springtransactiondemo.domain.Order;
import com.feng.springtransactiondemo.domain.Product;
import com.feng.springtransactiondemo.domain.Users;
import com.feng.springtransactiondemo.mapper.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private UsersMapper usersMapper;

    // 用于记录日志的Mapper,我们直接用OrderMapper来模拟一个日志表
    @Autowired
    private OrderMapper logMapper; // 假设这是一个独立的日志Mapper

    // 使用 @Lazy 是一个好习惯,可以防止在某些Bean初始化顺序下可能出现的循环依赖问题
    @Autowired
    @Lazy
    private OrderService self; // 注入的是被Spring AOP代理过的对象


    /**
     * 场景一:REQUIRED (默认值)
     * 外部方法事务,内部方法加入外部事务。任何一个失败,整体回滚。
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void placeOrder_REQUIRED(Long userId, Long productId, BigDecimal amount) {
        System.out.println("\n--- 场景一:REQUIRED 传播 ---");
        System.out.println("[外部事务] 开始下单...");

        // 1. 创建订单
        createOrder(userId, productId, amount);

        // 2. 扣减库存
        reduceStock(productId);

        // 3. 扣减余额 (这里会模拟失败)
        deductBalance(userId, amount);

        System.out.println("[外部事务] 下单成功!");
    }

    /**
     * 场景二:REQUIRES_NEW
     * 内部方法开启一个新事务 REQUIRES_NEW,独立于外部事务。即使外部事务回滚,内部事务也提交。
     */
    @Transactional(propagation = Propagation.REQUIRED)  // 外部事务设置为 REQUIRED,表示如果当前存在事务,就加入该事务
    public void placeOrder_REQUIRES_NEW(Long userId, Long productId, BigDecimal amount) {
        System.out.println("\n--- 场景二:REQUIRES_NEW 传播 ---");
        System.out.println("[外部事务] 开始下单...");

        try {
            // 1. 创建订单,调用 createOrder 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务
            createOrder(userId, productId, amount);

            // 2. 扣减库存,调用 reduceStock 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务
            reduceStock(productId);

            // 3. 扣减余额 (这里会模拟失败) , 这里的异常会开启新的子事务,子事务吞掉了一场,外部异常就没有回滚了
            deductBalance(userId, amount);

            System.out.println("[外部事务] 下单成功!uuid");
        } catch (Exception e) {
            System.err.println("[外部事务] 下单失败: " + e.getMessage());
            // 这里一定使用 self , 否则在同一个对象内,AOP 代理不会触发,自然不会开启一个新事务。
            // 只有走了 AOP 代理,才会让 @Transactional 注解生效,从而开启recordLog的子事务。
            self.recordLog(userId, productId, "下单失败");
            // 抛出异常,重新触发外部事务回滚
            throw e;
        }
    }

    // ============== 内部私有方法 ==============

    private void createOrder(Long userId, Long productId, BigDecimal amount) {
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setAmount(amount);
        orderMapper.insert(order);
        System.out.println("[内部方法] 创建订单成功,订单ID: " + order.getId());
    }

    @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务
     void reduceStock(Long productId) {
        Product product = productMapper.selectById(productId);
        if (product.getStock() <= 0) {
            throw new RuntimeException("库存不足!");
        }
        product.setStock(product.getStock() - 1);
        productMapper.updateById(product);
        System.out.println("[内部方法] 扣减库存成功,剩余库存: " + product.getStock());
    }

    @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务
     void deductBalance(Long userId, BigDecimal amount) {
        Users user = usersMapper.selectById(userId);
        if (user.getBalance().compareTo(amount) < 0) {
            throw new RuntimeException("用户余额不足!");
        }
        user.setBalance(user.getBalance().subtract(amount));
        usersMapper.updateById(user);
        System.out.println("[内部方法] 扣减余额成功,用户余额: " + user.getBalance());
        
        // 模拟一个系统异常,导致事务回滚
        if (true) {
            throw new RuntimeException("模拟系统异常:大额订单需要人工审核!");
        }
    }

    /**
     * 记录日志,使用 REQUIRES_NEW 开启一个独立的事务
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordLog(Long userId, Long productId, String status) {
        System.out.println("[独立事务] 开始记录日志...");
        Order logEntry = new Order(); // 借用Order表做日志
        logEntry.setUserId(userId);
        logEntry.setProductId(productId);
        // 在订单表中金额 -1 表示日志,执行一次你会在订单表中看到两条记录,一条是订单,另一条是日志
        logEntry.setAmount(new BigDecimal("-1"));
        // 这里可以设置一个状态字段,为了简化,我们就不加了
        logMapper.insert(logEntry);
        System.out.println("[独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。");
    }
}

最终日志:

java 复制代码
========================================
Spring 事务传播行为实战演示
========================================

========== 场景一:REQUIRED 传播 ==========
--- 重置数据 ---
用户余额、商品库存、订单记录已重置。

--- 场景一:REQUIRED 传播 ---
[外部事务] 开始下单...
[内部方法] 创建订单成功,订单ID: 1
[内部方法] 扣减库存成功,剩余库存: 9
[内部方法] 扣减余额成功,用户余额: 2000.00
主程序捕获到异常: 模拟系统异常:大额订单需要人工审核!
--- 查看最终状态 ---
用户余额: 8000.00      <-- 余额回滚了
商品库存: 10           <-- 库存回滚了
订单记录数量: 0        <-- 订单也回滚了
-------------------

========== 场景二:REQUIRES_NEW 传播 ==========
--- 重置数据 ---
用户余额、商品库存、订单记录已重置。

--- 场景二:REQUIRES_NEW 传播 ---
[外部事务] 开始下单...
[内部方法] 创建订单成功,订单ID: 2
[内部方法] 扣减库存成功,剩余库存: 9
[内部方法] 扣减余额成功,用户余额: 2000.00
[外部事务] 下单失败: 模拟系统异常:大额订单需要人工审核!
[独立事务] 开始记录日志...
[独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。
主程序捕获到异常: 模拟系统异常:大额订单需要人工审核!
--- 查看最终状态 ---
用户余额: 8000.00      <-- 余额回滚了
商品库存: 10           <-- 库存回滚了
订单记录数量: 1        <-- 注意!这里有一条记录,就是日志!
订单记录: [Order{id=3, ...}] 
-------------------
结果分析
  • 场景一 ( **REQUIRED**)

    • **placeOrder_REQUIRED** 开启了一个外部事务。
    • **reduceStock****deductBalance** 都加入了这个外部事务。
    • **deductBalance** 抛出异常时,整个外部事务被标记为回滚。
    • 结果 :创建订单、扣减库存、扣减余额这三个操作全部被回滚。数据库恢复到下单前的状态。这保证了业务的原子性。
  • 场景二 ( **REQUIRES_NEW**)

    • **placeOrder_REQUIRES_NEW** 开启了外部事务。
    • **deductBalance** 抛出异常,外部事务被标记为回滚。
    • **catch** 块中,调用了 **recordLog** 方法。因为它使用了 **REQUIRES_NEW**,它会挂起 当前失败的外部事务,并开启一个全新的、独立的事务
    • **recordLog** 执行成功,新事务被提交
    • **recordLog** 执行完毕,被挂起的外部事务恢复,然后执行回滚。
    • 结果 :下单相关的操作(订单、库存、余额)全部回滚,但 **recordLog** 的操作被成功提交 。这对于需要记录失败日志的场景非常有用,即使主业务失败被回滚了,子事务也能保留关键的错误信息。
有意思的异常

在最后的例子中,场景二发生了异常交给 catch 处理,如果在catch中,你不抛出异常给外部,那么本次事务算成功!发生了异常事务还算做成功?这是为什么呢?

(关注最后的 throw e; 如果没有它,主业务会正常提交,不会回滚,结果不符合预期)

java 复制代码
     /**
     * 场景二:REQUIRES_NEW
     * 内部方法开启一个新事务 REQUIRES_NEW,独立于外部事务。即使外部事务回滚,内部事务也提交。
     */
    @Transactional(propagation = Propagation.REQUIRED)  // 外部事务设置为 REQUIRED,表示如果当前存在事务,就加入该事务
    public void placeOrder_REQUIRES_NEW(Long userId, Long productId, BigDecimal amount) {
        System.out.println("\n--- 场景二:REQUIRES_NEW 传播 ---");
        System.out.println("[外部事务] 开始下单...");

        try {
            // 1. 创建订单,调用 createOrder 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务
            createOrder(userId, productId, amount);

            // 2. 扣减库存,调用 reduceStock 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务
            reduceStock(productId);

            // 3. 扣减余额 (这里会模拟失败) , 这里的异常会开启新的子事务,子事务吞掉了一场,外部异常就没有回滚了
            deductBalance(userId, amount);

            System.out.println("[外部事务] 下单成功!uuid");
        } catch (Exception e) {
            System.err.println("[外部事务] 下单失败: " + e.getMessage());
            // 这里一定使用 self , 否则在同一个对象内,AOP 代理不会触发,自然不会开启一个新事务。
            // 只有走了 AOP 代理,才会让 @Transactional 注解生效,从而开启recordLog的子事务。
            self.recordLog(userId, productId, "下单失败");
            // 抛出异常,重新触发外部事务回滚
            throw e;
        }
    }

这里要了解异常的生命周期了,如果异常被 catch 处理,那么异常就销毁了,程序又恢复正常健康的继续执行,只不过try里面的代码块一定没有全部执行完。

分析结果

  1. 异常产生deductBalance 方法内抛出一个 RuntimeException,这个异常向上传播,被 placeOrder_REQUIRES_NEW 方法中的 catch (Exception e) 块捕获。
java 复制代码
  @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务
     void deductBalance(Long userId, BigDecimal amount) {
        // 其它代码 ..
      
        // 模拟一个系统异常,导致事务回滚
        if (true) {
            throw new RuntimeException("模拟系统异常:大额订单需要人工审核!");
        }
    }
  1. 异常被"处理" :一旦异常进入 catch 块,从 JVM 和调用栈的角度来看,这个异常就已经被"处理"了。程序不会再因为这个异常而崩溃,而是会继续执行 catch 块内部的代码。
  2. **catch** 块执行System.err.println(...)self.recordLog(...) 被执行。注意,self.recordLog 方法本身执行是成功的,它没有抛出任何新的异常。
  3. **catch** 块结束 :如果没有 throw e;catch 块就会正常结束。

事务是AOP实现的,那么此时Spring 的事务管理器(AOP 代理)像一个监控员,它在 placeOrder_REQUIRES_NEW 方法的外部观察着:

  • 它只关心一件事:当 placeOrder_REQUIRES_NEW 方法执行完毕时,它是正常结束的,还是因为抛出异常而结束的。

场景分析:如果没有 **throw e;**

  1. 代理开启事务 T1。
  2. 代理调用目标方法 placeOrder_REQUIRES_NEW
  3. 方法内部发生异常,但被 catch 块捕获了。
  4. self.recordLog 在新事务 T2 中成功执行并提交。
  5. catch 块执行完毕,placeOrder_REQUIRES_NEW 方法正常结束。
  6. 代理看到方法正常结束,它认为:"太好了,一切顺利,没有发生任何错误!"
  7. 于是,代理提交事务 T1。
  8. 结果:订单、库存、余额的修改全部被保存,这与我们期望的"失败回滚"背道而驰。

场景分析:如果有 **throw e;**

  1. 代理开启事务 T1。
  2. 代理调用目标方法 placeOrder_REQUIRES_NEW
  3. 方法内部发生异常,被 catch 块捕获。
  4. self.recordLog 在新事务 T2 中成功执行并提交。
  5. catch 块执行到 throw e;,将之前捕获的那个异常重新抛出。
  6. placeOrder_REQUIRES_NEW 方法因为抛出异常而结束。
  7. 代理看到方法因异常而结束,它认为:"糟糕,出问题了,需要执行回滚操作!"
  8. 于是,代理回滚事务 T1。
  9. 结果:T1 中的所有操作(订单、库存、余额)都被撤销,而 T2 中的日志记录因为已经提交而保留下来。这正是我们想要的结果。
相关推荐
无敌的牛2 小时前
MySQL的开始,MySQL的安装
数据库·mysql
Zxxxxxy_3 小时前
【MYSQL】增删改查
java·数据库·mysql
菜鸟的迷茫3 小时前
线程池中的坑:线程数配置不当导致任务堆积与拒绝策略失效
java·后端
缺点内向3 小时前
Java 使用 Spire.XLS 库合并 Excel 文件实践
java·开发语言·excel
asdfsdgss3 小时前
多项目共享资源:Ruby 定时任务基于 Whenever 的动态扩缩容
java·网络·ruby
木辰風3 小时前
如何在MySQL中搜索JSON数据,并去除引号
数据库·mysql·json
zzhongcy3 小时前
分库分表详解,以及ShardingJDBC介绍
数据库·oracle
Deamon Tree3 小时前
Redis的过期策略以及内存淘汰机制
java·数据库·redis·缓存
Jing_jing_X3 小时前
Java 多线程:从单体到分布式的演进与陷阱
java·分布式