@Transactional 注了等于没用?Spring 事务失效的 7 种场景,你踩过几个
写 Service 层代码,顺手加个
@Transactional,事务就生效了?想多了。生产环境里因为事务失效导致脏数据的事故,比我吃过的外卖都多。今天把 7 种高频失效场景一次性讲透,附修复方案和源码定位。
先搞清楚:事务为什么能生效?
Spring 事务基于 AOP 动态代理 。当你调用一个 @Transactional 方法时,实际调用链是:
bash
Controller
│
▼
代理对象.$methodName() ← AOP 拦截
│
├─ 开启事务 (Connection.setAutoCommit(false))
├─ 目标对象.$methodName() ← 你的业务代码
├─ 提交事务 / 回滚事务
│
▼
返回结果
事务生效的前提 = 请求必须经过代理对象。所有失效场景,归根结底都是请求没走代理。
场景一:同类内自调用(this 调用绕过代理)
这是排名第一的事务失效原因。
错误写法
java
@Service
public class OrderService {
public void createOrder(Order order) {
orderMapper.insert(order);
this.deductStock(order.getItemId(), order.getQuantity()); // ❌ 直接 this 调用
}
@Transactional(rollbackFor = Exception.class)
public void deductStock(String itemId, int qty) {
stockMapper.deduct(itemId, qty);
// 如果这里抛异常,事务不会回滚!
}
}
为什么失效?
this.deductStock() 是通过目标对象直接调用,不经过代理。AOP 拦截器根本没机会介入。
scss
createOrder()
│
├─ this.deductStock() ← 直接调用目标对象方法
│ │ 没有 Proxy,没有 AOP,没有事务
│ ▼
│ stockMapper.deduct()
│
▼
修复方案
方案 1:把方法拆到不同的 Service(推荐)
java
@Service
public class StockService {
@Transactional(rollbackFor = Exception.class)
public void deductStock(String itemId, int qty) {
stockMapper.deduct(itemId, qty);
}
}
@Service
public class OrderService {
@Autowired
private StockService stockService; // 注入的是代理对象
public void createOrder(Order order) {
orderMapper.insert(order);
stockService.deductStock(order.getItemId(), order.getQuantity()); // ✅ 走代理
}
}
方案 2:自己注入自己
java
@Service
public class OrderService {
@Autowired
@Lazy
private OrderService self; // 注入自己的代理对象
public void createOrder(Order order) {
orderMapper.insert(order);
self.deductStock(order.getItemId(), order.getQuantity()); // ✅ 走代理
}
}
方案 3:AopContext 获取当前代理
java
@EnableAspectJAutoProxy(exposeProxy = true) // 启动类加这个
((OrderService) AopContext.currentProxy())
.deductStock(itemId, qty); // ✅ 走代理
| 方案 | 优点 | 缺点 |
|---|---|---|
| 拆分 Service | 职责清晰,最干净 | 类变多 |
| 自注入 | 改动最小 | 循环依赖风险,需 @Lazy |
| AopContext | 一行搞定 | 侵入性强,需开启 exposeProxy |
场景二:标注在非 public 方法上
错误写法
java
@Service
public class PaymentService {
@Transactional(rollbackFor = Exception.class)
private void processPayment(Payment payment) { // ❌ private 方法
paymentMapper.insert(payment);
}
@Transactional(rollbackFor = Exception.class)
protected void refund(String orderId) { // ❌ protected 也不行
paymentMapper.refund(orderId);
}
}
为什么失效?
Spring AOP 事务拦截器 TransactionInterceptor 底层调用的是 MethodInvoker,在创建代理时,非 public 方法不会被事务通知织入。
看 Spring 源码 AbstractFallbackTransactionAttributeSource.computeTransactionAttribute():
java
// Spring 6.x 源码
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null; // 非 public 直接返回 null,不应用事务
}
修复方案
把方法改成 public,就这么简单:
java
@Transactional(rollbackFor = Exception.class)
public void processPayment(Payment payment) { // ✅ public
paymentMapper.insert(payment);
}
场景三:异常类型不对(默认只回滚 RuntimeException)
错误写法
java
@Service
public class OrderService {
@Transactional // ❌ 默认只回滚 RuntimeException 和 Error
public void createOrder(Order order) throws Exception {
orderMapper.insert(order);
if (order.getAmount() < 0) {
throw new Exception("金额不能为负"); // 受检异常,不会回滚!
}
}
}
为什么失效?
Spring 事务默认回滚策略遵循 EJB 规范:
| 异常类型 | 默认是否回滚 |
|---|---|
| RuntimeException 及其子类 | ✅ 回滚 |
| Error | ✅ 回滚 |
| 受检异常(checked Exception) | ❌ 不回滚 |
源码位置 DefaultTransactionAttribute.rollbackOn():
java
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
修复方案
方案 1:指定 rollbackFor
java
@Transactional(rollbackFor = Exception.class) // ✅ 所有异常都回滚
public void createOrder(Order order) throws Exception { ... }
方案 2:抛 RuntimeException
java
@Transactional
public void createOrder(Order order) {
if (order.getAmount() < 0) {
throw new RuntimeException("金额不能为负"); // ✅ 默认回滚
}
}
团队规范 :所有 @Transactional 统一加 rollbackFor = Exception.class,没得商量。
场景四:异常被 try-catch 吞掉
错误写法
java
@Service
public class TransferService {
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, BigDecimal amount) {
accountMapper.deduct(from, amount);
try {
accountMapper.add(to, amount); // 如果这里抛异常...
} catch (Exception e) {
log.error("转账失败", e); // ❌ 吞掉了,事务感知不到异常
}
}
}
为什么失效?
事务回滚依赖 TransactionInterceptor 捕获目标方法抛出的异常。你在方法内部 catch 住了,代理层看到的是"正常返回",于是提交事务。
scss
代理对象.transfer()
│
├─ 开启事务
├─ 目标方法执行
│ ├─ deduct(from, amount) ← 已执行
│ ├─ add(to, amount) 抛异常
│ └─ catch 吞掉了 ← 代理层看不到异常
├─ 提交事务 ← 😱 deduct 生效了,add 没生效
▼
修复方案
方案 1:catch 后重新抛出
java
try {
accountMapper.add(to, amount);
} catch (Exception e) {
log.error("转账失败", e);
throw new RuntimeException("转账失败", e); // ✅ 代理层能感知
}
方案 2:手动标记回滚
java
@Autowired
private TransactionStatus transactionStatus;
// 或者更优雅的方式
try {
accountMapper.add(to, amount);
} catch (Exception e) {
log.error("转账失败", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // ✅ 标记回滚
}
方案 3:业务不允许用 try-catch 包住事务方法
把事务方法设计成"要么全成功,要么全回滚",不要在方法内部做 catch。
场景五:数据库引擎不支持事务
错误写法
sql
-- 建表用了 MyISAM 引擎
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
...
) ENGINE = MyISAM; -- ❌ MyISAM 不支持事务!
为什么失效?
MySQL 的 MyISAM 引擎不支持事务,InnoDB 才支持。如果你在 MyISAM 表上加 @Transactional,Spring 不会报错,但事务根本不生效------COMMIT 和 ROLLBACK 都是空操作。
验证方式
sql
SELECT ENGINE FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'orders';
修复方案
建表时明确指定 InnoDB:
sql
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
...
) ENGINE = InnoDB; -- ✅ InnoDB 支持事务
MySQL 5.5+ 默认引擎已经是 InnoDB,老项目升级时要注意检查。
场景六:传播行为配置错误
错误写法
java
@Service
public class ReportService {
@Transactional(rollbackFor = Exception.class)
public void generateReport() {
reportMapper.insertHeader();
detailService.saveDetails(); // 内层方法
reportMapper.updateStatus("DONE");
}
}
@Service
public class DetailService {
@Transactional(propagation = Propagation.REQUIRES_NEW,
rollbackFor = Exception.class)
public void saveDetails() {
detailMapper.batchInsert(details);
}
}
问题分析
当 saveDetails() 配置了 REQUIRES_NEW:
- 外层事务挂起,内层开启一个新事务
- 如果
saveDetails()成功提交后,外层后续代码报错,内层事务不会回滚
scss
外层事务 T1
│
├─ insertHeader() ← T1
├─ saveDetails()
│ ├─ 挂起 T1
│ ├─ 开启新事务 T2
│ ├─ batchInsert() ← T2
│ ├─ 提交 T2 ✅ ← 内层已提交,不可逆
│ └─ 恢复 T1
│
├─ updateStatus() 抛异常
├─ 回滚 T1 ← insertHeader 回滚了,但 saveDetails 没有
▼
传播行为速查表
| 传播行为 | 含义 | 使用场景 |
|---|---|---|
| REQUIRED(默认) | 有事务就加入,没有就新建 | 90% 场景用这个 |
| REQUIRES_NEW | 总是新建事务,挂起外层 | 日志记录(不希望日志失败影响主事务) |
| NESTED | 嵌套事务(savepoint) | 内层可独立回滚,外层也可回滚内层 |
| SUPPORTS | 有事务就加入,没有就非事务执行 | 查询方法 |
| NOT_SUPPORTED | 非事务执行,挂起当前事务 | 不需要事务的操作 |
| MANDATORY | 必须在事务内调用 | 强制要求在事务中 |
| NEVER | 必须非事务调用 | 强制不要事务 |
修复方案 :大部分情况下用默认的 REQUIRED 就够了,别瞎改传播行为。
场景七:多线程环境下事务失效
错误写法
java
@Service
public class BatchService {
@Transactional(rollbackFor = Exception.class)
public void batchProcess(List<Order> orders) {
orders.parallelStream().forEach(order -> { // ❌ 并行流 = 多线程
orderMapper.update(order); // 每个线程拿到的 Connection 不同
});
}
}
为什么失效?
Spring 事务通过 ThreadLocal 绑定 Connection 到当前线程。子线程拿不到父线程的 Connection,它拿到的是一个新的连接,自然不在同一个事务里。
css
主线程 (事务 Connection-A)
│
├─ Thread-1 → Connection-B ← 不同连接,不同事务
├─ Thread-2 → Connection-C ← 不同连接,不同事务
└─ Thread-3 → Connection-D ← 不同连接,不同事务
修复方案
方案 1:不用多线程处理事务方法(最简单)
java
@Transactional(rollbackFor = Exception.class)
public void batchProcess(List<Order> orders) {
for (Order order : orders) { // ✅ 单线程顺序处理
orderMapper.update(order);
}
}
方案 2:必须并行时,手动管理事务
java
public void batchProcess(List<Order> orders) {
List<CompletableFuture<Void>> futures = orders.stream()
.map(order -> CompletableFuture.runAsync(() -> {
transactionTemplate.execute(status -> { // ✅ 每个线程独立事务
orderMapper.update(order);
return null;
});
}, executor))
.collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
注意:方案 2 每个线程是独立事务,不是整体一个事务。如果需要"全部成功或全部失败",必须在应用层做补偿。
七种场景一图总览
kotlin
┌──────────────────────────────────────────────────────┐
│ @Transactional 失效的 7 种场景 │
│ │
│ ① this 自调用 ──── 绕过代理,AOP 不拦截 │
│ │
│ ② 非 public ──── Spring 只织入 public 方法 │
│ │
│ ③ 异常类型错 ──── 默认只回滚 RuntimeException │
│ │
│ ④ try-catch 吞 ──── 代理层感知不到异常 │
│ │
│ ⑤ 引擎不支持 ──── MyISAM 无事务能力 │
│ │
│ ⑥ 传播行为错 ──── REQUIRES_NEW 导致内外隔离 │
│ │
│ ⑦ 多线程 ──── ThreadLocal 跨线程失效 │
│ │
├──────────────────────────────────────────────────────┤
│ 本质:所有失效都指向一个根因 ------ 请求没经过代理对象 │
├──────────────────────────────────────────────────────┤
│ 记忆口诀: │
│ 自调非公异常吞,引擎传播多线程 │
│ 七种场景全搞懂,事务面试能加分 │
└──────────────────────────────────────────────────────┘
修复 Checklist:Code Review 时对照检查
| # | 检查项 | 快速判断 |
|---|---|---|
| 1 | 是否有 this.方法名() 调用 @Transactional 方法 |
搜索 this. + @Transactional |
| 2 | @Transactional 方法是否为 public |
IDE 报警告 |
| 3 | 是否指定了 rollbackFor = Exception.class |
团队规约检查 |
| 4 | 事务方法内是否有 catch 吞异常 | 搜索 try-catch |
| 5 | 表引擎是否为 InnoDB | SHOW CREATE TABLE |
| 6 | 传播行为是否为默认 REQUIRED | 非必要不改 |
| 7 | 是否在多线程中调用事务方法 | 搜索 parallelStream/CompletableFuture |
下篇预告
下一篇我们聊 Spring Boot 自动配置原理 :@EnableAutoConfiguration 到底做了什么?spring.factories 和 AutoConfiguration.imports 有什么区别?@Conditional 系列注解怎么控制 Bean 加载?------从源码层面把自动配置的骨架拆清楚。
本文是 Java 技术系列第 2 篇,系列目录:
- Spring 循环依赖到底怎么解的?三级缓存源码拆解
- 本文:@Transactional 注了等于没用?Spring 事务失效的 7 种场景