Spring 事务提交顺序深度解析:从踩坑到理解原理

Spring 事务提交顺序深度解析:从踩坑到理解原理

本文基于真实开发场景,结合 Spring Boot + MySQL + MyBatis-Plus 技术栈,深入讲解 @Transactional 的事务提交顺序、子服务事务传播、以及一个让很多人困惑的"日志打了但数据没提交"的经典问题。


一、没有 @Transactional 时,提交是什么顺序?

先看最基础的情况,假设 Service 方法上没有任何事务注解

java 复制代码
public boolean addRewardOrder(AddOrderDTO addOrderDTO) {
    // ...
    paymentOnAutomService.save(paymentOnAutom); // 第1步
    save(aOrder);                               // 第2步
    paymentQuzrtzService.save(paymentQuartz);   // 第3步
}

每一句 save 就是一次独立提交,顺序如下:

复制代码
① paymentOnAutomService.save()  ──→ 立即提交到数据库(循环几次提交几次)
② save(aOrder)                  ──→ 立即提交到数据库
③ paymentQuzrtzService.save()   ──→ 立即提交到数据库(循环几次提交几次)

代码从上往下执行,遇到一个 save 就立刻提交一次,不存在"等到最后一起提交"的概念

这种方式的风险极大:

  • save(paymentOnAutom) 存了几条记录,然后 save(aOrder) 失败 → 付款记录存在,但订单不存在,数据孤儿
  • save(aOrder) 成功,但 save(paymentQuartz) 中途失败 → 支付队列不完整

二、加上 @Transactional 之后,提交顺序是什么?

写在类上和写在方法上效果一样

java 复制代码
@Slf4j
@Service
@Transactional  // 写在类上,等价于所有 public 方法都加了 @Transactional
public class OrderServiceImpl extends ServiceImpl<...> implements OrderService {
}

加上事务之后,只有一次提交,不再有顺序的概念:

复制代码
方法开始 ──→ BEGIN(开启事务)

  ① paymentOnAutomService.save() × N   ← SQL已发送,未提交
  ② save(aOrder)                        ← SQL已发送,未提交
  ③ paymentQuzrtzService.save() × N    ← SQL已发送,未提交

方法正常返回 ──→ COMMIT(全部一次性写入数据库)
任意步骤异常 ──→ ROLLBACK(全部回滚,数据库无任何变化)

要么全部成功,要么全部回滚,不存在"谁先提交谁后提交"的说法。

注意:默认不回滚 Checked Exception

类上的 @Transactional 默认等价于:

java 复制代码
@Transactional(rollbackFor = {RuntimeException.class, Error.class})

如果抛出的是 IOException、自定义 Exception 等 checked exception,不会触发回滚。建议统一改成:

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

三、经典疑问:日志打印出来了,但数据没提交?

这是很多开发者遇到过的困惑,来看这段代码:

java 复制代码
if (save) {
    for (APaymentOnAutom paymentOnAutom : paymentOnAutomList) {
        log.info("奖励支付订单添加到支付队列!{}", paymentOnAutom); // 日志立即打印
        paymentQuzrtzService.save(PaymentQuartz.builder()
                .paymentId(paymentOnAutom.getId())
                .build());
    }
}

log.info 和数据库提交是两回事。

  • log.info 直接输出到控制台,不受事务控制,立即执行
  • save 的 SQL 已经发送给数据库连接,但处于未提交状态

所以你看到日志,不代表数据已经写入数据库。数据要等到整个方法正常返回后才会 COMMIT

如果你看到了日志,但数据库里没有数据,通常是方法后续抛出了异常触发了回滚:

复制代码
log 打印了 ──→ save 发送了 SQL ──→ 后续某处抛异常 ──→ ROLLBACK ──→ 数据消失

四、子服务没有 @Transactional,save() 为什么不能加入当前事务?

这是一个非常底层但重要的知识点。

问题现象

java 复制代码
// 调用子服务的 save,数据没有加入当前事务
paymentQuzrtzService.save(paymentQuartz);  // ❌ 独立提交,不受当前事务控制

// 改用 getBaseMapper().insert(),反而能加入当前事务
paymentQuzrtzService.getBaseMapper().insert(paymentQuartz);  // ✅ 加入当前事务

根本原因

Spring 事务是通过 AOP 代理 实现的。MyBatis-Plus 的 IService.save() 内部实现是这样的:

java 复制代码
default boolean save(T entity) {
    return SqlHelper.retBool(getBaseMapper().insert(entity));
}

PaymentQuzrtzServiceImpl 没有加 @Transactional 时,这个 save() 方法不经过 Spring 的事务代理,它会新开一个数据库连接,用自己的连接独立提交,和你当前事务的连接不是同一个。

getBaseMapper().insert() 走的是 MyBatis 的 Mapper 层,MyBatis 在执行 SQL 前会检查当前线程是否绑定了事务连接,有的话直接复用,所以它自动加入了你的事务。

三种情况对比

调用方式 子服务有无 @Transactional 是否加入当前事务
service.save() ✅ 有 ✅ 加入,一起提交
service.save() ❌ 无 ❌ 独立提交,无法回滚
getBaseMapper().insert() 无所谓 ✅ 自动加入当前事务

正确解决方案

不推荐用 getBaseMapper().insert() 绕过去,正确做法是给子服务加上 @Transactional

java 复制代码
@Service
@Transactional(rollbackFor = Exception.class)  // ✅ 加上这个
public class PaymentQuzrtzServiceImpl extends ServiceImpl<PaymentQuartzMapper, PaymentQuartz>
        implements PaymentQuzrtzService {
}

加上之后,两个 service 的事务传播级别都是默认的 PROPAGATION_REQUIRED,含义是:

当前已有事务 → 加入这个事务(同一个连接,同一次提交)

这样所有 save 调用都在同一个事务里,方法正常返回时统一提交。


五、总结

场景 提交方式 风险
@Transactional 每个 save 立即独立提交 数据不一致,无法回滚
@Transactional,子服务也有 方法返回时统一一次提交 ✅ 安全
@Transactional,子服务没有 子服务独立提交,主服务回滚时子服务数据无法还原 ⚠️ 部分数据不一致
@Transactional,抛 checked exception 默认不回滚 ⚠️ 需配置 rollbackFor

一句话记住核心:Spring 事务的提交发生在方法正常返回的那一刻,日志打印和数据库提交无关,子服务必须也受事务管理才能保证原子性。


技术栈 :Spring Boot · MySQL · MyBatis-Plus
关键注解@Transactional(rollbackFor = Exception.class)
传播级别PROPAGATION_REQUIRED(默认)

相关推荐
xcjbqd03 小时前
Python中Pandas如何将DataFrame写入MySQL_使用to_sql函数
jvm·数据库·python
ZOOOOOOU3 小时前
智慧社区云对讲门禁系统架构设计:中优云联免布线、全免费核心功能技术解析
数据库·人工智能·架构·边缘计算
yzp-3 小时前
Spring 三级缓存 ---- 简单明了豆包版
java·mysql·spring
Francek Chen3 小时前
【大数据存储与管理】NoSQL数据库:02 NoSQL兴起的原因
大数据·数据库·分布式·nosql
张涛酱1074563 小时前
Agent Skills 深入解析:构建可插拔的智能体知识体系
spring·设计模式·ai编程
斌味代码3 小时前
RAG API 接入:从注册到生产级应用的10分钟上手指南
数据库·oracle
送秋三十五3 小时前
Spring 源码---------Spring Core
java·数据库·spring
Cat_Rocky3 小时前
redis数据库基础学习
数据库·redis·学习
正在走向自律3 小时前
多源异构数据融合技术实践:GIS、时序、文档与缓存数据整合方案
数据库