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 中的日志记录因为已经提交而保留下来。这正是我们想要的结果。
相关推荐
架构师沉默16 分钟前
别又牛逼了!AI 写 Java 代码真的行吗?
java·后端·架构
DolphinDB1 小时前
集成 Prometheus 与 DolphinDB 规则引擎,构建敏捷监控解决方案
数据库
IvorySQL1 小时前
PostgreSQL 技术日报 (3月10日)|IIoT 性能瓶颈与内核优化新讨论
数据库·postgresql·开源
Java水解3 小时前
微服务架构下Spring Session与Redis分布式会话实战全解析
后端·spring
DBA小马哥5 小时前
时序数据库是什么?能源行业国产化替换的入门必看
数据库·时序数据库
后端AI实验室5 小时前
我把一个生产Bug的排查过程,交给AI处理——20分钟后我关掉了它
java·ai
凉年技术7 小时前
Java 实现企业微信扫码登录
java·企业微信
爱可生开源社区7 小时前
某马来西亚游戏公司如何从 SQL Server 迁移至 OceanBase?
数据库
狂奔小菜鸡8 小时前
Day41 | Java中的锁分类
java·后端·java ee
hooknum8 小时前
学习记录:基于JWT简单实现登录认证功能-demo
java