@Transactional 失效的 7 种场景:第 5 种最难排查
加了 @Transactional 事务还是没回滚?不是注解的问题,是你的用法有问题。7 种失效场景,你至少踩过 3 个。
一、事故现场
线上有个工单状态更新接口,先更新工单状态,再写操作日志。两个操作要在同一个事务里,要么都成功,要么都回滚。
java
@Service
public class TicketService {
@Autowired
private TicketMapper ticketMapper;
@Autowired
private OperationLogMapper logMapper;
@Transactional
public void updateTicketStatus(Long ticketId, String status) {
ticketMapper.updateStatus(ticketId, status); // 更新工单状态
logMapper.insert(new OperationLog(ticketId, status)); // 写操作日志
}
}
测试环境跑得好好的。上线后某天,工单状态更新了,但操作日志没写进去。事务没回滚,@Transactional 失效了。
排查发现:这个方法被同一个类的另一个方法调用了(自调用),@Transactional 的 AOP 代理没生效。
二、@Transactional 为什么会失效
@Transactional 的原理是 Spring AOP 动态代理。Spring 在 Bean 初始化时生成一个代理对象,代理对象在方法执行前后管理事务的开启和提交。
凡是绕过代理对象的调用,@Transactional 都不生效。
这个原理决定了 7 种失效场景,下面逐个讲。
三、7 种失效场景
场景 1:自调用(最常见)
java
@Service
public class TicketService {
@Transactional
public void updateTicketStatus(Long ticketId, String status) {
ticketMapper.updateStatus(ticketId, status);
logMapper.insert(new OperationLog(ticketId, status));
}
public void doUpdate(Long ticketId, String status) {
// 自调用:this 调用,走的是原始对象不是代理对象
updateTicketStatus(ticketId, status); // @Transactional 失效!
}
}
doUpdate 调用 this.updateTicketStatus(),this 是原始对象不是 Spring 代理对象。代理拦截不到,事务不生效。
解决:
java
// 方案 1:拆到另一个 Service
@Service
public class TicketTxService {
@Transactional
public void updateTicketStatus(Long ticketId, String status) {
ticketMapper.updateStatus(ticketId, status);
logMapper.insert(new OperationLog(ticketId, status));
}
}
@Service
public class TicketService {
@Autowired
private TicketTxService ticketTxService;
public void doUpdate(Long ticketId, String status) {
ticketTxService.updateTicketStatus(ticketId, status); // 走代理,事务生效
}
}
// 方案 2:注入自己的代理
@Service
public class TicketService {
@Autowired
@Lazy
private TicketService self;
public void doUpdate(Long ticketId, String status) {
self.updateTicketStatus(ticketId, status); // 走代理
}
}
场景 2:方法不是 public
java
@Service
public class TicketService {
@Transactional
void updateTicketStatus(Long ticketId, String status) { // 包级私有!
ticketMapper.updateStatus(ticketId, status);
logMapper.insert(new OperationLog(ticketId, status));
}
}
Spring AOP 默认只代理 public 方法。protected、private、包级私有方法上的 @Transactional 不生效。
解决: 改成 public。
java
@Transactional
public void updateTicketStatus(Long ticketId, String status) { // ✅ public
// ...
}
场景 3:异常被 catch 了
java
@Service
public class TicketService {
@Transactional
public void updateTicketStatus(Long ticketId, String status) {
ticketMapper.updateStatus(ticketId, status);
try {
logMapper.insert(new OperationLog(ticketId, status));
} catch (Exception e) {
log.error("写日志失败", e); // 异常被吞了!
// 事务不会回滚,因为 Spring 没看到异常
}
}
}
Spring 通过捕获方法抛出的异常来判断是否回滚。异常被 catch 了,Spring 看不到异常,认为方法正常执行完,提交事务。
解决:
java
@Transactional
public void updateTicketStatus(Long ticketId, String status) {
ticketMapper.updateStatus(ticketId, status);
try {
logMapper.insert(new OperationLog(ticketId, status));
} catch (Exception e) {
log.error("写日志失败", e);
throw e; // ✅ 重新抛出,让 Spring 感知到异常
}
}
场景 4:异常类型不对(最难排查)
java
@Service
public class TicketService {
@Transactional // 默认只回滚 RuntimeException 和 Error
public void updateTicketStatus(Long ticketId, String status) throws Exception {
ticketMapper.updateStatus(ticketId, status);
if (!status.equals("VALID")) {
throw new Exception("状态不合法"); // 受检异常,不回滚!
}
logMapper.insert(new OperationLog(ticketId, status));
}
}
@Transactional 默认只回滚 RuntimeException 和 Error,不回滚受检异常(checked exception)。 throw new Exception() 是受检异常,Spring 默认不会回滚。
这是最坑的场景。代码看起来没问题,异常也抛了,但事务就是不回滚。而且不会报错,只在数据层面出问题,排查极难。
解决:
java
// 方案 1:指定 rollbackFor
@Transactional(rollbackFor = Exception.class) // ✅ 所有异常都回滚
public void updateTicketStatus(Long ticketId, String status) throws Exception {
// ...
}
// 方案 2:抛 RuntimeException
@Transactional
public void updateTicketStatus(Long ticketId, String status) {
ticketMapper.updateStatus(ticketId, status);
if (!status.equals("VALID")) {
throw new RuntimeException("状态不合法"); // ✅ RuntimeException 会回滚
}
// ...
}
建议养成习惯:所有 @Transactional 都加
rollbackFor = Exception.class。宁可多回滚,不可漏回滚。
场景 5:事务传播行为不对
java
@Service
public class TicketService {
@Autowired
private LogService logService;
@Transactional
public void updateTicketStatus(Long ticketId, String status) {
ticketMapper.updateStatus(ticketId, status);
logService.writeLog(ticketId, status); // REQUIRES_NEW,独立事务提交
throw new RuntimeException("更新失败"); // 外层事务回滚
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // 新开事务
public void writeLog(Long ticketId, String status) {
logMapper.insert(new OperationLog(ticketId, status));
}
}
LogService.writeLog 用了 REQUIRES_NEW,会挂起当前事务,新开一个独立事务。writeLog 执行完,内层事务就提交了。随后外层抛异常回滚,但内层事务已经提交了,不会跟着回滚。工单状态更新被回滚,操作日志却留下来了。
如果这里的日志不是"必须留痕"而是"必须跟主业务一致",就会出现数据不一致。这就是 REQUIRES_NEW 的陷阱:内外层事务独立,一个回滚不影响另一个。
反过来也一样:如果内层事务抛异常回滚,异常会传播到外层,外层如果不 catch 也会回滚。别以为用了 REQUIRES_NEW 就互不影响了。
解决: 理解传播行为,按业务需要选择。
| 传播行为 | 行为 | 什么时候用 |
|---|---|---|
| REQUIRED(默认) | 有事务就加入,没有就新建 | 99% 的场景 |
| REQUIRES_NEW | 挂起当前事务,新建独立事务 | 日志记录(即使主事务回滚日志也要留) |
| NESTED | 嵌套事务(基于 savepoint) | 部分回滚场景 |
| SUPPORTS | 有事务就加入,没有就非事务执行 | 查询方法 |
| NOT_SUPPORTED | 非事务执行,挂起当前事务 | 不需要事务的操作 |
| MANDATORY | 必须在事务中,否则报错 | 强制要求调用方有事务 |
| NEVER | 非事务执行,有事务则报错 | 不允许在事务中执行 |
场景 6:数据库引擎不支持事务
sql
-- 建表时用了 MyISAM 引擎
CREATE TABLE ticket (
id BIGINT PRIMARY KEY,
status VARCHAR(20)
) ENGINE = MyISAM; -- ❌ MyISAM 不支持事务
MyISAM 引擎不支持事务,InnoDB 才支持。即使代码层面 @Transactional 配置正确,数据库引擎不支持,事务也不生效。
解决:
sql
-- 改成 InnoDB
ALTER TABLE ticket ENGINE = InnoDB;
MySQL 5.5+ 默认引擎已经是 InnoDB,但老项目或手动建表可能还是 MyISAM。检查一下:
SHOW TABLE STATUS FROM your_db;
场景 7:Bean 没有被 Spring 管理
java
// 没有 @Service 注解,Spring 不会管理这个 Bean
public class TicketService {
@Transactional // 没用,Spring 根本不知道这个类
public void updateTicketStatus(Long ticketId, String status) {
// ...
}
}
// 手动 new 出来的也不行
TicketService service = new TicketService();
service.updateTicketStatus(1L, "VALID"); // @Transactional 失效
@Transactional 依赖 Spring 容器创建代理对象。如果类没有被 Spring 管理(没加 @Service/@Component),或者手动 new 的,Spring 没机会生成代理。
解决: 加上 @Service 或 @Component,通过依赖注入使用。
四、一张图总结
java
@Transactional 生效的前提:
│
├─ 1. 方法是 public ──→ 非 public 不代理
│
├─ 2. 通过代理对象调用 ──→ 自调用 / 手动 new 失效
│
├─ 3. 异常抛到代理层 ──→ catch 吞异常失效
│
├─ 4. 异常类型匹配 ──→ 受检异常默认不回滚(加 rollbackFor)
│
├─ 5. 传播行为正确 ──→ REQUIRES_NEW 会独立提交/回滚
│
├─ 6. 数据库支持事务 ──→ MyISAM 不支持
│
└─ 7. Bean 被 Spring 管理 ──→ 没注解 / 手动 new 失效
五、CheckList:@Transactional 上线前排查
| # | 检查项 | 风险点 | 正确做法 |
|---|---|---|---|
| 1 | 自调用 | this 调用绕过代理 | 拆到另一个 Service 或注入代理 |
| 2 | 方法非 public | AOP 不代理非 public | 改成 public |
| 3 | 异常被 catch | Spring 感知不到异常 | catch 后重新 throw |
| 4 | 抛受检异常 | 默认不回滚 | 加 rollbackFor = Exception.class |
| 5 | 传播行为 | REQUIRES_NEW 独立事务 | 按业务需要选择传播行为 |
| 6 | 数据库引擎 | MyISAM 不支持事务 | 用 InnoDB |
| 7 | Bean 未被管理 | 没有代理对象 | 加 @Service,通过注入使用 |
六、总结
7 种失效场景,记住核心原理:
@Transactional 依赖 Spring AOP 代理。凡是不经过代理的调用、代理拦截不到的异常、不匹配的回滚条件,都会让事务失效。
养成三个习惯:
- 所有 @Transactional 加
rollbackFor = Exception.class- 事务方法不要自调用,拆到不同的 Service
- 异常别吞,catch 了就 throw 出去
附录:本地复现完整代码
java
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class TransactionalFailApp {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(TransactionalFailApp.class, args);
TicketService service = ctx.getBean(TicketService.class);
// 测试 1:自调用失效
System.out.println("\n=== 场景1:自调用 ===");
try {
service.testSelfInvoke(1L, "CLOSED");
} catch (Exception e) {
System.out.println("异常: " + e.getMessage());
}
System.out.println("工单状态: " + service.getTicketStatus(1L));
// 如果事务生效,状态不应该被更新
// 如果事务失效,状态被更新了(因为自调用绕过了代理)
// 测试 2:异常被 catch
System.out.println("\n=== 场景3:异常被catch ===");
try {
service.testCatchException(1L, "CLOSED");
} catch (Exception e) {
System.out.println("异常: " + e.getMessage());
}
System.out.println("工单状态: " + service.getTicketStatus(1L));
// 异常被吞,事务不回滚,状态被更新
// 测试 3:受检异常不回滚
System.out.println("\n=== 场景4:受检异常 ===");
try {
service.testCheckedException(1L, "CLOSED");
} catch (Exception e) {
System.out.println("异常: " + e.getMessage());
}
System.out.println("工单状态: " + service.getTicketStatus(1L));
// 受检异常默认不回滚,状态被更新
// 测试 4:加 rollbackFor 后回滚
System.out.println("\n=== 场景4修复:rollbackFor ===");
try {
service.testCheckedExceptionFixed(1L, "CLOSED");
} catch (Exception e) {
System.out.println("异常: " + e.getMessage());
}
System.out.println("工单状态: " + service.getTicketStatus(1L));
// 加了 rollbackFor,事务回滚,状态没变
ctx.close();
}
}
@Service
class TicketService {
@Autowired
private TicketMapper ticketMapper;
@Autowired
private LogMapper logMapper;
// 场景 1:自调用
@Transactional
public void updateTicketStatus(Long ticketId, String status) {
ticketMapper.updateStatus(ticketId, status);
logMapper.insert(new OperationLog(ticketId, status));
}
public void testSelfInvoke(Long ticketId, String status) {
updateTicketStatus(ticketId, status); // 自调用,事务失效
}
// 场景 3:异常被 catch
@Transactional
public void testCatchException(Long ticketId, String status) {
ticketMapper.updateStatus(ticketId, status);
try {
throw new RuntimeException("故意抛异常");
} catch (Exception e) {
System.out.println("异常被吞了: " + e.getMessage());
// 没有 throw,事务不回滚
}
}
// 场景 4:受检异常,默认不回滚
@Transactional
public void testCheckedException(Long ticketId, String status) throws Exception {
ticketMapper.updateStatus(ticketId, status);
throw new Exception("受检异常,默认不回滚");
}
// 场景 4 修复:加 rollbackFor
@Transactional(rollbackFor = Exception.class)
public void testCheckedExceptionFixed(Long ticketId, String status) throws Exception {
ticketMapper.updateStatus(ticketId, status);
throw new Exception("受检异常,加了 rollbackFor 会回滚");
}
public String getTicketStatus(Long ticketId) {
return ticketMapper.selectStatus(ticketId);
}
}
运行前需要配置数据库(MySQL + InnoDB 引擎)和对应的 Mapper。
复现要点:
- 自调用:
testSelfInvoke调this.updateTicketStatus,事务失效,工单状态被更新- 异常被 catch:事务不回滚,工单状态被更新
- 受检异常:默认不回滚,工单状态被更新
- 加 rollbackFor:事务回滚,工单状态没变
对比场景 3 和场景 4 的结果差异,理解"异常感知"对事务回滚的影响。