【JavaEE32-后端部分】Spring事务进阶:@Transactional三大利器,把事务玩明白【AI辅助理解】
老铁们,上回咱们学会了用
@Transactional这个注解搞定事务,再也不用自己写那一坨开启、提交、回滚的代码了,爽不爽?
但是,咱们真的会用@Transactional吗?
- 为啥我抛了个
IOException(读作“ai-o-exception”,就是输入输出异常,比如文件找不到、网络断了),事务它就不回滚呢?- 啥叫脏读?啥叫不可重复读?啥叫幻读?隔离级别又是个啥玩意儿?
- 一个事务方法调另一个事务方法,它们到底是“穿一条裤子”还是“各过各的”?
别慌,今天这篇,咱们就把
@Transactional身上的三个核心属性——rollbackFor(读作“肉贝壳
佛”,意思是“回滚条件”)、isolation(读作“爱骚雷神”,意思是“隔离级别”)、propagation(读作“普罗帕给神”,意思是“传播机制”) 向大家介绍清楚
声明:本文AI辅助理解,本人整理,阅读的老铁别忘了重点看 第五点 小贴士 帮你避坑哦。
开场:一个让哥们儿社死的翻车现场
先给老铁们讲个趣事。
有一哥们儿,刚入职一家公司写支付系统。有一天他写了个退款功能,逻辑大概是这样的:
- 从用户账户里把钱扣回来(退款嘛)
- 记录一条退款日志
- 调用第三方接口通知商家“钱退了啊”
他觉得万无一失,给方法上加了个 @Transactional。结果测试的时候,第三方接口挂了(抛了个 IOException,就是网络异常)。他心想:“没事,事务会回滚,钱不会少。”
结果一查数据库——钱扣了,日志也记了,但第三方通知失败了。用户炸了,老板也炸了。
为啥?因为 IOException 这玩意儿,在 Java 里属于非运行时异常(也叫检查型异常)。@Transactional 默认只回滚运行时异常(比如除以零、空指针),对这种“外部问题”它不管。这哥们儿当场社死。
所以今天这篇,咱们就从 rollbackFor 开始,把事务的底裤都扒干净。
一、rollbackFor(回滚条件)—— 让“所有异常”都回滚

1.1 默认只回滚运行时异常,这是个坑
咱先搞清楚:Java 里的异常分两类。
- 运行时异常(
RuntimeException):编译的时候不强制你处理。比如除以零(10 / 0)、数组越界、空指针。这种通常是代码写错了。 - 非运行时异常(也叫检查型异常,
Checked Exception):编译的时候必须处理,要么try-catch,要么throws。比如文件找不到、网络断了、数据库连不上(IOException、SQLException)。
@Transactional 默认的规则是:只对运行时异常和 Error 回滚,对非运行时异常不回滚。
Spring 为啥这么设计?它觉得:非运行时异常通常是外部环境问题(网络、IO、数据库),不一定代表你的业务数据错了,所以它就不帮你回滚。
来,咱们验证一下。
写一个注册用户的方法,注册成功后手动抛一个 IOException(假装网络断了)。
@Service
public class UserService {
@Transactional
public void register(String name, String password) throws IOException {
// 插入用户
userMapper.insertUser(name, password);
log.info("用户插入成功");
// 故意抛个 IO 异常
throw new IOException("网络异常,假装连不上第三方");
}
}
运行,你会发现:用户数据成功插进数据库了! 事务没回滚!这就是那个哥们儿翻车的原因😄。
1.2 rollbackFor = Exception.class,把所有异常都拉回来
那怎么办?用 rollbackFor 属性。
rollbackFor 的意思就是“回滚条件”。你给它指定一个异常类型,告诉 Spring:“只要遇到这种异常,就给我回滚。”
Exception 是所有异常的爹。你写 rollbackFor = Exception.class,意思就是:“甭管啥异常,运行时还是非运行时,统统给我回滚!”
@Transactional(rollbackFor = Exception.class)
public void register(String name, String password) throws IOException {
userMapper.insertUser(name, password);
throw new IOException("网络异常");
}
再运行,抛出 IOException 后,事务乖乖回滚,数据库干干净净。
你还可以指定多个异常,比如:
@Transactional(rollbackFor = {IOException.class, SQLException.class})
意思是:只有遇到 IO异常 或 SQL异常 才回滚,别的异常不管。
1.3 总结一下 rollbackFor
| 写法 | 效果 |
|---|---|
@Transactional |
只回滚运行时异常(RuntimeException)和 Error |
@Transactional(rollbackFor = Exception.class) |
所有异常都回滚 |
@Transactional(rollbackFor = {IOException.class}) |
只有指定的异常才回滚 |
老铁们记住:如果你的方法里调了文件上传、网络请求、第三方接口,一定加上 rollbackFor = Exception.class,否则钱飞了你都不知道喔,😁。
二、isolation(隔离级别)—— 解决多人抢数据的“打架”问题

2.1 三个读数据的问题,用生活例子讲
多个事务同时操作同一张表,就像多个人同时改同一个 Excel 文件,会出幺蛾子。主要有三个问题:
- 脏读:你读到别人还没提交的数据。人家万一反悔了(回滚),你读到的就是垃圾。
- 不可重复读:你在同一个事务里两次读同一行数据,结果不一样。因为中间被别人改了并提交了。
- 幻读:你在同一个事务里两次查询,记录条数不一样。因为中间被别人插入了新数据。
举个例子(用你和你老婆同时登录网银来理解):
- 脏读:你老婆转了 100 元给你,还没点“确认”。这时候你查余额,发现多了 100 元。结果她点了“取消”,钱根本没转。你刚才看到的是假数据。
- 不可重复读:你先查余额 1000 元,然后你老婆转了 100 元并确认。你再查余额变成 900 元。两次结果不一样。
- 幻读:你先查订单列表有 10 条,然后你老婆下了一个新订单并确认。你再查变成 11 条。多出来一条像幻觉一样。

2.2 MySQL 的四种隔离级别(从松到严)
数据库用“隔离级别”来控制这些问题。级别越高越安全,但性能越低(因为要加锁)。
| 隔离级别(英文) | 中文名 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|---|
read uncommitted |
读未提交 | 可能 | 可能 | 可能 | 最高 |
read committed |
读已提交 | 不会 | 可能 | 可能 | 较高 |
repeatable read |
可重复读 | 不会 | 不会 | 不会 | 一般(MySQL默认) |
serializable |
串行化 | 不会 | 不会 | 不会 | 最低 |
注意:MySQL InnoDB 在
repeatable read级别下,通过 MVCC + 间隙锁(Next-Key Lock)解决了当前读的幻读问题,普通快照读不存在幻读。日常业务开发中,我们可以认为不会出现幻读。
2.3 Spring 里的隔离级别怎么用

Spring 提供了 5 种选项:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoney() {
// 业务代码
}
各种隔离级别的中文意思:
Isolation.DEFAULT:用数据库自己的(MySQL 默认是repeatable read)Isolation.READ_UNCOMMITTED:读未提交(最不安全,基本不用)Isolation.READ_COMMITTED:读已提交(常用,性能好)Isolation.REPEATABLE_READ:可重复读(MySQL 默认,安全)Isolation.SERIALIZABLE:串行化(最安全,但最慢,基本不用)
老铁们,实际开发怎么选?
- 普通业务(比如论坛、评论)用
DEFAULT就完事了。 - 高并发、读多写少的场景,用
READ_COMMITTED,性能更好。 - 金融、支付、库存这种对数据一致性要求高的,用
REPEATABLE_READ。 SERIALIZABLE千万慎用,慢到你怀疑人生。
三、propagation(传播机制)—— 方法调用时事务咋传递

3.1 啥是传播机制?举个例子你就懂了
先看一段代码:
@Transactional
public void methodA() {
// 干点啥
methodB(); // 调用 methodB
}
@Transactional
public void methodB() {
// 干点啥
}
当 methodA 调用 methodB 时,methodB 是跟着 methodA 共用同一个事务,还是 methodB 自己单独开一个新事务?
这就是事务传播机制——它决定了事务方法之间调用时,事务怎么传递。
生活例子:你和同事都在写报告,你们都有各自的“工作单”。你写了部分内容后,需要同事帮忙。同事是直接在你的工作单上继续写(加入你的事务),还是自己另开一张新工作单(新建独立事务)?这就是传播机制。
重要前提:必须是不同类之间的方法调用,AOP 才能生效。同一个类里,方法 A 调方法 B,事务是不生效的!这是 Spring 代理的坑。
3.2 Spring 的 7 种传播行为(重点记 2 个)
Spring 提供了 7 种传播行为,但咱普通人只需要记住两个最常用的,其他的了解就行。
| 传播行为(英文) | 中文意思 | 干啥用的 |
|---|---|---|
required |
必需的 | 有事务就加入,没有就新建(默认) |
requires_new |
需要新的 | 不管有没有事务,都新建一个独立事务 |
supports |
支持 | 有事务就加入,没有就算了 |
mandatory |
强制的 | 必须有事务,没有就报错 |
not_supported |
不支持 | 不用事务,有事务就先挂起 |
never |
绝不 | 不能用事务,有事务就报错 |
nested |
嵌套 | 嵌套事务,可以局部回滚 |
用生活例子帮你记(结婚买房版):
required:必须有房。你有房我就跟你住,你没房咱俩一起买。requires_new:必须买新房。你有房我也不住,必须两人一起买一套新的。supports:有房就住,没房就租房。随缘。mandatory:必须有房,没房就不结婚。霸道。not_supported:不要房。你有房我也不住,必须租房。never:绝对不能有房。你有房咱就分手。nested:有房就住,但允许你在房子里搞点副业(局部回滚不影响整体)。
3.3 重点演示:required vs requires_new
咱用“用户注册 + 记录日志”这个经典场景来演示。
场景:用户注册成功了要记一条日志。但是记录日志的方法可能会失败(比如抛异常)。
情况1:两个方法都用 required(默认,穿一条裤子)
@Service
public class UserService {
@Transactional(propagation = Propagation.REQUIRED)
public void register() {
userMapper.insert();
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRED)
public void addLog() {
int i = 1 / 0; // 故意除零,抛异常
logMapper.insert();
}
}
@Service
public class BizService {
@Transactional
public void doRegister() {
userService.register(); // 成功
logService.addLog(); // 抛异常
}
}
运行结果:addLog 抛异常 → 整个事务回滚 → 用户也没注册成功。这就是“穿一条裤子”,一荣俱荣,一损俱损。
情况2:LogService 用 requires_new(各过各的)
把 LogService 改成:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addLog() {
int i = 1 / 0; // 还是抛异常
}
运行结果:addLog 抛异常,但它自己开了一个新事务,只回滚自己的操作,不影响外层事务。用户注册成功,日志没记上。这就是“各过各的”,你炸了跟我没关系。
3.4 实际开发怎么选?
- 90% 的场景用
required就够了。 - 什么时候用
requires_new?当内层操作允许失败且不应该影响主业务的时候。比如:记录日志、发送通知、写消息队列。这些操作失败了,不应该让用户注册失败。 nested很少用,知道有这么个东西就行。
四、一张表记住三大属性
| 属性(英文) | 中文意思 | 干啥的 | 常用值 |
|---|---|---|---|
rollbackFor |
回滚条件 | 控制哪些异常触发回滚 | Exception.class(所有异常) |
isolation |
隔离级别 | 控制并发读的问题 | DEFAULT 或 READ_COMMITTED |
propagation |
传播机制 | 控制事务方法调用时咋传递 | REQUIRED(默认)或 REQUIRES_NEW |
五、血泪实战小贴士(必看!)
1. 一定加 rollbackFor = Exception.class
只要你的方法里调了网络、文件、第三方接口,就别偷懒。否则一个 IOException 就能让你的钱飞走。
2. 事务只加在 Service 层,Controller 和 Mapper 都不行
很多老铁刚学的时候,喜欢在 Controller 的方法上直接加 @Transactional,或者在 Mapper 接口的方法上加。这两种都是错误用法,咱一个一个说。
为什么不能加在 Controller 层?
职责问题:
Controller 的职责是接收请求、参数校验、调用 Service、返回响应。它不应该关心“数据库事务怎么开怎么关”这种业务逻辑层的事情。
如果你在 Controller 上加事务,会带来几个问题:
- 一个 Controller 方法可能调用多个 Service,这些 Service 可能各自有自己的事务,你强行在 Controller 上开一个事务,会把多个不相关的业务操作绑在一起,导致不该回滚的也回滚了。
- 事务会延长数据库连接的持有时间,从 Controller 进入开始一直到方法结束,连接一直被占用,在高并发下容易造成连接池耗尽。
- 违反分层架构:表现层(Controller)不应该包含业务逻辑,事务属于业务逻辑的范畴,应该放在业务层(Service)。
举个翻车例子:
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private LogService logService;
@Transactional // ❌ 错误!事务加在 Controller
@PostMapping("/create")
public Result createOrder(@RequestBody OrderDto dto) {
orderService.create(dto); // 下单
logService.saveLog("创建订单"); // 记日志
return Result.ok();
}
}
如果 logService.saveLog() 抛异常,整个事务回滚,订单也会被回滚。但记日志失败真的应该导致下单失败吗?显然不合理。日志应该用 REQUIRES_NEW 独立事务,而 Controller 上的 @Transactional 把所有操作强行绑在了一起。
正确做法:Controller 不加事务,让 Service 自己控制。
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/create")
public Result createOrder(@RequestBody OrderDto dto) {
orderService.createOrder(dto); // Service 内部自己管理事务
return Result.ok();
}
}
为什么不能加在 Mapper 层?
Mapper 只是执行单条 SQL 的接口。事务的本质是将多个数据库操作捆绑成一个原子操作,如果你只在 Mapper 的单个方法上加 @Transactional,那相当于每个 SQL 自己开一个事务,跟没开一样。
而且,Mapper 是数据访问层,它不应该知道“哪些操作要放在一起”。这是业务层(Service)的职责。
举个反例:
@Mapper
public interface UserMapper {
@Transactional // ❌ 完全没用,而且没有意义
@Insert("insert into user...")
int insert(User user);
}
这个事务只包裹了一条 SQL,根本起不到“多个操作一起成功或失败”的作用。
事务应该加在哪一层?
答案:Service 层(业务逻辑层)。
因为一个业务往往由多个数据访问操作组成(比如:扣库存 + 创建订单 + 减余额)。把这些操作放在一个 Service 方法里,然后在这个 Service 方法上加 @Transactional,才是正确姿势。
@Service
public class OrderService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockMapper stockMapper;
@Transactional(rollbackFor = Exception.class) // ✅ 正确:加在 Service 方法上
public void createOrder(OrderDto dto) {
// 1. 扣库存
stockMapper.deduct(dto.getProductId(), dto.getCount());
// 2. 创建订单
orderMapper.insert(dto);
// 3. 减用户余额
userMapper.reduceBalance(dto.getUserId(), dto.getAmount());
}
}
总结:
| 层级 | 能否加事务 | 原因 |
|---|---|---|
| Controller | ❌ 不能 | 职责是接收请求,不应包含业务事务 |
| Service | ✅ 应该 | 业务逻辑层,一个业务包含多个数据操作 |
| Mapper | ❌ 不能 | 单条 SQL 不需要事务,且分层不合理 |
3. 异常被 catch 住不抛出 = 事务不回滚
如果你用 try-catch 把异常吃了,Spring 感知不到,事务照常提交。要么重新抛出,要么手动回滚:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
4. 同类内部方法调用,事务不生效(这个坑很多人踩!)
先说原因: 因为 Spring 事务靠代理,自己调自己绕过了代理。必须跨类调用。
老铁,这个问题一定要搞清楚:是事务本身不生效,不是传播机制不生效。
Spring 的事务是靠 AOP 代理 实现的。从外部调用你类的方法时,调用的是 Spring 生成的代理对象,代理对象会帮你开启、提交、回滚事务。但是,当你在类内部通过 this.methodB() 调用自己的另一个方法时,用的是原始对象,不是代理对象,所以事务增强的代码根本不会执行。
举个例子:
@Service
public class UserService {
@Transactional
public void methodA() {
this.methodB(); // ❌ 内部调用,事务不生效!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 这个事务不会开启,因为是通过 this 直接调用的
}
}
从外面调用 userService.methodA() 时,methodA 本身的事务是生效的(因为是外部调用)。但 methodA 里面通过 this.methodB() 调用 methodB,methodB 上的 @Transactional 完全不生效,不会开启新事务,也不会加入现有事务。
那怎么解决? 让 Spring 帮你拿到代理对象,然后用代理对象调用内部方法。有两种常见方式:
方式一:自己注入自己(推荐,简单)
@Service
public class UserService {
@Autowired
private UserService self; // 注入自己的代理对象
@Transactional
public void methodA() {
self.methodB(); // ✅ 通过代理调用,事务生效
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() { }
}
方式二:使用 AopContext.currentProxy()(需要开启配置)
先在 Spring Boot 启动类或任意 @Configuration 配置类上加 @EnableAspectJAutoProxy(exposeProxy = true),然后:
@Service
public class UserService {
@Transactional
public void methodA() {
UserService proxy = (UserService) AopContext.currentProxy();
proxy.methodB(); // ✅ 通过代理调用
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() { }
}
一句话总结:同类内部调用,事务不生效,是因为直接调用了原始对象,绕过了 Spring 代理。不是传播机制不生效,而是事务压根儿就没开。遇到这种情况,用上面两种方式改成代理调用就行了。
5. requires_new 会挂起事务,有性能开销
别滥用,只在真正需要独立事务的地方用(比如日志、消息通知)。
结尾
老铁们,到这里,Spring 事务的核心用法就简单介绍完了。
- 上一篇:咱们学会了声明式事务,会用
@Transactional了。 - 这一篇:咱们把
rollbackFor(回滚条件)、isolation(隔离级别)、propagation(传播机制)这三个属性彻底吃透了。
觉得有用别忘了 点赞、收藏、关注,老铁们下期见!🚀
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)