Spring事务避坑笔记:从入门到解决自调用陷阱
(适合事务小白的入门级分享)
一、先搞懂:什么是事务?为什么需要它?
1. 事务的"人话定义"
事务就是一组要么全成功、要么全失败的操作。比如银行转账:从A账户扣钱、给B账户加钱,这两步必须同时成功------如果扣钱后系统崩溃,加钱没执行,A的钱就"凭空消失"了,这时候就需要事务让扣钱操作"回滚",恢复到之前的状态。
2. 事务的核心要求:ACID(大白话版)
- 原子性:像原子一样不可分割,要么全成,要么全败
- 一致性:操作前后数据符合业务规则(比如转账后总金额不变)
- 隔离性:多个事务同时执行时互不干扰(比如A转账给B时,C查A的余额不会看到"扣了没加"的中间状态)
- 持久性:成功后数据永久保存(断电也不会丢)
二、Spring事务入门:最常用的@Transactional注解
Spring把复杂的事务操作封装成了@Transactional注解,加在方法上就能实现事务控制,小白也能快速上手。
1. 基础用法(复制就能用)
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryMapper inventoryMapper;
// 加了这个注解,方法里的操作就有了事务保障
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 1. 保存订单
orderMapper.insertOrder(dto);
// 2. 扣减库存
inventoryMapper.decreaseStock(dto.getProductId(), dto.getCount());
}
}
2. 小白必知的3个默认规则
- 回滚规则 :默认只回滚
RuntimeException(运行时异常,比如空指针),不回滚Exception(编译时异常,比如IO异常)。所以必须加rollbackFor = Exception.class,确保所有异常都能回滚。 - 传播行为 :默认
REQUIRED(如果已有事务就加入,没有就新建)------简单理解:"跟着大部队走",主方法的事务会包含子方法。 - 生效前提 :注解只能加在
public方法上,非public方法(private/protected)加了也白加。
三、核心坑:自调用为什么会让事务失效?
1. 先看一个"失效案例"
同一个Service里,A方法加了事务,调用本类的B方法(也加了事务),结果B方法的事务没生效:
java
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void A() { // 主方法,有事务
// 业务操作
this.B(); // 调用本类的B方法(自调用)
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void B() { // 子方法,想开启新事务,但失效了
inventoryMapper.decreaseStock(...);
}
}
2. 失效原因:Spring事务的"代理机制"
Spring事务是靠AOP代理实现的------简单说,Spring会给你的Service创建一个"代理对象"(类似"中介"),所有外部调用都会先经过代理,由代理来开启/提交/回滚事务。
但自调用(this.B())是"直接找本人",绕开了代理中介,代理没参与,B方法的@Transactional注解自然就没被执行,事务也就失效了。
比喻理解 :
你找中介(代理)租房,中介会帮你办合同(事务);但你直接找房东(this),中介没参与,自然不会帮你办合同。
四、小白也能学会的5种解决方案(按推荐度排序)
方案1:拆分Service(最推荐,符合设计规范)
原理
把需要独立事务的方法(比如B方法)拆到另一个Service里,通过注入调用------此时调用的是对方的代理对象,事务就生效了。
实操代码
- 新建一个专门处理库存的Service:
java
@Service
public class InventoryService {
// 独立的事务方法,拆到新Service
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void decreaseStock(...) {
inventoryMapper.decreaseStock(...);
}
}
- 原Service注入新Service调用:
java
@Service
public class OrderService {
@Autowired
private InventoryService inventoryService; // 注入新Service
@Transactional(rollbackFor = Exception.class)
public void A() {
// 调用其他Service的方法(走代理,事务生效)
inventoryService.decreaseStock(...);
}
}
优点
- 代码结构清晰,一个Service只做一件事(符合"单一职责")
- 无技术"黑魔法",后续维护方便(同事接手也能看懂)
方案2:自注入当前Service(快速解决,改动最小)
原理
在当前Service里注入自己的代理对象,用注入的对象代替this调用方法------相当于"绕回中介"。
实操代码
java
@Service
public class OrderService {
// 关键:注入自己的代理对象
@Autowired
private OrderService selfService;
@Transactional(rollbackFor = Exception.class)
public void A() {
// 用注入的代理对象调用B方法(事务生效)
selfService.B();
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void B() {
inventoryMapper.decreaseStock(...);
}
}
优点
- 不用拆分类,代码改动极小
- 小白也能快速复制使用
注意
- 不能用构造器注入(会报循环依赖错误),用
@Autowired字段注入即可
方案3:AopContext获取代理(通用方案,需加配置)
原理
通过Spring提供的AopContext工具类,直接获取当前Service的代理对象,手动绕回代理调用。
实操步骤
- 启动类加配置(开启代理暴露):
java
@SpringBootApplication
// 关键配置:暴露代理对象到AopContext
@EnableAspectJAutoProxy(exposeProxy = true)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
- Service里用代理对象调用:
java
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void A() {
// 关键:获取代理对象
OrderService proxy = (OrderService) AopContext.currentProxy();
proxy.B(); // 用代理调用,事务生效
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void B() {
inventoryMapper.decreaseStock(...);
}
}
方案4:AspectJ静态织入(彻底解决,配置稍复杂)
原理
前面的方案都是靠"动态代理",而AspectJ是"静态织入"------编译时就把事务逻辑嵌入到类的字节码里,不管是this调用还是外部调用,事务都能生效。
适用场景
- 自调用场景极多,拆分Service成本太高
- 需要对private方法加事务(动态代理不支持)
核心步骤
- 加依赖(pom.xml):
xml
<!-- AspectJ织入器 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!-- Spring AspectJ集成 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
- 启动类改配置:
java
@SpringBootApplication
// 启用AspectJ模式的事务管理
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class DemoApplication { ... }
- 原代码不用改!
this调用自动生效:
java
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void A() {
this.B(); // 自调用也能生效了
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void B() { ... }
}
方案5:编程式事务(灵活控制,侵入性强)
原理
放弃@Transactional注解,手动写代码控制事务的开启、提交、回滚------完全不依赖代理,自然没有自调用问题。
实操代码
java
@Service
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate; // 事务模板
public void A() {
// 主事务
transactionTemplate.execute(status -> {
orderMapper.insertOrder(...);
// 调用子方法(手动控制事务)
B();
return null;
});
}
// 子方法:手动开启新事务
private void B() {
transactionTemplate.execute(status -> {
inventoryMapper.decreaseStock(...);
return null;
});
}
}
适用场景
- 事务逻辑特别复杂(比如根据条件动态决定是否回滚)
- 不希望依赖AOP代理的场景
五、小白必会:如何验证事务是否生效?
光改代码不够,得确认事务真的起作用了,3个简单方法:
1. 调试看代理对象
在Service方法里打个断点,鼠标悬停在this上:
- 如果显示
XXXService$$EnhancerBySpringCGLIB$$xxx(带CGLIB后缀),说明代理创建成功 - 如果直接显示
XXXService(无后缀),说明代理没创建,事务肯定失效
2. 加日志看事务状态
在方法里加一行日志,打印是否在事务中:
java
// 导入这个类
import org.springframework.transaction.support.TransactionSynchronizationManager;
public void B() {
// 打印true说明事务生效,false说明失效
System.out.println("事务是否生效:" + TransactionSynchronizationManager.isActualTransactionActive());
inventoryMapper.decreaseStock(...);
}
3. 异常测试法
故意在方法里抛个异常,看数据是否回滚:
java
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
orderMapper.insertOrder(dto);
// 故意抛异常
if (dto.getCount() > 10) {
throw new RuntimeException("数量超标");
}
inventoryMapper.decreaseStock(...);
}
- 若订单表没有新增数据,说明事务回滚成功
- 若订单表有数据,说明事务失效
六、总结:小白避坑核心要点
- 事务生效的前提 :必须是Spring代理对象调用
public方法,@Transactional才会生效 - 自调用失效的根源 :
this调用绕开了代理,注解没被执行 - 解决方案优先级 :
- 优先选「拆分Service」(规范、易维护)
- 快速解决选「自注入」或「AopContext」(改动小)
- 复杂场景选「AspectJ」(彻底解决)
- 必记小技巧 :
- 注解必加
rollbackFor = Exception.class - 调试看
this是否有代理后缀 - 抛异常测试回滚是否生效
- 注解必加
按照这个思路,不管是自己写代码还是排查别人的问题,都能快速定位事务相关的坑啦!