Spring 事务失效的 8 种场景,很多老手依然频繁踩雷

Spring 事务失效的 8 种场景,很多老手依然频繁踩雷

Spring 事务,是面试高频,也是线上事故的"重灾区"。

很多同学觉得:

加个 @Transactional 就万事大吉了。

但在真实项目中,事务不回滚、部分回滚、甚至根本没开启,每天都在发生。

本文盘点 Spring 事务失效的 8 大经典场景,从原理 → 反例 → 正解 → 源码级原因,一次性讲透。


先给结论(速记版)

Spring 事务的本质是 AOP + 代理

只要代理没生效,或异常被"吃掉",事务一定会翻车


一、方法不是 public:最基础却最高频

❌ 错误示例

typescript 复制代码
@Transactional
protected void createOrder() {
    orderMapper.insert(order);
    throw new RuntimeException();
}

✅ 原因

Spring 的事务 AOP 默认使用 JDK 动态代理 / CGLIB

  • JDK 动态代理 → 只能代理接口方法
  • CGLIB → 只能拦截 public 方法

@Transactional 注解在 protected / private / package 方法上:

不会被代理,事务直接失效

✅ 正确写法

typescript 复制代码
@Transactional
public void createOrder() {
    ...
}

📌 经验法则

事务方法一律写成 public


二、自调用(this 调用):老手最容易栽的坑

❌ 错误示例

typescript 复制代码
@Service
public class OrderService {

    public void create() {
        save();
    }

    @Transactional
    public void save() {
        orderMapper.insert(order);
        throw new RuntimeException();
    }
}

✅ 原因

create() 内部调用 save()

kotlin 复制代码
this.save(); // 调用的是目标对象本身

而 Spring 事务是通过 代理对象​ 实现的:

kotlin 复制代码
外部调用 → 代理 → 目标方法
this调用 → 目标方法(绕过代理)

👉 事务完全失效

✅ 解决方案

方案 1:拆分 Service(推荐)
less 复制代码
@Service
public class OrderTxService {
    @Transactional
    public void save() { ... }
}

@Service
public class OrderService {
    @Autowired
    private OrderTxService txService;

    public void create() {
        txService.save();
    }
}
方案 2:注入自己(不优雅但可用)
ruby 复制代码
@Autowired
private OrderService self;

self.save();
方案 3:AspectJ 模式(高级)
ini 复制代码
spring.aop.proxy-target-class=true

三、异常类型不对:RuntimeException 与 Exception 的坑

❌ 错误示例

java 复制代码
@Transactional
public void create() throws Exception {
    orderMapper.insert(order);
    throw new Exception();
}

✅ 原因

@Transactional 默认规则:

异常类型 是否回滚
RuntimeException & Error
Checked Exception

Exception 属于 受检异常,默认不回滚。

✅ 正确写法

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void create() throws Exception {
    ...
}

📌 强烈建议:

永远显式声明 rollbackFor

python 复制代码
@Transactional(rollbackFor = Exception.class)

四、异常被吞掉:最隐蔽的"假成功"

❌ 错误示例

csharp 复制代码
@Transactional(rollbackFor = Exception.class)
public void create() {
    try {
        orderMapper.insert(order);
        int i = 1 / 0;
    } catch (Exception e) {
        log.error("error", e);
        // ⚠️ 吞掉异常
    }
}

✅ 原因

Spring 事务回滚的前提是:

异常抛出到代理层

你在方法内部把异常吃了,代理完全不知道发生了异常。

✅ 正确写法

php 复制代码
catch (Exception e) {
    log.error("error", e);
    throw e; // 或 new RuntimeException(e)
}

📌 记住一句话:

事务回滚靠"抛异常",不是靠"打日志"


五、传播行为配置错误:REQUIRES_NEW 的误用

❌ 错误示例

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log() {
    logMapper.insert(log);
}

外层事务回滚,日志却提交了。

✅ 原因

REQUIRES_NEW 的含义:

挂起外层事务,新建一个独立事务

  • 内外事务完全隔离
  • 外层回滚不影响内层

✅ 正确使用场景

✅ 审计日志

✅ 消息发送

✅ 不希望被主事务影响的操作

❌ 错误使用场景

❌ 核心业务数据

❌ 需要一起回滚的操作

📌 90% 的业务场景只需要:

ini 复制代码
@Transactional(propagation = Propagation.REQUIRED)

六、数据库引擎不支持事务

❌ 典型场景

MySQL 使用 MyISAM

ini 复制代码
CREATE TABLE order (
  ...
) ENGINE=MyISAM;

✅ 原因

  • MyISAM:不支持事务
  • InnoDB:支持事务

Spring 再怎么努力也没用。

✅ 检查方式

ini 复制代码
SHOW TABLE STATUS WHERE Name = 'order';

✅ 正确做法

ini 复制代码
ENGINE=InnoDB;

📌 现在还有人用 MyISAM,基本可以判定为事故级问题


七、非事务方法调用事务方法 + 多线程

❌ 错误示例

scss 复制代码
@Transactional
public void create() {
    new Thread(() -> {
        orderMapper.insert(order);
    }).start();
}

✅ 原因

Spring 事务是基于 ThreadLocal​ 绑定的:

复制代码
一个线程 = 一个数据库连接 = 一个事务

新线程:

  • 没有事务上下文
  • 没有数据库连接绑定

👉 子线程操作不在事务中

✅ 正确做法

  • 不要在事务方法中开线程
  • 使用异步 + 新事务
less 复制代码
@Async
@Transactional
public void asyncCreate() { ... }

八、没有被 Spring 管理:忘了 @Service / @Component

❌ 错误示例

typescript 复制代码
// @Service 忘了写
public class OrderService {

    @Transactional
    public void create() {
        orderMapper.insert(order);
    }
}

✅ 原因

Spring 只对 Bean​ 创建代理。

不是 Bean → 没有代理 → 没有事务。

✅ 正确写法

kotlin 复制代码
@Service
public class OrderService { ... }

📌 排查技巧:

  • 看启动日志有没有 Bean 注册
  • Debug 看 this 是不是代理对象

九、Spring 事务失效全景对照表(建议收藏)

场景 是否失效 原因
非 public 方法 AOP 限制
this 自调用 绕过代理
吞掉异常 未抛到代理
非 RuntimeException rollbackFor
REQUIRES_NEW 误用 ⚠️ 设计问题
MyISAM 引擎不支持
多线程 ThreadLocal
非 Spring Bean 无代理

十、如何快速定位事务问题?

✅ 1️⃣ 打印代理对象

less 复制代码
log.info("class={}", AopContext.currentProxy().getClass());

✅ 2️⃣ 开启事务日志

ini 复制代码
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG

✅ 3️⃣ Debug 断点

  • TransactionInterceptor
  • DataSourceTransactionManager

十一、一句话总结

Spring 事务不是魔法,而是代理。

  • 代理没生效 → 事务失效
  • 异常没抛出 → 不会回滚
  • 多线程 / 自调用 → 上下文丢失

真正理解这三点,事务问题至少少踩 80% 的坑。

相关推荐
神经蛙199615 小时前
🌍 别再硬编码中文了!Python Web 项目国际化(i18n)完全指南
后端·python
掘金酱15 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
长大198815 小时前
MyBatis 常见性能陷阱:N+1 查询、一级缓存踩坑解决方案
后端
用户18615580086016 小时前
MinIO Java 对接试用:从连接、上传到下载的完整示例
后端
爱勇宝16 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
极客悟道16 小时前
SDKMAN vs jEnv vs JetTUI,JDK 版本管理到底选哪个
后端
长大198816 小时前
Java8 新特性到底要不要吃透?工作中高频使用的 5 个功能总结
后端
二月龙16 小时前
Spring Bean 生命周期 & 循环依赖:90% 开发者只知结论不懂原理
后端