目录
[1.1 什么是事务?(代码体现版)](#1.1 什么是事务?(代码体现版))
[1.2 为什么需要事务?(代码场景)](#1.2 为什么需要事务?(代码场景))
[2.1 编程式事务(详细注释版)](#2.1 编程式事务(详细注释版))
[2.2 声明式事务 @Transactional(推荐方式)](#2.2 声明式事务 @Transactional(推荐方式))
[三、@Transactional 注解详解](#三、@Transactional 注解详解)
[3.1 rollbackFor 属性(异常回滚详解)](#3.1 rollbackFor 属性(异常回滚详解))
[3.2 事务隔离级别](#3.2 事务隔离级别)
[3.2.1 MySQL隔离级别回顾(用代码理解)](#3.2.1 MySQL隔离级别回顾(用代码理解))
[3.2.2 Spring设置隔离级别](#3.2.2 Spring设置隔离级别)
[3.3 事务传播机制(重头戏)](#3.3 事务传播机制(重头戏))
[3.3.1 什么是事务传播机制?](#3.3.1 什么是事务传播机制?)
[3.3.2 7种传播机制全览](#3.3.2 7种传播机制全览)
[3.3.3 代码演示(完整带注释版)](#3.3.3 代码演示(完整带注释版))
[3.3.3.1 REQUIRED(加入事务)](#3.3.3.1 REQUIRED(加入事务))
[3.3.3.2 REQUIRES_NEW(新建事务)](#3.3.3.2 REQUIRES_NEW(新建事务))
[3.3.3.3 NEVER(不支持事务)](#3.3.3.3 NEVER(不支持事务))
[3.3.3.4 NESTED(嵌套事务)](#3.3.3.4 NESTED(嵌套事务))
[3.3.3.5 NESTED vs REQUIRED(局部回滚)](#3.3.3.5 NESTED vs REQUIRED(局部回滚))
[4.1 pom.xml(依赖详细注释)](#4.1 pom.xml(依赖详细注释))
[4.2 application.properties(配置详细注释)](#4.2 application.properties(配置详细注释))
[4.3 实体类(详细注释)](#4.3 实体类(详细注释))
[4.4 Mapper接口(详细注释)](#4.4 Mapper接口(详细注释))
[4.5 Service层(详细注释)](#4.5 Service层(详细注释))
[4.6 Controller层(详细注释)](#4.6 Controller层(详细注释))
[4.7 数据库初始化脚本(详细注释)](#4.7 数据库初始化脚本(详细注释))
[5.1 Spring事务实现方式对比](#5.1 Spring事务实现方式对比)
[5.2 @Transactional关键属性代码体现](#5.2 @Transactional关键属性代码体现)
[5.3 事务传播机制总结表](#5.3 事务传播机制总结表)
Q1:为什么要在Service层而不是Controller层使用事务?
[7.1 事务ACID特性的代码体现](#7.1 事务ACID特性的代码体现)
[7.2 事务实现原理(AOP代理)](#7.2 事务实现原理(AOP代理))
[8.1 同类自调用问题(最坑!)](#8.1 同类自调用问题(最坑!))
[8.2 方法修饰符非public](#8.2 方法修饰符非public)
[8.3 方法是final/static](#8.3 方法是final/static)
[8.4 多线程调用问题](#8.4 多线程调用问题)
[8.5 异常被catch未抛出](#8.5 异常被catch未抛出)
[8.6 数据库引擎不支持事务](#8.6 数据库引擎不支持事务)
[8.7 未配置事务管理器](#8.7 未配置事务管理器)
[错误配置(Spring Boot)](#错误配置(Spring Boot))
[8.8 传播机制设置错误](#8.8 传播机制设置错误)
[8.9 总结:事务失效检查清单](#8.9 总结:事务失效检查清单)
Spring事务和事务传播机制:新手超详细指南
我会为你重新梳理整个Spring事务知识体系,重点补充代码与知识点的对应关系 ,并对所有代码进行逐行详细注释。
一、事务核心概念(代码前的准备知识)
1.1 什么是事务?(代码体现版)
事务就是一组"要么一起成功,要么一起失败"的数据库操作。来看一个没有事务的灾难场景:
java
// 没有事务的转账方法(危险!)
public void transferMoney(String fromAccount, String toAccount, int amount) {
// 第一步:A账户扣钱
jdbcTemplate.update("UPDATE account SET balance = balance - ? WHERE user_id = ?", amount, fromAccount);
// 第二步:B账户加钱
// 如果这里发生异常(比如网络断开),A的钱已经扣了,但B没收到!
int i = 10 / 0; // 模拟异常
jdbcTemplate.update("UPDATE account SET balance = balance + ? WHERE user_id = ?", amount, toAccount);
}
对应知识点:这个例子说明,如果没有事务保护,两个操作之间发生异常会导致数据不一致。A账户的钱凭空消失。
1.2 为什么需要事务?(代码场景)
看三个真实业务场景:
场景1:转账操作(上面已演示)
场景2:秒杀系统
java
// 没有事务的秒杀方法(会导致超卖!)
public void seckill(String productId) {
// 第一步:创建订单(成功)
orderMapper.createOrder(productId, userId);
// 第二步:扣减库存(失败)
// 如果库存扣减失败,但订单已创建,就超卖了!
stockMapper.reduceStock(productId); // 假设这里抛异常
}
场景3:订单创建
java
// 没有事务的订单创建(数据不完整)
public void createOrder(Order order) {
// 主订单表插入成功
orderMapper.insertMainOrder(order);
// 订单明细表插入失败(比如某个明细数据格式错误)
orderMapper.insertOrderItems(order.getItems()); // 抛出异常
// 库存更新没执行
// 结果:数据库中只有主订单,没有明细,数据不完整!
}
二、Spring中事务的实现方式
2.1 编程式事务(详细注释版)
这是最原始但最灵活的方式,适合需要精细控制事务的场景。
java
// 1. 导入Spring事务管理核心类
// Spring提供的JDBC事务管理器,负责实际的事务操作(开启、提交、回滚)
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
// 事务定义接口,用于设置事务属性(隔离级别、传播行为、超时时间等)
import org.springframework.transaction.TransactionDefinition;
// 事务状态接口,表示当前事务的状态,用于提交或回滚
import org.springframework.transaction.TransactionStatus;
// Spring提供的模板类,简化编程式事务的写法(后面会介绍)
import org.springframework.transaction.support.TransactionTemplate;
// Spring MVC注解,标识这是一个处理HTTP请求的控制器
import org.springframework.web.bind.annotation.RequestMapping;
// Spring MVC注解,标识控制器返回的数据直接写入HTTP响应体(RESTful风格)
import org.springframework.web.bind.annotation.RestController;
// Spring依赖注入注解,自动将Spring容器中的Bean注入到字段
import org.springframework.beans.factory.annotation.Autowired;
// 业务服务类(处理用户注册逻辑)
import com.example.demo.service.UserService;
/**
* 用户注册控制器 - 演示编程式事务
* 这是最原始的事务管理方式,手动控制事务的每一个步骤
*/
@RequestMapping("/user") // 设置该控制器的基础请求路径为/user
@RestController // 声明这是一个RESTful控制器,所有方法返回JSON数据
public class UserController {
// 2. 注入事务管理器(核心组件)
// DataSourceTransactionManager是Spring提供的JDBC事务管理器
// 它负责:获取数据库连接、开启事务、提交事务、回滚事务
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
// 3. 注入事务定义(设置事务属性)
// TransactionDefinition用于定义事务的行为特性
// 比如:隔离级别、传播行为、超时时间、是否只读等
@Autowired
private TransactionDefinition transactionDefinition;
// 4. 注入用户服务(执行业务逻辑)
// UserService包含具体的业务操作(如插入用户数据)
@Autowired
private UserService userService;
/**
* 用户注册接口 - 使用编程式事务
* @param name 用户名
* @param password 密码
* @return 注册结果
*/
@RequestMapping("/registry") // 映射HTTP请求路径为/user/registry
public String registry(String name, String password) {
// 5. 【核心】开启事务
// getTransaction()方法:根据transactionDefinition的定义,开启一个新事务
// 返回TransactionStatus对象,用于追踪当前事务状态
// 相当于执行SQL: START TRANSACTION;
TransactionStatus transactionStatus =
dataSourceTransactionManager.getTransaction(transactionDefinition);
try {
// 6. 在事务中执行业务逻辑
// 如果这里抛出RuntimeException,事务会自动回滚
userService.registryUser(name, password);
// 7. 【核心】提交事务
// 如果try块中的所有操作都成功,提交事务
// 相当于执行SQL: COMMIT;
// 所有对数据库的修改永久生效
dataSourceTransactionManager.commit(transactionStatus);
return "注册成功";
} catch (Exception e) {
// 8. 【核心】回滚事务
// 如果try块中任何代码抛出异常,捕获后回滚事务
// 相当于执行SQL: ROLLBACK;
// 所有在事务中的数据库操作都撤销,就像从未执行过
dataSourceTransactionManager.rollback(transactionStatus);
// 重新抛出异常,让Spring框架处理(比如返回500错误)
throw e;
}
}
}
知识点与代码的对应关系:
|---------|-------------------------|---------|
| 知识点 | 代码体现 | 关键行 |
| 开启事务 | getTransaction() | 第55行 |
| 提交事务 | commit() | 第65行 |
| 回滚事务 | rollback() | 第72行 |
| 事务属性设置 | TransactionDefinition | 第31行 |
| 事务状态追踪 | TransactionStatus | 第55行 |
2.2 声明式事务 @Transactional(推荐方式)
这是实际开发中最常用的方式,通过注解自动管理事务。
第一步:添加依赖(pom.xml)
XML
<!-- Spring事务管理依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
第二步:使用注解
java
// 导入Spring事务注解
// 声明式事务的核心注解,加在方法或类上自动开启事务管理
import org.springframework.transaction.annotation.Transactional;
// 导入其他必要类
import org.springframework.beans.factory.annotation.Autowired; // 依赖注入
import org.springframework.web.bind.annotation.RequestMapping; // 请求映射
import org.springframework.web.bind.annotation.RestController; // REST控制器
/**
* 用户注册控制器 - 演示声明式事务
* 这种方式无需手动编写事务管理代码,Spring通过AOP自动代理实现
*/
@RequestMapping("/user") // 基础请求路径
@RestController // RESTful控制器
public class TransactionalController {
// 注入用户服务
@Autowired
private UserService userService;
/**
* 用户注册接口 - 声明式事务
* * @Transactional注解的作用:
* 1. 在方法执行前自动开启事务(相当于getTransaction())
* 2. 在方法成功执行后自动提交事务(相当于commit())
* 3. 在方法抛出未捕获异常时自动回滚事务(相当于rollback())
* * 注意:默认只对RuntimeException和Error回滚
*/
@Transactional // 核心注解!加在public方法上才有效
@RequestMapping("/registry") // 映射请求路径
public String registry(String name, String password) {
// Spring会在调用此方法前自动开启事务
// 方法内的所有数据库操作都在同一个事务中
userService.registryUser(name, password); // 插入用户信息
// 如果这里抛出RuntimeException,整个事务会自动回滚
// 模拟异常:int i = 10/0;
return "注册成功"; // 方法正常返回,Spring自动提交事务
}
}
代码与知识点的桥梁:
java
// 编程式事务的繁琐写法
public void oldWay() {
TransactionStatus status = manager.getTransaction(definition); // 手动开启
try {
// 业务代码
manager.commit(status); // 手动提交
} catch(Exception e) {
manager.rollback(status); // 手动回滚
throw e;
}
}
// 声明式事务的简洁写法(Spring AOP自动代理)
@Transactional // 一个注解替代所有手动代码!
public void newWay() {
// 业务代码
// Spring自动处理开启、提交、回滚
}
三、@Transactional 注解详解
3.1 rollbackFor 属性(异常回滚详解)
问题演示代码:
java
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import import java.io.IOException; // 导入IO异常类(检查型异常)
/**
* 演示默认回滚机制的缺陷
*/
@RestController
public class RollbackDemoController {
/**
* 默认事务配置
* 问题:当抛出IOException(检查型异常)时,事务不会回滚!
* 原因:Spring默认只对RuntimeException和Error回滚
*/
@Transactional // 没有指定rollbackFor
@RequestMapping("/r2")
public String r2(String name, String password) throws IOException {
userService.registryUser(name, password); // 插入用户成功
log.info("用户数据插入成功");
// 抛出检查型异常IOException(不是RuntimeException)
// Spring会认为"这不是我需要管的异常",所以不会回滚事务!
if (true) {
throw new IOException("网络异常"); // 数据库操作不会回滚!
}
return "r2";
}
/**
* 正确的事务配置
* 解决方案:通过rollbackFor指定需要回滚的异常类型
*/
@Transactional(
rollbackFor = Exception.class // 指定所有Exception都回滚
// rollbackFor = {IOException.class, SQLException.class} // 也可以指定多个
)
@RequestMapping("/r3")
public String r3(String name, String password) throws IOException {
userService.registryUser(name, password);
log.info("用户数据插入成功");
// 现在抛出IOException也会回滚了!
if (true) {
throw new IOException("网络异常");
}
return "r3"; // 这行不会执行,因为上面抛异常了
}
}
结论与代码的对应关系:
|---------------------|-------------------------------------------------|-----------------|
| 默认行为 | 代码体现 | 结果 |
| 只回滚RuntimeException | @Transactional(无参数) | IOException不会回滚 |
| 回滚所有异常 | @Transactional(rollbackFor = Exception.class) | 所有异常都会回滚 |
3.2 事务隔离级别
3.2.1 MySQL隔离级别回顾(用代码理解)
sql
-- 查询当前MySQL隔离级别
SELECT @@global.tx_isolation, @@tx_isolation;
-- 结果示例:REPEATABLE-READ(MySQL默认)
3.2.2 Spring设置隔离级别
java
import org.springframework.transaction.annotation.Isolation; // 导入隔离级别枚举
import org.springframework.transaction.annotation.Transactional;
/**
* 隔离级别演示
*/
@RestController
public class IsolationDemoController {
/**
* 设置隔离级别为READ_COMMITTED
* * 解决:脏读问题(读取到未提交的数据)
* 场景:适合读多写少的场景,提高并发性能
* * 代码中的体现:
* @Transactional(isolation = ...) 指定隔离级别
*/
@Transactional(
isolation = Isolation.READ_COMMITTED // 读已提交
)
@RequestMapping("/r3")
public String r3(String name, String password) {
// 在这个事务中,只能读取到其他事务已提交的数据
// 避免了脏读,但可能出现不可重复读
return "r3";
}
/**
* 使用数据库默认隔离级别
* * Isolation.DEFAULT = -1,表示"跟随数据库默认设置"
* MySQL下等效于Isolation.REPEATABLE_READ
*/
@Transactional(
isolation = Isolation.DEFAULT // 使用数据库默认(推荐)
)
@RequestMapping("/r4")
public String r4(String name, String password) {
return "r4";
}
}
知识点与代码的直接对应:
java
// 隔离级别在代码中的体现就是这一个参数
@Transactional(
isolation = Isolation.READ_UNCOMMITTED // 读未提交,最低级别,可能脏读
// isolation = Isolation.READ_COMMITTED // 读已提交,避免脏读
// isolation = Isolation.REPEATABLE_READ // 可重复读,避免不可重复读(MySQL默认)
// isolation = Isolation.SERIALIZABLE // 串行化,最高级别,避免幻读但性能差
)
3.3 事务传播机制(重头戏)
3.3.1 什么是事务传播机制?
场景代码:
java
// 主方法(带事务)
@Transactional
public void methodA() {
// 调用另一个带事务的方法
serviceB.methodB(); // 问题:B是加入A的事务,还是创建自己的事务?
}
代码体现 :传播机制就是解决methodB该如何处理事务的问题。
3.3.2 7种传播机制全览
java
// Spring源码中的Propagation枚举
public enum Propagation {
REQUIRED(0), // 默认值,如果存在事务则加入,否则新建
SUPPORTS(1), // 如果存在事务则加入,否则非事务方式运行
MANDATORY(2), // 强制要求存在事务,否则抛异常
REQUIRES_NEW(3), // 总是新建事务,挂起当前事务
NOT_SUPPORTED(4), // 以非事务方式运行,挂起当前事务
NEVER(5), // 以非事务方式运行,如果存在事务则抛异常
NESTED(6); // 如果存在事务则创建嵌套事务
}
3.3.3 代码演示(完整带注释版)
场景说明:注册用户信息 + 记录日志,两个操作都需要事务,但日志记录失败不应该影响用户注册。
3.3.3.1 REQUIRED(加入事务)
java
// =============== Controller层 ===============
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 事务传播机制测试控制器
* 场景:用户注册时记录日志
*/
@RequestMapping("/propaga") // 基础路径
@RestController // REST控制器
public class PropagationController {
// 注入用户服务
@Autowired
private UserService userService;
// 注入日志服务
@Autowired
private LogService logService;
/**
* 测试REQUIRED传播机制
* * REQUIRED的行为:
* 1. 如果methodA()调用methodB()时,A已经有事务
* 2. 那么B会加入A的事务,不会创建新事务
* 3. 整个事务要么全部成功,要么全部失败
* * 代码体现:
* @Transactional(propagation = Propagation.REQUIRED)
*/
@Transactional(propagation = Propagation.REQUIRED) // 开启主事务
@RequestMapping("/p1")
public String p1(String name, String password) {
// 1. 注册用户(加入当前事务)
userService.registryUser(name, password); // 使用主事务
// 2. 记录日志(加入当前事务)
logService.insertLog(name, "用户注册"); // 使用主事务
// 3. 如果insertLog()抛出异常,整个事务(包括registryUser)都会回滚
return "p1";
}
}
// =============== UserService层 ===============
import com.example.demo.mapper.UserInfoMapper; // 用户Mapper
import lombok.extern.slf4j.Slf4j; // 日志注解
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; // 服务层注解
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户服务类
* 负责用户相关的业务逻辑
*/
@Slf4j // 自动生成日志对象log,用于打印日志
@Service // 声明这是一个服务层组件,Spring会自动扫描并注册为Bean
public class UserService {
// 注入用户Mapper(操作数据库)
@Autowired
private UserInfoMapper userInfoMapper;
/**
* 注册用户方法
* * 传播机制:REQUIRED(加入当前事务)
* 行为:如果调用者已经开启事务,就加入该事务;否则新建
* * 代码体现:
* @Transactional(propagation = Propagation.REQUIRED)
*/
@Transactional(propagation = Propagation.REQUIRED)
public void registryUser(String name, String password) {
// 插入用户信息到数据库
// 这个方法没有自己的事务,它加入调用者的事务
userInfoMapper.insert(name, password);
log.info("用户数据插入成功"); // 记录成功日志
}
}
// =============== LogService层 ===============
import com.example.demo.mapper.LogInfoMapper; // 日志Mapper
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* 日志服务类
* 负责记录操作日志
*/
@Slf4j
@Service
public class LogService {
// 注入日志Mapper
@Autowired
private LogInfoMapper logInfoMapper;
/**
* 插入日志方法
* * 传播机制:REQUIRED(加入当前事务)
* 问题:如果这里抛出异常,整个事务都会回滚
* 包括已经成功的registryUser()!
* * 代码体现:在方法中主动抛出异常
*/
@Transactional(propagation = Propagation.REQUIRED)
public void insertLog(String name, String op) {
// 模拟异常:除零错误(RuntimeException)
// 这会导致整个事务回滚,包括用户注册信息
int a = 10 / 0; // ❌ 抛出ArithmeticException
// 这行代码不会执行,因为上面已经抛异常
logInfoMapper.insertLog(name, op); // 记录日志
}
}
执行结果分析:
sql
// 调用链:
p1() [主事务开始]
→ registryUser() [加入主事务] → 插入用户成功
→ insertLog() [加入主事务] → 抛出异常
→ 主事务回滚 → 用户数据也被回滚!
// 数据库结果:用户表和日志表都**没有**数据
// 原因:REQUIRED让所有方法共享同一个事务
知识点与代码的对应关系:
|----------|------------------------------------------------------|-----------------------|
| 传播机制 | 代码体现 | 执行结果 |
| REQUIRED | @Transactional(propagation = Propagation.REQUIRED) | 所有操作在同一个事务中,一荣俱荣,一损俱损 |
3.3.3.2 REQUIRES_NEW(新建事务)
修改代码:
java
// =============== UserService层修改 ===============
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
/**
* 传播机制:REQUIRES_NEW(新建事务)
* 行为:总是创建新事务,挂起调用者的事务
* * 代码体现:
* @Transactional(propagation = Propagation.REQUIRES_NEW)
* * 执行流程:
* 1. p1()开启主事务
* 2. registryUser()发现REQUIRES_NEW,挂起主事务,创建新事务
* 3. 在新事务中执行插入用户操作
* 4. 新事务提交,主事务被恢复
* 5. insertLog()抛出异常,只回滚自己的事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registryUser(String name, String password) {
userInfoMapper.insert(name, password); // 在新事务中执行
log.info("用户数据插入成功");
}
}
// =============== LogService层修改 ===============
@Slf4j
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
/**
* 传播机制:REQUIRES_NEW(新建事务)
* 行为:总是创建新事务,挂起调用者的事务
* * 代码体现:
* @Transactional(propagation = Propagation.REQUIRES_NEW)
* * 执行流程:
* 1. 主事务被挂起
* 2. 创建新的事务执行日志插入
* 3. 抛出异常,只回滚当前新事务
* 4. 恢复主事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insertLog(String name, String op) {
int a = 10 / 0; // 在新事务中抛异常
logInfoMapper.insertLog(name, op);
}
}
执行结果分析:
java
// 调用链:
p1() [主事务开始]
→ registryUser() [挂起主事务,创建新事务] → 插入用户成功 → **新事务提交** → 恢复主事务
→ insertLog() [挂起主事务,创建新事务] → 抛出异常 → **新事务回滚** → 恢复主事务
→ 主事务提交
// 数据库结果:用户表**有**数据,日志表**没有**数据
// 原因:REQUIRES_NEW让每个方法独立事务,互不影响
知识点与代码的对应关系:
|--------------|----------------------------------------------------------|--------------------------|
| 传播机制 | 代码体现 | 执行结果 |
| REQUIRES_NEW | @Transactional(propagation = Propagation.REQUIRES_NEW) | 方法独立事务,成功的方法提交,失败的方法单独回滚 |
3.3.3.3 NEVER(不支持事务)
java
@Slf4j
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
/**
* 传播机制:NEVER(不支持事务)
* 行为:以非事务方式运行,如果调用者存在事务,则抛出异常
* * 代码体现:
* @Transactional(propagation = Propagation.NEVER)
* * 执行流程:
* 1. p1()开启了主事务
* 2. registryUser()被调用,但传播机制是NEVER
* 3. 由于当前存在事务,Spring抛出异常:
* "Existing transaction found for transaction marked with propagation 'never'"
* 4. 整个操作失败
*/
@Transactional(propagation = Propagation.NEVER)
public void registryUser(String name, String password) {
userInfoMapper.insert(name, password);
}
}
执行结果:
// 抛出异常:Existing transaction found for transaction marked with propagation 'never'
// NESTED与REQUIRED区别(最关键!)
3.3.3.4 NESTED(嵌套事务)
java
// =============== UserService层修改 ===============
@Slf4j
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
/**
* 传播机制:NESTED(嵌套事务)
* 行为:如果调用者存在事务,则创建嵌套事务(子事务)
* * 代码体现:
* @Transactional(propagation = Propagation.NESTED)
* * 技术原理:
* 嵌套事务使用数据库的Savepoint(保存点)实现
* 相当于:SAVEPOINT savepoint1; // 创建保存点
* 回滚时:ROLLBACK TO SAVEPOINT savepoint1; // 回滚到保存点
* * 执行流程:
* 1. p1()开启主事务
* 2. registryUser()创建嵌套事务(保存点1)
* 3. insertLog()创建嵌套事务(保存点2)
* 4. insertLog()抛出异常,回滚到保存点2
* 5. 异常向上抛出,导致主事务也回滚
* 6. 结果:所有数据都回滚
*/
@Transactional(propagation = Propagation.NESTED)
public void registryUser(String name, String password) {
userInfoMapper.insert(name, password);
log.info("用户数据插入成功");
}
}
// =============== LogService层修改 ===============
@Slf4j
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
/**
* 传播机制:NESTED(嵌套事务)
* 行为:创建嵌套事务
* * 执行流程:
* 1. 在主事务中创建保存点
* 2. 在嵌套事务中执行操作
* 3. 抛出异常,嵌套事务回滚
* 4. 异常继续抛出,主事务也回滚
*/
@Transactional(propagation = Propagation.NESTED)
public void insertLog(String name, String op) {
int a = 10 / 0; // 嵌套事务内抛异常
logInfoMapper.insertLog(name, op);
}
}
3.3.3.5 NESTED vs REQUIRED(局部回滚)
关键区别代码演示:
java
// =============== LogService层 - NESTED实现局部回滚 ===============
import org.springframework.transaction.interceptor.TransactionAspectSupport; // 手动回滚工具类
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
/**
* NESTED传播机制 + 手动回滚 = 局部回滚
* * 执行流程:
* 1. p1()开启主事务
* 2. registryUser()在主事务中执行成功
* 3. insertLog()创建嵌套事务(保存点)
* 4. 发生异常,捕获后在catch块中手动回滚嵌套事务
* 5. 主事务继续执行,不受影响,最终提交
* * 代码关键点:
* - NESTED传播机制
* - catch块中调用setRollbackOnly()
*/
@Transactional(propagation = Propagation.NESTED) // 必须是NESTED
public void insertLog(String name, String op) {
try {
// 业务逻辑
int a = 10 / 0; // 模拟异常
// 正常插入日志
logInfoMapper.insertLog(name, op);
log.info("日志记录成功");
} catch (Exception e) {
// 【核心】手动回滚当前嵌套事务
// 只回滚insertLog这个嵌套事务,不影响主事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.error("日志记录失败,但主事务继续执行", e);
// 注意:这里catch了异常,主事务感知不到,所以不会回滚
}
}
}
对比REQUIRED的代码:
// 如果改为REQUIRED,同样的代码会导致整个事务回滚
@Transactional(propagation = Propagation.REQUIRED) // 改为REQUIRED
public void insertLog(String name, String op) {
try {
int a = 10 / 0;
logInfoMapper.insertLog(name, op);
} catch (Exception e) {
// 即使是REQUIRED,在catch中setRollbackOnly也会标记整个事务为回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 主事务最终也会回滚!
}
}
知识点与代码的对应关系:
|----------|------------------------------------------------------|-----------------------|---------------|
| 传播机制 | 代码体现 | 异常处理方式 | 结果 |
| REQUIRED | @Transactional(propagation = Propagation.REQUIRED) | catch中setRollbackOnly | 整个事务回滚 |
| NESTED | @Transactional(propagation = Propagation.NESTED) | catch中setRollbackOnly | 仅嵌套事务回滚,主事务继续 |
四、完整项目代码(超详细注释版)
4.1 pom.xml(依赖详细注释)
XML
<?xml version="1.0" encoding="UTF-8"?>
<!--
Maven项目对象模型文件
定义项目信息、依赖管理、构建配置等
-->
<project xmlns="[http://maven.apache.org/POM/4.0.0](http://maven.apache.org/POM/4.0.0)"
xmlns:xsi="[http://www.w3.org/2001/XMLSchema-instance](http://www.w3.org/2001/XMLSchema-instance)"
xsi:schemaLocation="[http://maven.apache.org/POM/4.0.0](http://maven.apache.org/POM/4.0.0)
[https://maven.apache.org/xsd/maven-4.0.0.xsd](https://maven.apache.org/xsd/maven-4.0.0.xsd)">
<!-- Maven模型版本,固定为4.0.0 -->
<modelVersion>4.0.0</modelVersion>
<!-- 父项目配置:spring-boot-starter-parent
作用:提供Spring Boot的默认配置、依赖版本管理、插件配置等
好处:无需手动指定每个依赖的版本号
-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version> <!-- Spring Boot版本 -->
<relativePath/> <!-- 不从本地路径查找父项目 -->
</parent>
<!-- 本项目信息 -->
<groupId>com.example</groupId> <!-- 组织ID -->
<artifactId>spring-trans</artifactId> <!-- 项目ID -->
<version>0.0.1-SNAPSHOT</version> <!-- 版本号 -->
<name>spring-trans</name> <!-- 项目名称 -->
<description>Spring事务演示项目</description> <!-- 描述 -->
<!-- 属性配置 -->
<properties>
<!-- Java版本 -->
<java.version>11</java.version>
</properties>
<!-- 依赖列表 -->
<dependencies>
<!-- Spring Web Starter
包含:Spring MVC、Tomcat、JSON处理等
作用:提供Web应用基础功能
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Spring Boot Starter
包含:MyBatis、Spring JDBC、连接池等
作用:简化MyBatis与Spring Boot的集成
-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version> <!-- 指定版本 -->
</dependency>
<!-- MySQL驱动
scope: runtime表示编译时不依赖,运行时由容器提供
-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope> <!-- 只在运行时需要 -->
</dependency>
<!-- Lombok
作用:通过注解减少样板代码(getter、setter、toString等)
optional: true表示不强制依赖
-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring事务
虽然starter-web已包含,但显式声明更清晰
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
</dependencies>
<!-- 构建配置 -->
<build>
<plugins>
<!-- Spring Boot Maven插件
作用:支持打包可执行jar、运行Spring Boot应用
-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<!-- 打包时排除Lombok -->
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2 application.properties(配置详细注释)
java
# ==================== 数据库配置 ====================
# MySQL数据库连接URL
# 参数说明:
# - characterEncoding=utf8:字符编码为UTF-8
# - useSSL=false:不使用SSL连接
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=false
# 数据库用户名
spring.datasource.username=root
# 数据库密码
spring.datasource.password=root
# MySQL驱动类名(8.0+版本)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# ==================== MyBatis配置 ====================
# 日志实现类:在控制台输出SQL语句
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 自动将下划线命名转换为驼峰命名
# 例如:user_name字段 → userName属性
mybatis.configuration.map-underscore-to-camel-case=true
4.3 实体类(详细注释)
java
// =============== UserInfo.java ===============
import lombok.Data; // Lombok注解,自动生成getter、setter、toString、equals、hashCode
import java.util.Date; // 日期类
/**
* 用户信息实体类
* 对应数据库表:user_info
* * Lombok的@Data注解等价于:
* - @Getter:生成所有字段的get方法
* - @Setter:生成所有字段的set方法
* - @ToString:生成toString方法
* - @EqualsAndHashCode:生成equals和hashCode方法
* - @RequiredArgsConstructor:生成必填字段的构造方法
*/
@Data // 使用Lombok减少样板代码
public class UserInfo {
// 主键ID
private Integer id;
// 用户名(对应表字段user_name)
private String userName;
// 密码
private String password;
// 创建时间
private Date createTime;
// 更新时间
private Date updateTime;
}
// =============== LogInfo.java ===============
import lombok.Data;
import java.util.Date;
/**
* 日志信息实体类
* 对应数据库表:log_info
*/
@Data
public class LogInfo {
// 主键ID
private Integer id;
// 用户名
private String userName;
// 操作类型
private String op;
// 创建时间
private Date createTime;
// 更新时间
private Date updateTime;
}
4.4 Mapper接口(详细注释)
java
// =============== UserInfoMapper.java ===============
import org.apache.ibatis.annotations.Insert; // MyBatis插入注解
import org.apache.ibatis.annotations.Mapper; // MyBatis Mapper标识注解
/**
* 用户信息Mapper接口
* 作用:定义操作user_info表的方法
* * @Mapper注解:告诉MyBatis这是Mapper接口,生成代理实现类
*/
@Mapper // 必须加,否则Spring无法扫描到
public interface UserInfoMapper {
/**
* 插入用户信息
* * @Insert注解:直接写SQL语句,无需XML配置
* #{name}:MyBatis的占位符,会被方法参数name替换
* * @param name 用户名
* @param password 密码
* @return 插入的行数(成功返回1,失败返回0)
*/
@Insert("INSERT INTO user_info(user_name, password) VALUES(#{name}, #{password})")
Integer insert(String name, String password);
}
// =============== LogInfoMapper.java ===============
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
/**
* 日志信息Mapper接口
* 作用:定义操作log_info表的方法
*/
@Mapper
public interface LogInfoMapper {
/**
* 插入日志信息
* * @param name 用户名
* @param op 操作描述
* @return 插入的行数
*/
@Insert("INSERT INTO log_info(user_name, op) VALUES(#{name}, #{op})")
Integer insertLog(String name, String op);
}
4.5 Service层(详细注释)
java
// =============== UserService.java ===============
import com.example.demo.mapper.UserInfoMapper; // 用户Mapper
import com.example.demo.model.UserInfo; // 用户实体
import lombok.extern.slf4j.Slf4j; // Lombok日志注解
import org.springframework.beans.factory.annotation.Autowired; // 依赖注入
import org.springframework.stereotype.Service; // 服务层注解
import org.springframework.transaction.annotation.Propagation; // 传播机制枚举
import org.springframework.transaction.annotation.Transactional; // 事务注解
/**
* 用户服务类
* 处理用户注册等业务逻辑
* * @Service注解:标识这是一个服务层组件
* 作用:1. 被Spring扫描注册为Bean 2. 支持AOP代理(事务)
*/
@Slf4j // 自动生成log日志对象
@Service
public class UserService {
// 注入UserInfoMapper(数据库操作)
// @Autowired:Spring自动装配,将Spring容器中的Mapper注入
@Autowired
private UserInfoMapper userInfoMapper;
/**
* 注册用户核心业务方法
* * @Transactional注解:启动声明式事务
* propagation = Propagation.REQUIRED:如果调用者有事务就加入,否则新建
* * 重要:Service层是事务的最佳放置位置
* 原因:Service层代表业务逻辑,一个业务可能包含多个DAO操作
*/
@Transactional(propagation = Propagation.REQUIRED)
public void registryUser(String name, String password) {
// 调用Mapper插入数据
// 如果插入失败(抛异常),事务会自动回滚
userInfoMapper.insert(name, password);
// 记录成功日志
// {}是占位符,会被后面的参数替换,比字符串拼接性能更好
log.info("用户数据插入成功:用户名={}", name);
}
}
// =============== LogService.java ===============
import com.example.demo.mapper.LogInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport; // 手动回滚工具
/**
* 日志服务类
* 负责记录用户操作日志
* * 设计思路:日志记录不应该影响主业务
* 即使日志记录失败,用户注册也应该成功
* 因此使用NESTED实现局部回滚
*/
@Slf4j
@Service
public class LogService {
// 注入LogInfoMapper
@Autowired
private LogInfoMapper logInfoMapper;
/**
* 插入日志方法
* * @Transactional注解:
* propagation = Propagation.NESTED:创建嵌套事务
* * NESTED的特殊能力:
* 1. 可以捕获异常并手动回滚嵌套事务
* 2. 不影响父事务的提交
* * 对比REQUIRED:
* 如果是REQUIRED,即使catch了异常,只要setRollbackOnly(),
* 整个事务(包括父事务)都会回滚
*/
@Transactional(propagation = Propagation.NESTED)
public void insertLog(String name, String op) {
try {
// 模拟业务逻辑
log.info("开始记录日志:用户={}, 操作={}", name, op);
// 模拟异常(比如日志服务器不可用)
int a = 10 / 0; // 抛出ArithmeticException
// 正常插入日志
logInfoMapper.insertLog(name, op);
log.info("日志记录成功");
} catch (Exception e) {
// 【核心】手动回滚嵌套事务
// TransactionAspectSupport.currentTransactionStatus()获取当前事务状态
// setRollbackOnly()标记当前事务为"仅回滚"
// 对于NESTED,只回滚嵌套事务;对于REQUIRED,标记整个事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 记录错误日志,但不抛出异常
// 这样父事务就不会感知到异常,会继续提交
log.error("日志记录失败,但主业务不受影响", e);
}
}
}
4.6 Controller层(详细注释)
java
// =============== TransactionalController.java ===============
import com.example.demo.service.LogService; // 日志服务
import com.example.demo.service.UserService; // 用户服务
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation; // 传播机制
import org.springframework.transaction.annotation.Transactional; // 事务注解
import org.springframework.web.bind.annotation.RequestMapping; // 请求映射
import org.springframework.web.bind.annotation.RestController; // REST控制器
/**
* 事务测试控制器
* * @RestController = @Controller + @ResponseBody
* 作用:1. 声明控制器 2. 所有方法返回JSON数据
* * @RequestMapping("/trans"):
* 类级别的路径映射,所有方法路径前都有/trans前缀
*/
@RestController
@RequestMapping("/trans")
public class TransactionalController {
// 注入用户服务
@Autowired
private UserService userService;
// 注入日志服务
@Autowired
private LogService logService;
/**
* 注册用户并记录日志
* * 完整业务流程:
* 1. 开启主事务(REQUIRED)
* 2. 注册用户(NESTED嵌套事务)
* 3. 记录日志(NESTED嵌套事务,可局部回滚)
* 4. 主事务提交
* * @param name 用户名
* @param password 密码
* @return 操作结果
*/
@RequestMapping("/registry") // 完整路径:/trans/registry
@Transactional(propagation = Propagation.REQUIRED) // 开启主事务
public String registry(String name, String password) {
// 第一步:注册用户
// 如果UserService是NESTED,这里创建嵌套事务
// 如果UserService是REQUIRED,这里加入当前事务
userService.registryUser(name, password);
// 第二步:记录日志
// 即使日志记录失败(被catch),也不会影响主事务
// 因为LogService中catch了异常并setRollbackOnly()
logService.insertLog(name, "用户注册");
// 方法正常返回,主事务自动提交
// 如果抛出RuntimeException,主事务自动回滚
return "注册成功";
}
}
完整调用流程注释:
html
// 浏览器请求:GET /trans/registry?name=Tom&password=123
// Spring MVC处理流程:
// 1. DispatcherServlet接收请求
// 2. 找到TransactionalController.registry()方法
// 3. 发现@Transactional注解,创建代理对象
// 4. 代理对象执行:
// a. 开启主事务(REQUIRED)
// b. 调用registry()方法
// i. userService.registryUser() → 嵌套事务,插入用户成功
// ii. logService.insertLog() → 嵌套事务,抛出异常但被catch
// - 在catch中setRollbackOnly() → 只回滚嵌套事务
// - 异常被捕获,不向上抛出
// c. registry()方法正常结束
// d. 提交主事务(用户数据保留)
// 5. 返回"注册成功"
// 数据库结果:
// user_info表:Tom用户插入成功
// log_info表:没有日志(因为嵌套事务回滚了)
4.7 数据库初始化脚本(详细注释)
sql
-- =============== trans_test.sql ===============
-- 删除已存在的数据库(如果存在)
-- 用于重新初始化测试环境
DROP DATABASE IF EXISTS trans_test;
-- 创建新数据库
-- DEFAULT CHARACTER SET utf8mb4:设置默认字符集为UTF-8(支持emoji)
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;
-- ==================== 用户表 ====================
-- 删除已存在的表
DROP TABLE IF EXISTS user_info;
-- 创建用户表
CREATE TABLE user_info(
-- 主键ID,自增
id INT NOT NULL AUTO_INCREMENT,
-- 用户名,VARCHAR(128)表示最大128字符,NOT NULL表示不能为空
user_name VARCHAR(128) NOT NULL,
-- 密码,VARCHAR(128)
password VARCHAR(128) NOT NULL,
-- 创建时间,DATETIME类型,DEFAULT now()表示默认当前时间
create_time DATETIME DEFAULT now(),
-- 更新时间,ON UPDATE now()表示每次更新记录时自动更新为当前时间
update_time DATETIME DEFAULT now() ON UPDATE now(),
-- 设置主键
PRIMARY KEY(id)
-- 存储引擎和字符集
) ENGINE=INNODB -- INNODB支持事务,MyISAM不支持
DEFAULT CHARACTER SET=utf8mb4
COMMENT='用户表'; -- 表注释
-- ==================== 日志表 ====================
DROP TABLE IF EXISTS log_info;
CREATE TABLE log_info(
id INT PRIMARY KEY auto_increment, -- 主键,简写形式
user_name VARCHAR(128) NOT NULL,
op VARCHAR(256) NOT NULL COMMENT '操作描述', -- 字段注释
create_time DATETIME DEFAULT now(),
update_time DATETIME DEFAULT now() ON UPDATE now()
) DEFAULT charset='utf8mb4';
五、总结与代码对应关系
5.1 Spring事务实现方式对比
java
// 方式一:编程式事务(手动控制)
public void programmatic() {
// 代码体现:手动getTransaction、commit、rollback
TransactionStatus status = manager.getTransaction(definition);
try {
// 业务代码
manager.commit(status);
} catch(Exception e) {
manager.rollback(status);
throw e;
}
}
// 方式二:声明式事务(注解控制)
@Transactional // 代码体现:一个注解搞定所有事务管理
public void declarative() {
// 业务代码
// Spring自动处理开启、提交、回滚
}
5.2 @Transactional关键属性代码体现
java
@Transactional(
// 1. rollbackFor:异常回滚
rollbackFor = Exception.class, // 代码:指定IOException等检查型异常也回滚
// 2. isolation:隔离级别
isolation = Isolation.READ_COMMITTED, // 代码:避免脏读
// 3. propagation:传播机制
propagation = Propagation.REQUIRED // 代码:决定事务如何在方法间传播
)
public void method() {
// 业务代码
}
5.3 事务传播机制总结表
java
// 代码体现:在Service方法上添加不同注解
// REQUIRED(默认)
@Transactional(propagation = Propagation.REQUIRED)
public void requiredMethod() {
// 加入当前事务,一损俱损
}
// REQUIRES_NEW(独立事务)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewMethod() {
// 新建事务,挂起调用者事务
}
// NESTED(嵌套事务)
@Transactional(propagation = Propagation.NESTED)
public void nestedMethod() {
// 创建嵌套事务,可实现局部回滚
try {
// 业务代码
} catch(Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 只回滚嵌套事务
}
}
// NEVER(不支持事务)
@Transactional(propagation = Propagation.NEVER)
public void neverMethod() {
// 调用者有事务就抛异常
}
六、常见问题与代码解答
Q1:为什么要在Service层而不是Controller层使用事务?
代码说明:
java
// ❌ 错误:在Controller层放事务
@RestController
public class UserController {
@Transactional
public String register() {
// Controller应该只负责请求和响应
// 如果业务逻辑复杂(调用多个Service),事务范围会过大
userService.insertUser(); // 操作1
orderService.createOrder(); // 操作2
logService.recordLog(); // 操作3
// 事务范围包含所有操作,性能差
}
}
// ✅ 正确:在Service层放事务
@Service
public class UserService {
@Transactional
public void registerUser() {
// Service层代表业务逻辑
// 事务范围只包含当前业务
userMapper.insert();
logMapper.insert(); // 相关业务在同一个事务
}
}
结论代码联系 :@Transactional放在Service层,事务边界更清晰,符合单一职责原则。
Q2:NESTED和REQUIRED有什么区别?
代码对比:
java
// ========== REQUIRED(全部回滚) ==========
@Transactional(propagation = Propagation.REQUIRED)
public void parent() {
child(); // REQUIRED,加入当前事务
}
@Transactional(propagation = Propagation.REQUIRED)
public void child() {
// 抛异常 → 整个事务(parent+child)回滚
throw new RuntimeException();
}
// ========== NESTED(局部回滚) ==========
@Transactional(propagation = Propagation.REQUIRED)
public void parent() {
child(); // NESTED,创建嵌套事务
}
@Transactional(propagation = Propagation.NESTED)
public void child() {
try {
throw new RuntimeException();
} catch(Exception e) {
// 只回滚嵌套事务,parent继续
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
结论代码联系 :Propagation.NESTED + setRollbackOnly() 实现了局部回滚,而REQUIRED会回滚整个事务。
Q3:如何在事务中捕获异常并实现部分回滚?
答案代码:
java
/**
* 答案:使用NESTED传播机制 + 手动setRollbackOnly()
*/
@Transactional(propagation = Propagation.NESTED) // 关键1:必须是NESTED
public void partialRollback() {
try {
// 可能失败的操作
riskOperation();
} catch(Exception e) {
// 关键2:手动回滚嵌套事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 记录日志或进行补偿操作
log.error("操作失败,但主事务继续", e);
}
// 主业务逻辑继续执行
mainBusiness();
// 结果:riskOperation()回滚,mainBusiness()提交
}
七、扩展知识点
7.1 事务ACID特性的代码体现
java
@Transactional
public void demonstrateACID() {
// 原子性(Atomicity)
// 代码体现:try块中的所有操作要么全部commit,要么全部rollback
try {
insertUser(); // 操作1
insertOrder(); // 操作2
updateStock(); // 操作3
// 全部成功 → commit()
} catch(Exception e) {
// 任何失败 → rollback()
}
// 一致性(Consistency)
// 代码体现:事务开始前和结束后,数据库约束不被破坏
// 隔离性(Isolation)
// 代码体现:@Transactional(isolation = Isolation.READ_COMMITTED)
// 持久性(Durability)
// 代码体现:commit()后,数据永久保存,即使系统崩溃
}
7.2 事务实现原理(AOP代理)
java
// Spring AOP代理伪代码(帮助你理解@Transactional如何工作)
public class UserServiceProxy extends UserService {
private UserService target; // 真实对象
private TransactionManager txManager; // 事务管理器
@Override
public void registryUser(String name, String password) {
// 1. 解析@Transactional注解
TransactionDefinition definition = parseTransactionAnnotation();
// 2. 开启事务(AOP前置通知)
TransactionStatus status = txManager.getTransaction(definition);
try {
// 3. 调用真实对象的方法
target.registryUser(name, password);
// 4. 提交事务(AOP返回通知)
txManager.commit(status);
} catch(RuntimeException e) {
// 5. 回滚事务(AOP异常通知)
txManager.rollback(status);
throw e;
}
}
}
八、最佳实践代码模板
java
/**
* Service层事务最佳实践模板
*/
@Service
public class BestPracticeService {
/**
* 1. 默认使用REQUIRED
* 适用场景:大多数业务操作
*/
@Transactional // 不指定propagation,默认就是REQUIRED
public void standardBusiness() {
// 多个数据库操作
}
/**
* 2. 需要独立事务使用REQUIRES_NEW
* 适用场景:发送邮件、记录审计日志等不影响主业务的操作
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void independentOperation() {
// 即使失败也不影响主事务
}
/**
* 3. 需要局部回滚使用NESTED
* 适用场景:主订单必须成功,但订单明细可以失败
*/
@Transactional(propagation = Propagation.NESTED)
public void nestedOperation() {
try {
// 可能失败的操作
} catch(Exception e) {
// 局部回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
/**
* 4. 只读查询使用readOnly = true
* 作用:优化性能,数据库可能启用只读优化
*/
@Transactional(readOnly = true)
public UserInfo getUserInfo(Integer userId) {
// 只读查询
return userMapper.selectById(userId);
}
/**
* 5. 指定超时时间
* 作用:防止长时间占用数据库连接
*/
@Transactional(timeout = 30) // 30秒超时
public void longTimeOperation() {
// 可能耗时的操作
}
}
最后的话
记住这个核心原则:
html
// 看到代码时,问自己三个问题:
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
public void method() {
// 1. 当前有事务吗?(传播机制决定)
// 2. 能看到其他事务未提交的数据吗?(隔离级别决定)
// 3. 抛 checked exception会回滚吗?(rollbackFor决定)
}
希望这份带详细注释的指南能帮你彻底理解Spring事务!每个代码片段都与知识点直接挂钩,注释覆盖了从import到方法返回的每一个细节。
八、Spring事务失效场景详解(新手必看)
本章目标:掌握Spring事务的8种常见失效场景,避免在实际开发中踩坑。这是从"会用"到"用好"的关键一步!
8.1 同类自调用问题(最坑!)
问题描述
在同一个类中,非事务方法调用本类的事务方法,事务会完全失效 。这是新手最容易踩、最难排查的坑。
错误代码示例
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户服务类 - 演示同类自调用导致事务失效
*/
@Slf4j
@Service
public class UserService {
/**
* 外部调用的入口方法(无事务)
* 问题:这个方法本身没有@Transactional,它直接调用内部方法
*/
public void registerUser(String name, String password) {
log.info("开始注册用户");
// 【致命错误】直接调用本类的方法,使用this调用
// this指向的是目标对象本身,而不是Spring的代理对象
// 结果就是绕过了Spring AOP代理,@Transactional注解被完全忽略!
this.insertUser(name, password); // 事务失效!
log.info("注册用户结束");
}
/**
* 内部事务方法(带@Transactional)
* 问题:虽然加了事务注解,但同类调用时不会生效
*/
@Transactional
public void insertUser(String name, String password) {
log.info("插入用户数据到数据库");
userInfoMapper.insert(name, password);
// 模拟异常
int i = 10 / 0; // 抛出RuntimeException
// 期望:事务回滚,数据库没有数据
// 实际:事务失效,数据库有数据(操作未回滚)
}
}
失效原因分析
java
// Spring AOP代理工作原理(伪代码)
public class UserService$$Proxy extends UserService {
private UserService target; // 真实的目标对象
private TransactionInterceptor interceptor; // 事务拦截器
@Override
public void insertUser(String name, String password) {
// 代理对象会在这里开启事务
TransactionStatus status = txManager.getTransaction(definition);
try {
target.insertUser(name, password); // 调用真实对象的方法
txManager.commit(status);
} catch(Exception e) {
txManager.rollback(status);
throw e;
}
}
}
// 当同类自调用时:
public void registerUser() {
// this指向的是UserService(目标对象),不是UserService$$Proxy(代理对象)
// 绕过了AOP代理,直接调用目标方法,事务拦截器完全不生效
this.insertUser(); // 没有AOP代理介入
}
正确解决方案
java
import org.springframework.beans.factory.annotation.Autowired; // 注入自身代理对象
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 正确做法:注入自己的代理对象,通过代理对象调用
*/
@Slf4j
@Service
public class UserService {
// 【解决方案1】注入Spring容器中的代理对象
// Spring会注入一个代理后的UserService实例
@Autowired
private UserService selfProxy; // 自己代理自己!
public void registerUser(String name, String password) {
log.info("开始注册用户");
// 通过代理对象调用,AOP生效!
// Spring会拦截这次调用,执行事务逻辑
selfProxy.insertUser(name, password); // ✅ 事务有效!
log.info("注册用户结束");
}
@Transactional
public void insertUser(String name, String password) {
userInfoMapper.insert(name, password);
int i = 10 / 0; // 异常后事务正确回滚
}
// 【解决方案2】将方法拆分到不同Service
// UserService调用LogService,天然跨类调用,AOP永远生效
@Autowired
private LogService logService; // 注入其他Service
public void registerUser2(String name, String password) {
this.insertUser(name, password); // 本类方法(无事务)
logService.recordLog(name); // 其他类方法(有事务),AOP生效
}
}
8.2 方法修饰符非public
问题描述
Spring AOP基于代理实现,private、protected、package-private 方法上的@Transactional不会生效。
错误代码示例
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 演示非public方法事务失效
*/
@Slf4j
@Service
public class OrderService {
/**
* 公共方法调用私有事务方法
*/
public void createOrder(Order order) {
// 调用私有方法
this.saveOrder(order); // 事务失效!
}
/**
* 【错误】私有方法上的事务注解
* 原因:Spring AOP代理无法拦截私有方法
* JDK动态代理:基于接口,只能代理public方法
* CGLIB代理:虽然可以代理类,但默认不拦截非public方法
*/
@Transactional
private void saveOrder(Order order) {
orderMapper.insert(order); // 数据库操作
int i = 10 / 0; // 期望回滚,但不会回滚
}
/**
* 【错误】protected方法上的事务注解
*/
@Transactional
protected void updateOrder(Order order) {
orderMapper.update(order);
}
/**
* 【错误】package-private方法上的事务注解
*/
@Transactional
void deleteOrder(Integer orderId) {
orderMapper.delete(orderId);
}
}
正确解决方案
java
@Slf4j
@Service
public class OrderService {
/**
* 【正确】将事务注解放在public方法上
* Spring AOP代理可以正常拦截public方法
*/
@Transactional
public void createOrder(Order order) {
// 所有数据库操作都在同一个事务中
orderMapper.insert(order);
orderDetailMapper.insert(order.getItems());
int i = 10 / 0; // 异常后正确回滚
}
/**
* 【正确】提取到独立的Service类
* 跨类调用,即使是package-private也生效(因为走代理)
*/
@Service
public class OrderInnerService {
@Transactional
public void saveOrder(Order order) {
orderMapper.insert(order);
}
}
}
8.3 方法是final/static
问题描述
final方法不能被重写,CGLIB无法生成代理子类;static方法属于类,不属于实例,无法被代理。
错误代码示例
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 演示final/static方法事务失效
*/
@Slf4j
@Service
public class ProductService {
/**
* 【错误】final方法上的事务注解
* 原因:CGLIB代理通过继承类生成代理子类,无法重写final方法
*/
@Transactional
public final void addProduct(Product product) {
productMapper.insert(product); // 事务失效!
int i = 10 / 0;
}
/**
* 【错误】static方法上的事务注解
* 原因:static方法属于类,代理对象无法拦截
*/
@Transactional
public static void updateProduct(Product product) {
// 静态方法无法在Spring容器中管理
// 也就无法创建代理
}
}
正确解决方案
java
@Slf4j
@Service
public class ProductService {
/**
* 【正确】去掉final修饰符
* CGLIB可以正常生成代理子类并重写此方法
*/
@Transactional
public void addProduct(Product product) {
productMapper.insert(product);
}
/**
* 【正确】static方法不要加事务
* 如果需要事务,改为实例方法
*/
@Transactional
public void updateProduct(Product product) {
productMapper.update(product);
}
}
8.4 多线程调用问题
问题描述
Spring事务基于ThreadLocal实现,绑定在当前线程。新线程无法获取主线程的事务。
错误代码示例
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 演示多线程导致事务失效
*/
@Slf4j
@Service
public class MultiThreadService {
// 线程池
private ExecutorService executor = Executors.newFixedThreadPool(5);
@Transactional
public void createOrderWithAsyncLog(Order order) {
// 主线程中插入订单(在主事务中)
orderMapper.insert(order);
log.info("订单插入成功,主线程事务ID:{}",
TransactionSynchronizationManager.getCurrentTransactionName());
// 【致命错误】在新线程中记录日志
// 新线程无法获取主线程的事务,会创建新连接
executor.submit(() -> {
log.info("新线程开始记录日志,事务ID:{}",
TransactionSynchronizationManager.getCurrentTransactionName());
// 问题1:这个操作不在主事务中,无法回滚
logMapper.insert(order.getLog());
// 问题2:如果这里抛出异常,主线程的事务不会感知
// 问题3:可能导致数据不一致(订单提交了,日志没记录)
int i = 10 / 0; // 新线程异常,不影响主事务
});
// 主线程继续执行
int j = 10 / 0; // 主线程异常,订单回滚,但日志可能已在新线程中插入
}
}
正确解决方案
java
import org.springframework.scheduling.annotation.Async; // 异步注解
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization; // 事务同步
import org.springframework.transaction.support.TransactionSynchronizationManager; // 事务同步管理器
/**
* 正确做法:使用事务同步管理器或异步事务
*/
@Slf4j
@Service
public class MultiThreadService {
/**
* 方案1:使用TransactionSynchronization(补偿机制)
* 在事务提交/回滚后执行操作
*/
@Transactional
public void createOrderWithSync(Order order) {
orderMapper.insert(order);
// 注册事务同步回调
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 事务提交成功后,在新线程中执行
executor.submit(() -> {
logMapper.insert(order.getLog());
});
}
@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
log.error("事务回滚,不记录日志");
}
}
}
);
}
/**
* 方案2:重构业务逻辑,避免在事务中开新线程
* 先完成事务,再异步处理
*/
public void createOrder(Order order) {
// 1. 先同步完成事务操作
transactionTemplate.execute(status -> {
orderMapper.insert(order);
return true;
});
// 2. 事务完成后,再异步执行其他操作
executor.submit(() -> {
logMapper.insert(order.getLog());
});
}
/**
* 方案3:使用@Async + 独立事务
* 注意:这里的@Async方法必须放在不同的类中
*/
@Service
public class LogService {
@Async // Spring异步执行
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void asyncInsertLog(Log log) {
// 独立事务,独立线程
logMapper.insert(log);
}
}
}
8.5 异常被catch未抛出
问题描述
事务方法中捕获了异常但没有重新抛出,Spring感知不到异常,不会回滚。
错误代码示例
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 演示异常被吞导致事务失效
*/
@Slf4j
@Service
public class ExceptionHandleService {
@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order);
// 模拟业务异常
int i = 10 / 0; // 抛出ArithmeticException
} catch (Exception e) {
log.error("创建订单失败:{}", e.getMessage());
// 【致命错误】没有重新抛出异常
// Spring认为方法正常执行完毕,会提交事务!
// 订单数据会残留在数据库中(不完整状态)
}
// 这里还会继续执行,导致逻辑错误
log.info("订单创建成功"); // 实际上失败了
}
}
正确解决方案
java
@Slf4j
@Service
public class ExceptionHandleService {
@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order);
int i = 10 / 0;
} catch (Exception e) {
log.error("创建订单失败:{}", e.getMessage());
// 【正确】重新抛出异常,让Spring感知到
// Spring捕获异常后会回滚事务
throw new RuntimeException("订单创建失败", e); // ✅ 事务回滚
// 或者抛出自定义业务异常
// throw new BusinessException("ORDER_CREATE_FAILED");
}
}
/**
* 方案2:如果需要特殊处理,手动标记回滚
* 场景:异常不能抛出,但又要回滚事务
*/
@Transactional
public void createOrder2(Order order) {
try {
orderMapper.insert(order);
int i = 10 / 0;
} catch (Exception e) {
log.error("创建订单失败:{}", e.getMessage());
// 手动标记事务为只回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 不抛出异常,但事务仍会回滚
return; // 方法正常返回,但事务已标记为回滚
}
}
}
8.6 数据库引擎不支持事务
问题描述
MySQL的MyISAM引擎不支持事务,即使代码正确,事务也不会生效。
问题代码(SQL层面)
java
-- 创建使用MyISAM引擎的表
CREATE TABLE user_info_myisam (
id INT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(128)
) ENGINE=MYISAM; -- MyISAM不支持事务
-- 插入数据
START TRANSACTION;
INSERT INTO user_info_myisam(user_name) VALUES('Tom');
ROLLBACK; -- 回滚无效!数据已经插入
正确解决方案
java
-- 使用InnoDB引擎(支持事务)
CREATE TABLE user_info_innodb (
id INT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(128)
) ENGINE=INNODB; -- InnoDB支持事务
-- 现在事务可以正常回滚
START TRANSACTION;
INSERT INTO user_info_innodb(user_name) VALUES('Tom');
ROLLBACK; -- 数据正确回滚,表中无数据
Spring中的检查代码:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct; // 初始化回调注解
/**
* 检查数据库引擎是否支持事务
*/
@Component
public class DatabaseEngineChecker {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* @PostConstruct:Spring初始化Bean后执行
* 作用:在应用启动时检查配置
*/
@PostConstruct
public void checkEngine() {
String sql = "SELECT ENGINE FROM information_schema.TABLES " +
"WHERE TABLE_SCHEMA = 'trans_test' " +
"AND TABLE_NAME = 'user_info'";
String engine = jdbcTemplate.queryForObject(sql, String.class);
if (!"InnoDB".equalsIgnoreCase(engine)) {
log.error("警告:user_info表引擎是{},不支持事务!", engine);
// 可以抛异常阻止应用启动
throw new IllegalStateException("数据库引擎不支持事务");
}
log.info("检查通过:数据库引擎支持事务");
}
}
8.7 未配置事务管理器
问题描述
没有配置DataSourceTransactionManager,Spring无法管理事务。
错误配置(Spring Boot)
java
// application.properties中未配置数据源
// spring.datasource.url=... (缺失)
// spring.datasource.username=... (缺失)
// 启动类缺少@EnableTransactionManagement
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 结果:@Transactional注解被忽略,事务失效
正确配置
java
/**
* 显式配置事务管理器(Spring Boot通常自动配置,但了解原理很重要)
*/
import org.springframework.context.annotation.Bean; // Bean定义
import org.springframework.context.annotation.Configuration; // 配置类
import org.springframework.jdbc.datasource.DataSourceTransactionManager; // 事务管理器
import org.springframework.transaction.PlatformTransactionManager; // 事务管理器接口
import org.springframework.transaction.annotation.EnableTransactionManagement; // 开启事务管理
import javax.sql.DataSource; // 数据源
/**
* 事务配置类
* * @EnableTransactionManagement:启用Spring的注解事务管理
* 作用:1. 创建事务代理 2. 启用@Transactional注解解析
* * 在Spring Boot中,只要引入了spring-boot-starter-jdbc,通常自动配置
* 但显式配置有助于理解原理
*/
@Configuration
@EnableTransactionManagement // ✅ 开启事务管理
public class TransactionConfig {
/**
* 定义事务管理器Bean
* * @param dataSource 数据源(Spring自动注入)
* @return 事务管理器
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
// DataSourceTransactionManager是JDBC的事务管理器
// 它管理数据库连接的获取、提交、回滚
return new DataSourceTransactionManager(dataSource);
}
}
8.8 传播机制设置错误
问题描述
错误使用NEVER/NOT_SUPPORTED等传播机制,导致事务意外失效或抛出异常。
错误代码示例
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* 演示传播机制误用
*/
@Slf4j
@Service
public class PropagationMisuseService {
@Transactional
public void mainBusiness() {
// 主业务逻辑
// 【致命错误】调用NEVER传播机制的方法
// NEVER要求当前没有事务,但mainBusiness()有事务
// 结果:抛出IllegalTransactionStateException异常
this.noTransactionMethod();
}
@Transactional(propagation = Propagation.NEVER) // 要求无事务环境
public void noTransactionMethod() {
log.info("此方法不应在事务中执行");
}
// 另一个误用场景
@Transactional
public void anotherBusiness() {
// 【致命错误】调用NOT_SUPPORTED,挂起当前事务
// 导致主事务被挂起,操作不在事务保护中
this.notSupportedMethod();
// 后续操作在主事务中,但前面的操作已脱离事务
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedMethod() {
// 此方法以非事务方式运行,当前事务被挂起
// 如果这里操作数据库失败,不会回滚
logMapper.insert("日志"); // 不受主事务保护
}
}
正确解决方案
java
@Slf4j
@Service
public class PropagationMisuseService {
@Transactional
public void mainBusiness() {
// 主业务逻辑
// 【正确】如果需要独立事务,使用REQUIRES_NEW
this.independentMethod(); // 创建新事务,不影响主事务
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void independentMethod() {
log.info("此方法在独立事务中执行");
logMapper.insert("日志");
// 即使这里失败,也只回滚自己的事务
}
/**
* 【正确】明确传播机制的使用场景表
*/
public class PropagationGuide {
// 场景1:大多数业务 → REQUIRED(默认)
@Transactional(propagation = Propagation.REQUIRED)
public void normalBusiness() {
// 加入当前事务
}
// 场景2:必须独立 → REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void mustIndependent() {
// 审计日志、消息通知等
}
// 场景3:部分回滚 → NESTED
@Transactional(propagation = Propagation.NESTED)
public void partialRollback() {
// 可选操作,失败不影响主流程
}
// 场景4:查询 → readOnly = true
@Transactional(readOnly = true)
public void query() {
// 只读查询,优化性能
}
}
}
8.9 总结:事务失效检查清单
html
/**
* 在怀疑事务失效时,按此清单检查代码
*/
public class TransactionChecklist {
public void checkBeforeCommit() {
// □ 1. 方法是否是public?
// if (!Modifier.isPublic(method.getModifiers())) return false;
// □ 2. 方法是否是final/static?
// if (Modifier.isFinal(method.getModifiers())) return false;
// □ 3. 是否是同类自调用?
// if (invocation.getThis() == target) return false;
// □ 4. 异常是否正确抛出?
// if (exceptionCaughtButNotRethrown) return false;
// □ 5. 传播机制是否正确?
// if (propagation == Propagation.NEVER && hasTransaction) return false;
// □ 6. 数据库引擎是否支持?
// if (engine != "InnoDB") return false;
// □ 7. 事务管理器是否配置?
// if (txManager == null) return false;
// □ 8. 是否在多线程中调用?
// if (Thread.currentThread() != mainThread) return false;
}
}
终极建议:写完后问自己一句:"我的代码真的能走到Spring的代理逻辑里吗?" 只要记住这一点,80%的事务失效问题都能避免。