精选

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

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

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

img

可以看出来,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 脚本实现原子操作 。
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 将丧失事务能力。

img

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 插件生成代码:

img

img

img

实体类:

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("[独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。"); } }

最终日志:

REQUIRED 传播是默认传播行为,子事务加入当前事务。REQUIRES_NEW 是子事务不加入当前事务,开启新事务。

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** 的操作被成功提交。这对于需要记录失败日志的场景非常有用,即使主业务失败被回滚了,子事务也能保留关键的错误信息。

img

有意思的异常

在最后的例子中,场景二发生了异常交给 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 中的日志记录因为已经提交而保留下来。这正是我们想要的结果。
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP