@Transactional 又失效了?把这 8 个坑全填了!

别再背面试八股了,这 8 种让事务原地爆炸的骚操作,你都亲手写过几种?

前言:事务失效的本质是"代理失联"

很多朋友找 Bug 找了半天,最后发现 @Transactional quietly 地挂掉了。其实 Spring 声明式事务的本质是 AOP 动态代理

我们实际调用的 Service 方法是经过 CGLIB 或 JDK 增强过的代理对象,代理对象帮我们开启、提交或回滚事务。一旦绕过了代理对象,事务就形同虚设。

今天我们就结合真实的业务场景,盘点 Spring 事务失效的 8 种核心场景 。针对大家反馈最多的 内部调用try-catch异常,我会给出最深度的解毒方案。


第一大坑:try-catch 吃了异常,事务一脸懵

这是很多新手(甚至老手)最容易犯的错,也是生产环境脏数据的头号元凶。

错误示范

java 复制代码
@Transactional
public void createOrder(OrderDto dto) {
    try {
        orderDao.insert(dto);          // 插入订单成功
        inventoryDao.deduct(dto.getSkuId()); // 扣库存时报错抛异常
    } catch (Exception e) {
        log.error("扣库存失败了,但我不能让用户看到报错", e);
        // 异常被吞了,什么也没做
    }
}

底层原理剖析

Spring 事务回滚的逻辑大致是这样的:

java 复制代码
// 代理逻辑伪代码
try {
    // 执行业务方法(即你的 createOrder)
    method.invoke(target, args);
    // 如果没抛异常,提交事务
    commit();
} catch (Exception e) {
    // 捕获到异常,执行回滚
    rollback();
}

当你在业务代码里把异常 catch 住并且不再抛出 ,对于 Spring 的代理拦截器来说,业务方法正常执行结束了,没抛异常,那自然是 "正常提交" 。库存扣失败了,但订单却提交了,这就是经典的数据不一致

解毒方案(重点)

方案一:手动强制回滚(最优雅,无需上层感知)

如果你的业务逻辑要求"即使扣库存失败,订单也要生成成功,但库存要回滚",那就必须手动标记回滚状态:

java 复制代码
@Transactional
public void createOrder(OrderDto dto) {
    try {
        orderDao.insert(dto);
        inventoryDao.deduct(dto.getSkuId());
    } catch (Exception e) {
        log.error("扣库存异常,事务手动回滚", e);
        // 核心!手动将当前事务标记为只回滚,Spring 会在提交时 abort
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

方案二:直接抛出(让代理感知)

java 复制代码
@Transactional
public void createOrder(OrderDto dto) {
    try {
        orderDao.insert(dto);
        inventoryDao.deduct(dto.getSkuId());
    } catch (Exception e) {
        log.error("扣库存异常", e);
        throw new RuntimeException(e); // 重新抛出运行时异常
    }
}

第二大坑:同类内部方法自调用(this 的致命陷阱)

如果你在面试中被问到事务失效,90% 会问这个。这也是掘金上争论最多的话题。

错误示范

java 复制代码
@Service
public class OrderService {
    public void placeOrder() {
        // 直接调用,没走代理! 
        this.createOrder(); 
    }
    
    @Transactional
    public void createOrder() {
        // 插入订单逻辑
    }
}

外部调用 orderService.placeOrder(),你会发现 createOrder 上的事务完全没反应

根源分析:代理对象 vs 原生对象

Spring 容器里放的是 被代理增强过的 OrderService$$CGLIB 对象。当外部调用 orderService.placeOrder() 时,走的是代理对象。

但进入 placeOrder() 方法内部后,this 指向的是当前的原生对象 (Target Object),而不是代理对象。因此调用 this.createOrder() 时,直接调用了原生方法,AOP 拦截器根本没机会执行,事务自然不存在。

如何验证是否走代理?

java 复制代码
@Transactional
public void createOrder() {
    // 如果输出包含 $$EnhancerBySpringCGLIB,说明走代理了
    // 如果只是 com.example.OrderService,说明是原生对象,事务失效
    System.out.println("当前类: " + this.getClass().getName());
}

解毒方案(深度剖析)

方案一:拆分类(最符合单一职责)

把事务方法抽离到另一个 Service 中。

java 复制代码
@Service
public class OrderService {
    @Autowired
    private InventoryService inventoryService; // 独立代理类
    
    public void placeOrder() {
        inventoryService.deductInventory(); // 走代理,事务生效
    }
}

方案二:自注入(Spring 允许,最优雅的"自救")

利用 Spring 解决循环依赖的特性,注入自己。

java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderService selfProxy; // 注入自己的代理对象
    
    public void placeOrder() {
        // 必须通过代理对象调用!
        selfProxy.createOrder(); 
    }
    
    @Transactional
    public void createOrder() { ... }
}

方案三:AopContext 强制暴露代理

在启动类开启 exposeProxy = true

java 复制代码
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class Application { ... }

调用时:

java 复制代码
public void placeOrder() {
    ((OrderService) AopContext.currentProxy()).createOrder();
}

第三大坑:方法修饰符不是 public

这是一个硬性规定,没什么商量的余地。

错误示范

java 复制代码
@Transactional
private void updatePrice() { ... } // 失效

@Transactional
protected void updatePrice() { ... } // 失效

@Transactional
void updatePrice() { ... } // 默认包可见,失效

源码实锤

在 Spring 源码 AbstractFallbackTransactionAttributeSource 中:

java 复制代码
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
    return null; // 直接不解析事务注解
}

Spring 官方认为事务应该只作用于公共接口方法。如果实在要在非 public 方法上用,需要开启 AspectJ 编织模式(mode=AspectJ),但非常不推荐,平白增加复杂度。

结论所有 @Transactional 必须标注在 public 方法上。


第四大坑:传播行为(Propagation)瞎改

很多朋友觉得默认的 REQUIRED 不够高级,非要改成 REQUIRES_NEWNESTED,结果事务莫名其妙不生效或死锁。

典型失效场景 1:SUPPORTS 和 NOT_SUPPORTED

java 复制代码
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void updateCache() {
    // 以为有事务,实际上 Spring 会挂起任何当前事务,根本不开启!
}

典型失效场景 2:REQUIRES_NEW 的"假失效"

你以为内外两个方法都有事务?看代码:

java 复制代码
@Transactional
public void outer() {
    inner(); // 调用内部 REQUIRES_NEW 方法
    int i = 1/0; // 这里抛异常
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() { ... }

真相outer 抛异常,inner 依然会被回滚 !因为这里又是内部调用this.inner()),走的还是原生对象,REQUIRES_NEW 根本没机会创建新事务,内外共用一个连接,一荣俱荣,一损俱损。

解毒方案

  • 除非你非常清楚嵌套事务的回滚点(Savepoint)机制,否则无脑使用 @Transactional(propagation = Propagation.REQUIRED)(默认)
  • 如果真的需要 REQUIRES_NEW,请配合 "自注入""拆分类" 使用,确保新事务走代理对象。

第五大坑:数据库引擎不支持事务

这个问题在接手老项目时极其常见,排查起来能让人怀疑人生。

错误示范

项目用 MySQL,建表语句是:

sql 复制代码
CREATE TABLE `order` (
  `id` bigint(20) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

原因分析

@Transactional 底层本质是 JDBC 的 conn.setAutoCommit(false) + conn.commit()。但 MyISAM 存储引擎压根不支持事务 ,无论你怎么发 commitrollback 指令,它都会立即落盘,无法撤销。

解决方案

查看并修改表引擎:

sql 复制代码
-- 查看表引擎
SHOW TABLE STATUS LIKE 'order';

-- 修改为 InnoDB
ALTER TABLE `order` ENGINE = InnoDB;

注意:MySQL 5.5.5 之后默认引擎就是 InnoDB,但如果是 DBA 手动建的老表,或者迁移过来的数据,千万要检查一下。


第六大坑:多数据源未指定事务管理器(分布式/分库场景必看)

现在微服务或读写分离项目非常普遍,如果你有多个数据源(主库、从库、业务库A、业务库B),只写一个 @Transactional 会出大事。

错误示范

java 复制代码
@Configuration
public class DataSourceConfig {
    @Bean(name = "orderDataSource") ... // 订单库
    @Bean(name = "logDataSource") ...   // 日志库
}

@Service
public class LogService {
    @Transactional // 默认去找 Primary 数据源的事务管理器
    public void saveLog() {
        // 实际操作的是 logDataSource,但事务管理器是 orderDataSource 的!
        // 连接不匹配,事务失效或直接报错
        logDao.insert();
    }
}

原因分析

Spring 事务管理器的核心是 PlatformTransactionManager,它绑定特定的 DataSource@Transactional 默认会取容器中类型为 PlatformTransactionManager 且标注了 @Primary 的那个。如果你操作的是另一个数据源,该数据源压根没被这个管理器托管,事务自然不生效。

解毒方案

必须在注解中显式指定事务管理器的 Bean 名称:

java 复制代码
@Transactional(transactionManager = "logTransactionManager")
public void saveLog() {
    logDao.insert();
}

第七大坑:未被 Spring 容器管理 / 未启用事务

这属于基本盘没打牢,但往往容易被忽略。

场景一:类没加 @Service

java 复制代码
// 忘记加 @Service 或 @Component
public class OrderService {
    @Transactional
    public void save() { ... }
}
// 这个类都不在 Spring 容器里,谁来给你代理?

场景二:传统 Spring XML 项目没开启注解驱动

Spring Boot 自动配置了 @EnableTransactionManagement,但如果你的老项目是 Spring MVC + XML 配置,必须显式开启:

xml

ini 复制代码
<tx:annotation-driven transaction-manager="transactionManager" />

或者在配置类上加:

java 复制代码
@Configuration
@EnableTransactionManagement
public class AppConfig { ... }

第八大坑(番外篇):多线程环境下事务飞了

极少数人会遇到,但遇到了就是大麻烦。

错误示范

java 复制代码
@Transactional
public void batchSave() {
    new Thread(() -> {
        // 子线程执行插入
        orderDao.insert(order);
    }).start();
}

原因分析

Spring 事务管理依赖 ThreadLocal 存储当前线程的数据库连接(ConnectionHolder)。事务是绑定在主线程 上的,子线程运行时去获取连接,根本拿不到主线程的事务上下文,所以子线程里的操作是独立于事务之外的,无法回滚。

解决方案

  • 不要在事务方法中手动 new Thread()
  • 如果是异步处理,确保使用 @Async 并配好独立的事务传播机制,或者干脆将子线程操作封装成单独的服务,让主线程等待结果。

终极总结:一张速查表搞定所有 Bug

坑位 失效场景 一句话核心原因 一键解毒
1 try-catch 吞异常 代理捕获不到异常,认为执行成功 手动 setRollbackOnly 或重新抛出
2 同类内部自调用 this 绕过代理,AOP 切面没执行 自注入 selfProxy 或拆分类
3 方法非 public Spring 硬编码只拦截 public 强制改成 public
4 传播行为瞎改 REQUIRES_NEW 遇内部调用失效 / SUPPORTS 不开启事务 非资深玩家只用 REQUIRED
5 数据库引擎不支持 MyISAM 不支持事务 DML 回滚 改为 ENGINE=InnoDB
6 多数据源未指定管理器 事务管理器管不了另一个数据源 指定 transactionManager 名称
7 未被容器管理/未开启 没有代理对象或解析器 @Service + @EnableTransactionManagement
8 多线程调用 ThreadLocal 线程隔离,子线程拿不到连接 避免事务内手动 new Thread

结语

@Transactional 用好了是神兵利器,用错了是埋雷高手。记住最核心的一条法则:永远确保你的业务代码是通过 Spring 代理对象进入事务方法的。 下次再遇到事务不回滚,把你 IDE 的断点打在 TransactionInterceptor 上,看看有没有进去;再看一眼控制台打印的类名是不是带 $$CGLIB,这两个动作能解决你 90% 的困惑。

你还在哪些奇葩场景下遇到过事务失效?欢迎评论区补充讨论!

相关推荐
颜进强1 小时前
从零搭建私人 RAG 知识库:让项目决策真正“可检索、可追溯”
前端·后端·ai编程
颜进强1 小时前
Embedding 模型介绍:热门模型对比与应用场景
前端·后端·ai编程
颜进强1 小时前
LanceDB 基础使用:用 TypeScript 完成第一次向量检索
前端·后端·ai编程
武子康1 小时前
低延迟不是更快地猜:EOU / Barge-in / Turn Protocol 必须统一(4 种结束 + Generation Fencing + 9 类可复现场景)
人工智能·后端·llm
颜进强1 小时前
Ollama 从入门到实践:本地模型运行、API 调用
前端·后端·ai编程
今天AI了吗1 小时前
Spring AI 框架实战:Java 后端集成大模型的架构设计与工程落地
java·人工智能·python·spring·机器学习
颜进强1 小时前
Embedding 基础使用:用 Ollama 和 LangChain.js 生成文本向量
前端·后端·ai编程
颜进强1 小时前
Vector Store 入门:什么是向量数据库,主流产品如何选择
前端·后端·ai编程
hexu_blog2 小时前
springboot3集成shardingsphere4.0 分表分库
java·spring boot·mybatis