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 声明事务过时了。
常用注解:
@Transactional
public void transferMoney() {accountDao.debit("A", 100);accountDao.credit("B", 100);
}
- 默认是 运行时异常(RuntimeException)回滚
- 可以通过属性定制行为:
@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 手动控制事务。比声明式事务更灵活、可控,但是代码入侵高。
示例:
@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() 方法,这就涉及到事务传播的知识范畴,事务传播有七种行为。
@Transactional(propagation = Propagation.REQUIRED)public void createUserAndOrder() {// 1. 插入用户insertUser();// 2. 调用 OrderServiceorderService.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
场景是经典转账场景,周杰伦给昆凌转账
初始化项目
项目依赖:
<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>
库表设计:
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);
配置文件:
spring:datasource:url: jdbc:mysql://localhost:3306/spring_tx_demo?useSSL=false&serverTimezone=UTCusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Drivermybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # SQL 输出global-config:db-config:id-type: autotable-underline: true
MyBatis X 插件生成代码:



配置文件:
spring:datasource:url: jdbc:mysql://localhost:3306/spring_tx_demo?useSSL=false&serverTimezone=UTCusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Drivermybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # SQL 输出global-config:db-config:id-type: autotable-underline: true
实体类:
@TableName(value ="users")
@Data
public class Users {/*** */@TableId(type = IdType.AUTO)private Integer id;/*** */private String name;/*** */private BigDecimal balance;}
事务基本功能示例
周杰伦给昆凌转账 , 我们模拟两次,一次是转账成功,一次是转账失败回滚,来验证Spring事务是正常工作的。 注意代码不用看的太仔细,一会跟我分析日志,日志是精髓,代码精髓就是一个 @Transactional ,代码逻辑无所谓。
Service 代码:
@Service
public class UsersService {@Autowiredprivate 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 调用。
@SpringBootApplication
@MapperScan("com.feng.springtransactiondemo.mapper")
public class SpringTransactionDemoApplication implements CommandLineRunner {@Autowiredprivate UsersService usersService; // 注入 UsersService@Autowiredprivate UsersMapper usersMapper; // 注入 UsersMapper,用于查询最终结果public static void main(String[] args) {SpringApplication.run(SpringTransactionDemoApplication.class, args);}@Overridepublic 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); // 使用 Usersusers.forEach(user -> System.out.println(user.getName() + " 的余额是: " + user.getBalance()));System.out.println("---------------------------");}}
最终得到日志就很有用了:
========================================
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读未提交
/*** 模拟周杰伦:增加余额,但最后会回滚(事务未成功提交)* @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方法相关代码,用以下的代码:
@Overridepublic 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());}}
最终得到日志是这样的,这一次简化给大家看:
========================================
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)
场景周杰伦下单,下单包含多个步骤,每个步骤单独开启事务,便能演示一个事务调用另一个事务。
- 创建订单
- 扣减库存
- 扣减用户余额
我们需要增加产品表和订单表:
-- 商品表
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 = '周杰伦';
实体类:
@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 类:
@Mapper
public interface OrderMapper extends BaseMapper<Order> {}
@Mapper
public interface ProductMapper extends BaseMapper<Product> {}
对应的mapper.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 类
@Service
public class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate ProductMapper productMapper;@Autowiredprivate UsersMapper usersMapper;// 用于记录日志的Mapper,我们直接用OrderMapper来模拟一个日志表@Autowiredprivate 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("[独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。");}
}
启动类:
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 {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate ProductMapper productMapper;@Autowiredprivate UsersMapper usersMapper;// 用于记录日志的Mapper,我们直接用OrderMapper来模拟一个日志表@Autowiredprivate OrderMapper logMapper; // 假设这是一个独立的日志Mapper// 使用 @Lazy 是一个好习惯,可以防止在某些Bean初始化顺序下可能出现的循环依赖问题@Autowired@Lazyprivate 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("[独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。");}
}
最终日志:
========================================
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; 如果没有它,主业务会正常提交,不会回滚,结果不符合预期)
/*** 场景二: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里面的代码块一定没有全部执行完。
分析结果
- 异常产生:
deductBalance方法内抛出一个RuntimeException,这个异常向上传播,被placeOrder_REQUIRES_NEW方法中的catch (Exception e)块捕获。
@Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务void deductBalance(Long userId, BigDecimal amount) {// 其它代码 ..// 模拟一个系统异常,导致事务回滚if (true) {throw new RuntimeException("模拟系统异常:大额订单需要人工审核!");}}
- 异常被“处理”:一旦异常进入
catch块,从 JVM 和调用栈的角度来看,这个异常就已经被“处理”了。程序不会再因为这个异常而崩溃,而是会继续执行catch块内部的代码。 **catch**块执行:System.err.println(...)和self.recordLog(...)被执行。注意,self.recordLog方法本身执行是成功的,它没有抛出任何新的异常。**catch**块结束:如果没有throw e;,catch块就会正常结束。
事务是AOP实现的,那么此时Spring 的事务管理器(AOP 代理)像一个监控员,它在 placeOrder_REQUIRES_NEW 方法的外部观察着:
- 它只关心一件事:当
placeOrder_REQUIRES_NEW方法执行完毕时,它是正常结束的,还是因为抛出异常而结束的。
场景分析:如果没有 **throw e;**
- 代理开启事务 T1。
- 代理调用目标方法
placeOrder_REQUIRES_NEW。 - 方法内部发生异常,但被
catch块捕获了。 self.recordLog在新事务 T2 中成功执行并提交。catch块执行完毕,placeOrder_REQUIRES_NEW方法正常结束。- 代理看到方法正常结束,它认为:“太好了,一切顺利,没有发生任何错误!”
- 于是,代理提交事务 T1。
- 结果:订单、库存、余额的修改全部被保存,这与我们期望的“失败回滚”背道而驰。
场景分析:如果有 **throw e;**
- 代理开启事务 T1。
- 代理调用目标方法
placeOrder_REQUIRES_NEW。 - 方法内部发生异常,被
catch块捕获。 self.recordLog在新事务 T2 中成功执行并提交。catch块执行到throw e;,将之前捕获的那个异常重新抛出。placeOrder_REQUIRES_NEW方法因为抛出异常而结束。- 代理看到方法因异常而结束,它认为:“糟糕,出问题了,需要执行回滚操作!”
- 于是,代理回滚事务 T1。
- 结果:T1 中的所有操作(订单、库存、余额)都被撤销,而 T2 中的日志记录因为已经提交而保留下来。这正是我们想要的结果。
