目录
[一、事务基础:先搞懂 "为什么需要它"](#一、事务基础:先搞懂 “为什么需要它”)
[1. 入门理解:用 "转账" 讲透事务](#1. 入门理解:用 “转账” 讲透事务)
[2. 专业解析:事务的 ACID 特性(必知)](#2. 专业解析:事务的 ACID 特性(必知))
[3. 实际场景:除了转账,还有这些地方需要事务](#3. 实际场景:除了转账,还有这些地方需要事务)
[二、Spring 事务的两种实现:编程式 vs 声明式](#二、Spring 事务的两种实现:编程式 vs 声明式)
[1. 编程式事务:手动 "做饭",一步都不能少](#1. 编程式事务:手动 “做饭”,一步都不能少)
[专业代码示例(Spring Boot)](#专业代码示例(Spring Boot))
[2.声明式事务:用 "自动炒菜机",一键搞定](#2.声明式事务:用 “自动炒菜机”,一键搞定)
[核心背景:Spring 事务默认回滚规则](#核心背景:Spring 事务默认回滚规则)
[例 1:捕获异常后,重新抛出运行时异常 → 事务回滚](#例 1:捕获异常后,重新抛出运行时异常 → 事务回滚)
[例 2:异常被捕获但不抛出 → 事务提交](#例 2:异常被捕获但不抛出 → 事务提交)
[例 3:异常被捕获后,直接抛出(等价于不捕获)→ 事务回滚](#例 3:异常被捕获后,直接抛出(等价于不捕获)→ 事务回滚)
[例 4:手动调用回滚方法 → 事务回滚](#例 4:手动调用回滚方法 → 事务回滚)
[例 5:抛出受检异常(未配置 rollbackFor)→ 事务提交](#例 5:抛出受检异常(未配置 rollbackFor)→ 事务提交)
[关键概念:受检异常 vs 运行时异常](#关键概念:受检异常 vs 运行时异常)
[例 6:用 @SneakyThrows 隐藏受检异常,实际转为 RuntimeException → 事务回滚](#例 6:用 @SneakyThrows 隐藏受检异常,实际转为 RuntimeException → 事务回滚)
[例 7:用 rollbackFor 指定 "受检异常也回滚" → 事务回滚](#例 7:用 rollbackFor 指定 “受检异常也回滚” → 事务回滚)
[小心坑:这 3 种情况事务会失效!](#小心坑:这 3 种情况事务会失效!)
[7 个例子总结](#7 个例子总结)
[三、@Transactional 注解 ------ 声明式事务的 "魔法开关"](#三、@Transactional 注解 —— 声明式事务的 “魔法开关”)
[1、@Transactional 能做什么?](#1、@Transactional 能做什么?)
[2、@Transactional 的核心属性](#2、@Transactional 的核心属性)
[3、用法示例:给方法 "插电",秒变事务方法](#3、用法示例:给方法 “插电”,秒变事务方法)
[4、注意:@Transactional 的 "生效规则"](#4、注意:@Transactional 的 “生效规则”)
一、事务基础:先搞懂 "为什么需要它"
在学 Spring 事务前,我们得先明确:事务到底是什么?解决了什么问题?
1. 入门理解:用 "转账" 讲透事务
假设你给朋友转 100 元,整个过程分两步:
- 你的账户扣 100 元(
A-100); - 朋友的账户加 100 元(
B+100)。
如果没有事务:第一步成功了,但第二步突然报错(比如网络断了),结果就是 "你的钱少了,朋友的钱没多"------ 钱平白消失了。
有了事务后:这两步会被 "捆成一个整体",要么全成功(转账完成),要么全失败(你的钱不扣,朋友的钱不加),绝对不会出现 "一半成功一半失败" 的情况。
一句话总结:事务是 "一组不可分割的操作",要么全成,要么全败。
2. 专业解析:事务的 ACID 特性(必知)
对专业开发者而言,事务的核心是ACID 特性,这是判断事务是否可靠的标准:
- 原子性(Atomicity):操作要么全执行,要么全回滚(如转账的两步不能拆分);
- 一致性(Consistency):事务执行前后,数据总量守恒(如转账前 A+B=1000,执行后还是 1000);
- 隔离性(Isolation):多个事务同时执行时,互不干扰(比如你转账的同时,朋友也在给别人转账,不会互相影响);
- 持久性(Durability):事务提交后,数据永久保存在数据库(即使数据库崩溃,重启后数据也在)。
3. 实际场景:除了转账,还有这些地方需要事务
- 秒杀系统:"下单" 和 "扣库存" 必须在一个事务里,避免 "超卖"(下单成功但库存没扣);
- 用户注册:"插入用户数据" 和 "记录注册日志" 必须同时成功,否则用户存在但日志缺失,后续查问题找不到依据;
- 订单支付:"扣余额""生成订单""减库存" 三步绑定,缺一步都不行。
二、Spring 事务的两种实现:编程式 vs 声明式
Spring 对事务的支持分两类,前者灵活但繁琐,后者简单且常用,我们分别来看。
1. 编程式事务:手动 "做饭",一步都不能少
编程式事务就像 "自己做饭":买菜、切菜、炒菜、洗碗,每一步都要手动操作。它的核心是通过DataSourceTransactionManager(事务管理器)手动控制事务的 "开启、提交、回滚"。
专业代码示例(Spring Boot)
@RestController
@RequestMapping("/user")
public class UserController {
// 1. 注入事务管理器(负责开启/提交/回滚事务)
@Autowired
private DataSourceTransactionManager transactionManager;
// 2. 注入事务属性(定义事务的隔离级别、超时时间等)
@Autowired
private TransactionDefinition transactionDefinition;
// 3. 注入业务层
@Autowired
private UserService userService;
@RequestMapping("/registry")
public String registry(String name, String password) {
// 开启事务:获取事务状态(相当于"打开燃气灶")
TransactionStatus status = transactionManager.getTransaction(transactionDefinition);
try {
// 执行核心业务(相当于"炒菜")
userService.registryUser(name, password);
// 提交事务(相当于"关火,完成做饭")
transactionManager.commit(status);
return "注册成功";
} catch (Exception e) {
// 回滚事务(如果炒菜糊了,"倒掉重做")
transactionManager.rollback(status);
return "注册失败";
}
}
}
优缺点分析
- 优点:完全手动控制,灵活(比如可根据不同异常决定是否回滚);
- 缺点:代码冗余(每个事务方法都要写开启 / 提交 / 回滚),不符合 "开闭原则";
- 适用场景:极少用,仅在需要精细控制事务时(如多数据源切换场景)。
2.声明式事务:用 "自动炒菜机",一键搞定

在了解 @Transactional 基础用法后,"异常处理" 与 "事务回滚" 的配合是新手(甚至老手)最易踩坑的点。下面通过 7 个实战例子,结合「代码 + 结果(提交 / 回滚)+ 日志对比」,彻底讲清事务回滚规则。
核心背景:Spring 事务默认回滚规则
Spring 事务默认只对 RuntimeException(运行时异常)和 Error 自动回滚 ,对「受检异常」(如 IOException、SQLException 等编译器强制处理的异常)不回滚。若要让 "受检异常也能触发回滚",需用 @Transactional(rollbackFor = 异常类.class) 显式配置。
例 1:捕获异常后,重新抛出运行时异常 → 事务回滚
@Transactional
@RequestMapping("/r1")
public Boolean r1(String userName, String password) {
Integer result = userService.insert(userName, password);
System.out.println("插入用户表,result:" + result);
try {
int a = 10 / 0; // 触发「算术异常」(属于RuntimeException)
} catch (Exception e) {
// 关键:捕获后,重新抛出RuntimeException
throw new RuntimeException("发生算术异常,事务将回滚", e);
}
return true;
}
逻辑分析
int a = 10 / 0会抛出ArithmeticException(属于RuntimeException);- 虽用
try-catch捕获异常,但重新抛出了RuntimeException; - 符合 Spring 「默认对
RuntimeException回滚」的规则。
结果:事务回滚
对应日志会显示 "回滚" 核心信息(类似如下行):
Transaction synchronization deregistering SqlSession [...]
Transaction synchronization closing SqlSession [...]
例 2:异常被捕获但不抛出 → 事务提交
@Transactional
@RequestMapping("/registry")
public Boolean registry(String userName, String password) {
Integer result = userService.insert(userName, password);
System.out.println("插入用户表,result:" + result);
try {
int a = 10 / 0; // 触发算术异常(RuntimeException)
} catch (Exception e) {
e.printStackTrace(); // 仅打印异常,不重新抛出
}
return true;
}
逻辑分析
- 异常被
try-catch捕获后,没有重新抛出任何异常; - Spring 认为方法 "正常执行完毕",因此自动提交事务。
结果:事务提交
对应日志会显示 **"提交"** 核心信息(类似如下行):
Transaction synchronization committing SqlSession [...]
Transaction synchronization deregistering SqlSession [...]
例 3:异常被捕获后,直接抛出(等价于不捕获)→ 事务回滚
@Transactional
@RequestMapping("/r2")
public Boolean r2(String userName, String password){
Integer result = userService.insert(userName, password);
System.out.println("插入用户表, result: "+ result);
try {
int a = 10/0; // 触发算术异常(RuntimeException)
}catch (Exception e){
throw e; // 直接抛出捕获的异常(RuntimeException)
}
return true;
}
逻辑分析
- 虽用
try-catch捕获异常,但直接把异常抛出去了; - 抛出的
ArithmeticException是RuntimeException,符合 Spring 自动回滚规则。
结果:事务回滚
日志显示 "回滚" 相关信息,与「例子 1」的回滚日志一致。
例 4:手动调用回滚方法 → 事务回滚
@Transactional
@RequestMapping("/r3")
public Boolean r3(String userName, String password){
Integer result = userService.insert(userName, password);
System.out.println("插入用户表, result: "+ result);
try {
int a = 10/0; // 触发算术异常(RuntimeException)
}catch (Exception e){
// 关键:手动标记"事务需要回滚"
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return true;
}
逻辑分析
- 即使异常被捕获且不抛出,只要通过
TransactionAspectSupport的setRollbackOnly()手动标记 "需要回滚",事务就会回滚。
结果:事务回滚
日志显示 "回滚" 相关信息,与「例子 1」的回滚日志一致。
例 5:抛出受检异常(未配置 rollbackFor)→ 事务提交
@Transactional
@RequestMapping("/r4")
public Boolean r4(String userName, String password) throws IOException {
Integer result = userService.insert(userName, password);
System.out.println("插入用户表, result: "+ result);
if (true){
throw new IOException(); // 抛出「受检异常」(非RuntimeException)
}
return true;
}
关键概念:受检异常 vs 运行时异常
- 受检异常 :编译器强制要求处理的异常(如
IOException),必须用try-catch或throws声明; - 运行时异常 :编译器不强制处理的异常(如
NullPointerException),继承自RuntimeException。
逻辑分析
IOException是受检异常 ,不属于 Spring 默认回滚的RuntimeException/Error;- 方法仅加了
@Transactional(未配置rollbackFor),因此 Spring 认为 "异常已被throws声明,方法正常执行",提交事务。
结果:事务提交
日志显示 "提交" 相关信息,与「例子 2」的提交日志一致。
例 6:用 @SneakyThrows 隐藏受检异常,实际转为 RuntimeException → 事务回滚
@SneakyThrows // Lombok注解,将受检异常包装为RuntimeException
@Transactional
@RequestMapping("/r5")
public Boolean r5(String userName, String password) {
Integer result = userService.insert(userName, password);
System.out.println("插入用户表, result: "+ result);
if (true){
throw new IOException(); // 受检异常
}
return true;
}
逻辑分析
@SneakyThrows是 Lombok 的 "黑魔法":它会把受检异常(如IOException)偷偷包装成RuntimeException的子类抛出;- Spring 看到的是
RuntimeException,因此自动回滚事务。
结果:事务回滚
日志显示 "回滚" 相关信息,与「例子 1」的回滚日志一致。
例 7:用 rollbackFor 指定 "受检异常也回滚" → 事务回滚
@Transactional(rollbackFor = Exception.class, isolation = Isolation.DEFAULT)
@RequestMapping("/r7")
public Boolean r7(String userName, String password) throws IOException {
Integer result = userService.insert(userName, password);
System.out.println("插入用户表, result: "+ result);
if (true){
throw new IOException(); // 受检异常
}
return true;
}
关键配置:rollbackFor
@Transactional(rollbackFor = Exception.class) 表示:只要方法抛出 Exception 及其子类异常(包括受检异常),事务就回滚。
逻辑分析
- 虽
IOException是受检异常,但通过rollbackFor = Exception.class显式配置了 "所有Exception都触发回滚"; - 方法抛出
IOException后,Spring 检测到符合回滚条件,回滚事务。
结果:事务回滚
日志显示 "回滚" 相关信息,与「例子 1」的回滚日志一致。
小心坑:这 3 种情况事务会失效!
-
异常被手动捕获了 :如果用
try-catch捕获了异常且不重新抛出,事务会认为方法执行成功,不会回滚;@Transactional public void registryUser(String name, String password) { try { userMapper.insert(name, password); int a = 10 / 0; // 异常被捕获,事务不回滚! } catch (Exception e) { e.printStackTrace(); // 仅打印异常,不抛出去 } }解决:要么不捕获异常,要么捕获后重新抛出(
throw e;),或手动回滚(TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();)。 -
修饰非 public 方法 :
@Transactional仅对public方法生效(非 public 方法不会被 AOP 代理); -
数据源没配置事务管理器 :如果 Spring 没扫描到
DataSourceTransactionManager,事务注解会 "形同虚设"(Spring Boot 默认自动配置,手动配置时需注意)。
7 个例子总结
| 例子 | 核心场景 | 事务结果 | 关键原因 |
|---|---|---|---|
| 1 | 捕获异常后,重新抛 RuntimeException |
回滚 | Spring 默认对 RuntimeException 回滚 |
| 2 | 捕获异常后,不抛出任何异常 | 提交 | Spring 认为方法 "正常执行完毕" |
| 3 | 捕获后直接抛出 RuntimeException |
回滚 | 等价于 "没捕获异常",触发 Spring 自动回滚 |
| 4 | 手动调用 setRollbackOnly |
回滚 | 强制标记 "事务需要回滚" |
| 5 | 抛出受检异常(无 rollbackFor) |
提交 | Spring 默认仅对 RuntimeException/Error 回滚,受检异常不触发 |
| 6 | @SneakyThrows 包装受检异常为 RuntimeException |
回滚 | Lombok 将受检异常转为 RuntimeException,触发默认回滚 |
| 7 | 用 rollbackFor 指定受检异常需回滚 |
回滚 | 显式配置 rollbackFor = Exception.class,让受检异常也能触发回滚 |
通过这 7 个例子,能清晰发现:事务是否回滚,核心取决于「异常类型」和「是否被 Spring 检测到需要回滚」。
三、@Transactional 注解 ------ 声明式事务的 "魔法开关"
在 Spring 声明式事务中,@Transactional 是最核心的注解。它像一个 "智能开关",能让方法自动具备事务能力(无需手动写开启 / 提交 / 回滚代码),还能通过 "属性" 精细控制事务的隔离级别、传播机制、回滚规则等。
1、@Transactional 能做什么?
想象你要做一道 "事务菜"(如 "转账"):
- 手动做(编程式事务):得自己买菜、切菜、炒菜、洗碗,每一步都要管;
- 用
@Transactional(声明式事务):把食材放进 "自动炒菜机",按下开关(加注解),机器自动完成炒菜(执行业务)、关火(提交事务)、翻车自动重做(异常回滚)。
2、@Transactional 的核心属性
通过注解的 "属性",你能像 "调炒菜机参数" 一样,控制事务的行为:
| 属性名 | 作用 | 生活类比(炒菜机) | 常用值示例 |
|---|---|---|---|
isolation |
控制事务隔离级别(解决 "多事务同时炒同一道菜,互相干扰" 的问题) | 控制 "是否允许别人在你炒菜时动锅" | Isolation.READ_COMMITTED(读已提交) |
propagation |
控制事务传播机制(解决 "一个事务方法调用另一个事务方法,锅怎么共用" 的问题) | 控制 "新菜用新锅还是旧锅" | Propagation.REQUIRED(默认,有锅用旧锅,没锅买新锅) |
rollbackFor |
指定哪些异常会让事务回滚 (默认只有 RuntimeException 才会触发回滚) |
控制 "炒糊到什么程度,会自动倒掉重做" | rollbackFor = Exception.class(所有异常都重做) |
3、用法示例:给方法 "插电",秒变事务方法
只需在public 方法 (或类)上添加 @Transactional,方法就会被 Spring "代理",自动具备事务能力。
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockMapper stockMapper;
// 给"创建订单"方法加事务:下单和扣库存必须同时成功/失败
@Transactional(
isolation = Isolation.READ_COMMITTED, // 隔离级别:读已提交
propagation = Propagation.REQUIRED, // 传播机制:默认(有事务则加入,无则新建)
rollbackFor = Exception.class // 所有Exception都触发回滚
)
public void createOrder(OrderDTO order) {
orderMapper.insert(order); // 步骤1:插入订单(核心业务)
stockMapper.decrease(order.getGoodsId()); // 步骤2:扣减库存(核心业务)
// 如果下面抛异常,上面两步会自动回滚
// int a = 10 / 0;
}
}
4、注意:@Transactional 的 "生效规则"
用不好这个注解,容易出现 "事务看似加了,实际没生效" 的问题,记住这 3 点:
-
仅对 public 方法生效
非 public 方法(如
private)加@Transactional,事务不会生效 ------ 因为 Spring AOP 代理是基于 "public 方法调用" 实现的。 -
方法调用要走 "代理"
如果在同一个类中 ,用 "普通方法" 调用 "加了
@Transactional的方法",事务也不生效 ------ 因为绕过了 Spring 的代理逻辑。(解决:把被调用的事务方法放到另一个 Bean 中,通过 Bean 注入调用)
-
异常处理要匹配
rollbackFor如果方法捕获了异常但不抛出,或异常类型不匹配
rollbackFor,事务可能不会回滚(参考上一节 "7 个实战例子")。