@Transactional 注了等于没用?Spring 事务失效的 7 种场景,你踩过几个

@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 不会报错,但事务根本不生效------COMMITROLLBACK 都是空操作。

验证方式

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.factoriesAutoConfiguration.imports 有什么区别?@Conditional 系列注解怎么控制 Bean 加载?------从源码层面把自动配置的骨架拆清楚。


本文是 Java 技术系列第 2 篇,系列目录:

  1. Spring 循环依赖到底怎么解的?三级缓存源码拆解
  2. 本文:@Transactional 注了等于没用?Spring 事务失效的 7 种场景
相关推荐
大模型码小白1 小时前
企业级检索增强后端集成:Java 服务如何管理知识库版本
java·服务器·开发语言·人工智能·python·microsoft
AI多Agent协作实战派2 小时前
AI多Agent协作系统实战(二十二):从6列到12列——任务监控报告的进化之路
java·人工智能·uni-app·bug
千桐科技2 小时前
DataX 执行引擎正式加入:qData 开源版 v1.6.0 新增 Quartz + DataX 轻量运行模式!
java·大数据·开源·数据治理·数据中台·qdata
Irene19912 小时前
Oracle PL/SQL Developer 版本差异(11g、14)实战总结
java·开发语言
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第191题】【08_Kafka篇】第7题:消息队列的优缺点
java·消息队列·系统设计·分布式架构·技术选型
用户446139430272 小时前
Spring Boot 接入大模型:从配置到实战的完整指南
java
青石路2 小时前
如果写Redis序列化与读Redis序列化不一致,你觉得会发生什么
java·redis
煎饼学大模型2 小时前
架构决定上限:Skill 知识架构的三次重构实践
java·重构·架构·skill
llwszx2 小时前
【Java/Go后端手撸原生Agent(第五篇):多工具并行调用 + BashTool执行引擎 + Judge证据链升级】
java·后端·golang·状态机·pydantic·agnet·llm-as-judge