当前位置: 首页 > news >正文

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 脚本实现原子操作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 插件生成代码:

img

img

img

配置文件:

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)

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

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

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

-- 商品表
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里面的代码块一定没有全部执行完。

分析结果

  1. 异常产生deductBalance 方法内抛出一个 RuntimeException,这个异常向上传播,被 placeOrder_REQUIRES_NEW 方法中的 catch (Exception e) 块捕获。
  @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 中的日志记录因为已经提交而保留下来。这正是我们想要的结果。
http://www.dtcms.com/a/541920.html

相关文章:

  • 3 VTK中的数据结构
  • IROS 2025 视触觉结合磁硅胶的MagicGel传感器
  • BETAFLIGHT CLI教程 带有串口软件进入cli模式教程
  • 做临时工看哪个网站网上服务系统
  • 哪家做网站性价比高如何做外贸业务
  • 做网站注册公司一个空间可以做几个网站
  • 网站建设咨询服务合同wordpress国外主题修改
  • 做淘宝要用的网站手游网站源码下载
  • Mysql基础知识之SQL语句——库表管理操作
  • 类似于建设通的网站巴中做网站 微信开发
  • dede装修网站模板预付网站建设服务费如何入账
  • Python异常处理详解:从概念到实战,让程序优雅应对错误
  • 长沙做网站推荐网站显示iis7
  • elasticSearch之API:索引操作
  • 网站建设官网怎么收费哪个网站可以做竖屏
  • 苏州做网站哪家比较好移动端快速排名
  • 做淘宝网站要多少钱如何申请域名邮箱
  • dede网站地图文章变量网站开发课程心得
  • 网站模板绑定域名中国交通建设集团有限公司英文名
  • BeautifulSoup 的页面中需要获取某个元素的 xpath 路径
  • 网站数字证书怎么做辽宁省建设工程注册中心网站
  • 网站开发 总结报告网站的版面设计
  • 网站策划素材网站备份流程
  • 最成功的个人网站新民电子网站建设哪家好
  • 十堰网站搜索优化价格网站建设流程新闻
  • 厦门网站制作公司创网网站后台管理系统
  • 汕头网站制作推荐小程序制作报价
  • 网站建设工作室 杭州网站建设方案论文1500
  • 6.1类的继承
  • 广东集团网站建设安徽网络seo