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(默认)

相关推荐
kirs_ur4 小时前
ECC & LDPC — SSD 的数据卫士
服务器·数据库·性能优化
qq_185198694 小时前
(Spring Bean + delegateExpression)实现http服务 的完整可运行项目
spring
是三一seven5 小时前
Sql注入基础
数据库·安全·网络安全
Sirens.5 小时前
MySQL表设计进阶-约束范式连接索引与事务
android·数据库·mysql
Ai拆代码的曹操5 小时前
Spring 事务 REQUIRES_NEW 嵌套调用:连接池翻倍的秘密
java·后端·spring
字节跳动开源6 小时前
火山引擎开源 Agent 驱动的搜索自迭代技术
数据库·开源·agent
Oo大司命oO8 小时前
藏在正则表达式里的陷阱
数据库·mysql·正则表达式
_oP_i9 小时前
mysql统计数据库使用存储大小
数据库
会编程的土豆9 小时前
MySQL 入门:库、表、行、主键是什么
linux·数据库·网络协议·http
jjjava2.010 小时前
系统日志:从入门到精通的完整指南
网络·数据库