Spring AOP 与 Spring 事务:用大白话彻底理解原理与实践

> Spring 系列 · 第 8 篇(进阶核心)

>适用读者 :会写 Spring Boot 项目,但说不清「加了 `@Transactional` 为什么没回滚」「AOP 到底是怎么生效的」的 Java 开发者。

> 技术环境 :Java 17+ · Spring Boot 3.4.x(兼容 3.x)· Spring AOP · Spring JDBC

>示例项目:spring-aop-tx-demo(./spring-aop-tx-demo/)(可运行 · 8 个集成测试全绿 · 含事务失效场景实验台)

系列文章目录

>系列阅读建议 :第 1、2 篇帮你搭好 Spring 骨架,第 4 篇解决「线上出问题怎么定位」,第 5 篇(拦截器 / MyBatis)解决「逻辑该放哪、SQL 怎么写」。本文补上最关键的一块------**你的代码到底是在什么机制下运行的**:横切逻辑怎么织进去,事务边界到底管不管用。

> 前置知识:需要你会用 `@Service` / `@Repository` 注入对象、了解基本的数据库读写。不需要你懂反射、动态代理、代理模式------本文都会从零讲起。


提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言:那个「加了注解却没回滚」的下午

先讲一个真实发生过的场景。

某天下午,测试环境报了个 bug:「用户点了取消订单,订单状态没变,但优惠券已经还回去了。」

开发同学第一反应是:事务没生效啊,加个 `@Transactional` 不就完了?

于是他在 `cancelOrder` 方法上补上了注解,重新发版------bug 依旧。

他不知道的是,那行注解加在了一个 `private` 方法上。

再过两天,同样的问题换个姿势复发:这次方法签名没问题,但方法内部 `try-catch` 把异常吞了,事务「看起来生效了」,可数据库里的数据已经写了一半。

这两个 bug 的根因是同一个:你以为 `@Transactional` 是一句「魔法咒语」,但它其实是一套有明确前提的机制。前提不满足,它会安静地不干活------不报错,不告警,只是让你的数据悄悄错掉。

而这套机制的地基,正是Spring AOP。

>一句话:AOP 是「在不改你业务代码的前提下,偷偷给方法套一层壳」的技术;而 `@Transactional` 就是 Spring 用这层壳实现出来的「要么全成、要么全败」的保险机制。

本文用费曼学习法讲清这两件事:先给结论,再讲原理,最后给能跑的代码。读完后你将能够:

  1. 说清楚 AOP 解决了什么问题,以及它和面向对象编程的分工;

  2. 画出 AOP 的 7 个核心概念,并说出每个概念在代码里长什么样;

  3. 解释 Spring AOP 为什么用代理,以及 JDK 代理和 CGLIB 的区别;

  4. 解释「同类内部方法调用」为什么会绕过代理;

  5. 完整讲出 ACID 四个特性,并举出「少了它会怎样」的例子;

  6. 熟练使用 `@Transactional`,知道事务边界该划在哪一层;

  7. 区分 `REQUIRED` / `REQUIRES_NEW` / `NESTED` 的差别与代价;

  8. 说清楚 5 种隔离级别分别防住了什么;

9.背出 12 个事务失效场景 ,遇到问题知道怎么查;

  1. 设计一个不会被并发打穿的下单流程。

本文所有代码都来自可运行的示例项目 spring-aop-tx-demo(./spring-aop-tx-demo/),包含 8 个集成测试(全部通过)和一个「事务失效场景实验台」,你可以边读边跑。

> 读前提醒:如果你对 AOP 完全没有概念,建议按顺序读完第 2、3、4 章再进入事务部分------事务部分的所有「坑」,追根溯源都在代理机制上。跳着看很容易只记住结论、记不住原因。


提示:以下是本篇文章正文内容,下面案例可供参考

一、开篇:为什么必须学AOP和事务?

1.1 一个真实的业务场景

来看电商下单。这段代码你大概率写过:

java 复制代码
public Order createOrder(CreateOrderRequest request) {
    // 1. 查商品、算价格
    Product product = productRepository.findById(request.getProductId());
    BigDecimal amount = product.getPrice().multiply(BigDecimal.valueOf(request.getQuantity()));
// 2. 创建订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
orderRepository.insert(order);

// 3. 扣库存
productRepository.deductStock(request.getProductId(), request.getQuantity());

// 4. 扣余额
accountRepository.deductBalance(request.getUserId(), amount);

// 5. 记日志
operationLogRepository.insert(order.getOrderNo(), "CREATE_ORDER");

return order;
}

看起来平平无奇对吧?问题藏在第 3 步和第 4 步之间。

1.2 如果没有事务,会发生什么?

假设用户余额只有50元,却要买一个399元的键盘。执行到第4步时,扣余额失败了。

没有事务时,数据库的状态是:

订单建了、库存扣了、钱没扣。用户拿到一个【0元订单】,商家亏一件货。

这类问题在开发环境几乎测不出来(数据都是好的,余额都够),一上生产就爆发------因为生产的真实情况余额会不足、库存会刚好卖完、网络会超时、数据库会抖动。

>核心洞察 : 出问题的不是某一行代码写错了,而是这5步被当成了5件独立的事。

>它们在业务上是一个整体(一次下单),在代码上却是5次独立的数据库写入。

>中间任何一步失败,前面的步骤不会自动撤销。

**事务要解决的就是这件事:**把5步捆成一个【要么全成、要么全败】的原子操作。

1.3 如果每个方法都手动写日志、校验、异常处理,会有什么问题?

再看上面代码的第5步【记日志】/

假设你有30个Service方法,每一个都需要:

  • 记录【谁在什么时候调用了我、参数是什么】;

  • 检查调用者有没有权限;

  • 统计这个方法耗时多少;

  • 捕获异常并转换成统一格式;

  • 开启事务、失败时回滚。

最朴素的写法是在每个方法开头手动加:

java 复制代码
long start = System.currentTimeMillis();
try {
    // ... 业务代码
} finally {
    log.info("方法执行完毕,耗时 {} ms", System.currentTimeMillis() - start);
}

问题清单:

你想到了【抽成公共方法】:

java 复制代码
public class CommonUtils {
    public static void logBefore(String method) { ... }
    public static void logAfter(String method, long cost) { ... }
}

好了一点,但只解决了一半:

真正的解法是:让这段逻辑自动生效,开发者想忘都忘不掉。

1.4 AOP 和事务分别解决了什么痛点?

用一张表收束:

> 一句话:

>AOP 解决「代码该写在哪」的问题,事务解决「数据该一致」的问题。

> 而 `@Transactional` 本身就是 AOP 的一种特殊用法------它们不是并列关系,是包含关系。

二、什么是Spring AOP

2.1 一句话定义

AOP(Aspect-Oriented Programing,面向切面编程) 是一种编程范式:把【与业务无关、却又要横跨很多类】的逻辑抽出来,在不修改原业务代码的前提下,自动织入到目标方法的执行流程中。

拆开看三个关键词:

**- 面向切面:**切面(Aspect) = 一份被抽出来的横切逻辑 + 一个【切到哪里】的规则;

-编程范式:和面向对象(OOP)、面向函数(FP)并列的一种组织代码的方式。不是某个库;

  • 自动织入: 不需要你手写调用,框架帮你插进去。

2.2 生活类比:给所有房间装门卫和计时器

想象一栋写字楼有100个房间,每间房都有人在办公。

现在物业说:

>【每个房间进门内都要刷工牌,出去要记录离开实践,且有人擅自闯入时要报警】。

你有两个选择:

方案 A(OOP 思维) :在 100 个房间里各写一遍门禁代码。

java 复制代码
public void enterRoom() {
    checkBadge();     // 刷工牌
    doBusiness();     // 办公
    recordLeave();    // 记录离开
}

结果:100 份重复代码。哪天物业说「加个访客登记」,你改100个文件

方案 B(AOP 思维) :物业在走廊上装一套统一门禁系统。

类比对照表

这就是 AOP 的全部思想:在「必经之路」上装一套统一设施,而不是在每个房间里重复施工。

2.3 AOP与面向对象编程的区别

很多人第一反应是【我写了这么多年的OOP,为什么突然要AOP?】

关键在与:OOP和AOP不是替代关系,而是分工关系。

它们是正交的。一个典型的SpringBoot项目:

OOP 负责:定义 Order、Product、Account 这些业务对象和它们的职责

AOP 负责:把「事务」「日志」「权限」这些横切关注点织入到业务方法上

一个必须澄清的误解:

> 【用了AOP就违背了OOP思想】这是错误的。

>AOP是OOP的补充 ,它解决的是OOP的一个固有短板:横切关注点无法被OOP优雅地复用。

2.4 什么是横切关注点(Cross-Cutting Concern)

横切关注点 = 横跨多个模块、但和具体业务无关的关注点。

判断标准很简单:它是不是【每个方法都要做一遍、但做的事和业务无关】?

一句话:核心关注点是【这个方法做什么】,横切关注点是【这个方法怎么做的时候,顺带做点什么】。

2.5 AOP适合解决哪些问题?

一个常见的误用是:把业务规则也塞进切面。

java 复制代码
//  反面示例:把业务规则藏进切面
@Before("@annotation(Discount)")
public void applyDiscount() {
    // 根据用户等级算折扣...(业务逻辑,不该在这)
}

判断标准:如果这个逻辑换个业务场景就不成立了,那它就不是横切关注点,而是业务逻辑,应该老老实实写在Service里。

三、AOP核心概念:一个切面到底由什么组成

SpringAOP有7个核心概念。它们对应到代码里,是这样的:

下面逐个拆解。

3.1 Aspect(切面)

切面 = 横切逻辑的代码单元,在Spring中就是一个被@Aspect标注的类。

java 复制代码
@Aspect
@Component
public class DemoLogAspect {
@Before("servicePointcut()")
public void before(JoinPoint joinPoint) {
    log.info("[Before] 即将执行: {}", joinPoint.getSignature().getName());
}
}

一个切面类里通常包含多个通知方法,每个通知针对切点上的一部分方法。

一句话:切面就是【门禁系统】这个这个整体------它包含规则(切点)和动作(通知)。

3.2 JoinPoint(连接点)

连接点 = 可以被织入通知的【点】。

在Java里,理论上每个方法调用、每个字段访问、每个构造器调用都是连接点。

但Spring AOP只支持【方法执行】 这一种连接点(SpringAOP 是基于代理的,代理只能拦截方法调用)。

java 复制代码
// 这是连接点
public Order createOrder(CreateOrderRequest request) { ... }
// 这不是 Spring AOP 能拦截的连接点
public void setStock(int stock) { this.stock = stock; }   // 字段赋值

>面试常见问题:【Spring AOP 和Aspect】有什么区别?

> 答:SpringAOP 基于代理 ,只能拦截 方法执行,且只对Spring容器管理的Bean生效;

Aspect 是 编译期/加载期字节码织入,能拦截字段访问、构造器、静态方法等,但需要额外的编译器插件。

本文讲的、以及Spring事务用的,都是SpringAOP。

3.3 Pointcut(切点)

切点 = 一个筛选器,告诉你【哪些连接点需要被织入】。

常用两种写法:

写法一:Aspect 表达式(最灵活)

java 复制代码
@Pointcut("execution(* com.example.aoptxdemo.service..*create*(..))")
public void createPointcut() { }

拆解:

写法二:注解匹配(更常用、更可读)

java 复制代码
// 匹配所有被 @Service 标注的类中的所有方法
@Pointcut("@within(org.springframework.stereotype.Service)")
public void servicePointcut() { }
// 匹配所有被 @Transactional 标注的方法
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalPointcut() { }

>*`@within` vs `@annotation` 的区别 (常被问):

> - `@within(Xxx)`:类上有 `@Xxx` 标注 → 匹配该类的所有 方法

> - `@annotation(Xxx)`:方法上有 `@Xxx` 标注 → 只匹配被标注的那个方法

> 切点方法体是空的 ,这是刻意的。它的作用只是「给切点起个名字」,

> 让下面的通知可以用 `servicePointcut()` 引用,而不用重复写一大串表达式。

3.4 Advice(通知)

通知 = 切面里具体的动作 ,即【拦截到之后要做什么】。

Spring AOP 支持5种通知:

来看一个完整的例子(来自示例项目DemoLogAspect)

java 复制代码
package org.example.springaoptxdemo;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
@Slf4j
public class DemoLogAspect {
/**
* 所有@Service
/
@Pointcut("@within(org.springframework.stereotype.Service)")
public void servicePointcut(){
//切点方法为空,作用只是【起名字】
}
/*
* 前置通知:目标方法执行前
* @param joinPoint
/
@Before("servicePointcut()")
public void before(JoinPoint joinPoint){
log.info("[Before]即将执行:{}",joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
Arrays.toString(joinPoint.getArgs()));
}
/*
* 返回通知:方法正常返回后
/
@AfterReturning(pointcut = "servicePointcut()",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result){
log.info("[AfterReturning] 正常返回:{}#{},返回值={}",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
result);
}
/*
* 异常通知:方法抛出异常时
*/
@AfterThrowing(pointcut = "servicePointcut()",throwing = "ex")
public void afterThrowing(JoinPoint joinPoint,Exception ex){
log.warn("[AfterThrowing] 抛出异常:{}#{},异常={}",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
ex.getMessage());
}
/**
 * 后置通知:无论成败都会执行,类似finally
 */
@After("servicePointcut()")
public void after(JoinPoint joinPoint){
    log.info("[After]     方法结束:{}#{}",
            joinPoint.getSignature().getDeclaringType().getSimpleName(),
            joinPoint.getSignature().getName() );
}
}

一次正常调用的日志输出

Before\] 即将执行: OrderService#createOrder,参数=\[CreateOrderRequest@1234

AfterReturning 正常返回: OrderService#createOrder,返回值=Order@5678

After 方法结束: OrderService#createOrder

一次抛异常的调用

Before\] 即将执行: OrderService#createOrder,参数=\[CreateOrderRequest@1234

AfterThrowing 抛出异常: OrderService#createOrder,异常=账户余额不足,userId=2

After 方法结束: OrderService#createOrder

> 注意执行顺序:

> `@AfterReturning` 和 `@AfterThrowing` 是互斥 的(一个走,另一个就不走),

> 而 `@After`总是 执行。

> 多个切面之间的顺序,会在第 11 章详细讲。

3.5 Target(目标对象)

目标对象 = 真正干活的业务对象,也就是你写的那个@Service类。

java 复制代码
@Service
public class OrderService {
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        // 这里是真正的业务逻辑
    }
}

`OrderService` 就是 Target。它自己完全不知道有 AOP 这回事------切面是被「织入」进去的。

3.6 Proxy(代理对象)------本文最关键的概念

代理对象=容器交给你、实际包着目标都西昂的那层壳。

这是理解SpringAOP的唯一钥匙。先记住一句话:

SpringAAOP 的所有【生效/不生效】问题,答案都在【这个调用经过代理了吗】这一句话上。

详细机制放在第四章展开,这里先给个直觉:

java 复制代码
@Service
public class OrderService { ... }
// 你以为你注入的是 OrderService
@Autowired
private OrderService orderService;
// 实际上容器注入的是 OrderService 的一个「代理子类」
// 它的类型是 OrderService(所以能注入成功),
// 但它不是你自己 new 出来的那个对象

3.7 Weaving(织入)

织入 = 把切面逻辑【插进】目标方法执行流程的过程。

SpringAOP用的是运行期织入------它不改你的字节码,而是在运行时生成一个代理对象,在你调用方法时【插手】。

>记忆要点:SPring AOP = 代理 = 运行期织入 = 只对Spring Bean生效 + 只拦截方法执行。

3.8 实战:一个【记录方法执行时间】的完整切面

现在把上面7个概念串起来,做一个最有用的切面:统计每个Service方法的耗时。

java 复制代码
package org.example.springaoptxdemo.aspeoct;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
@Slf4j
public class DemoTimingAspect {
/**
* 切点:所有Service方法
*/
@Pointcut("@within(org.springframework.stereotype.Service)")
public void servicePointcut(){
}
/**
 * 环绕通知:统计每个Service方法的耗时
 */
@Around("servicePointcut()")
public  Object around(ProceedingJoinPoint pjp)throws  Throwable{
    Long start = System.nanoTime();
    try {
        //关键一行:不屑这一行,目标方法根本不会执行
        Object result = pjp.proceed();
        return result;
    }finally {
        Long cost = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-start);
        log.info("[Around]     {}#{} 耗时{} ms",
                pjp.getSignature().getDeclaringType().getSimpleName(),
                pjp.getSignature().getName(),
                cost);
    }
}
}

7个概念在这段代码里的位置

3.9 `proceed()` 到底做了什么?

`proceed()` 是 `ProceedingJoinPoint` 上唯一的方法,它的作用是:「继续往下走,去执行目标方法」,并把目标方法的返回值交给你。

用门卫的来比喻:

@Around 就像一个门卫,可以:

  1. 先检查(前置逻辑)

  2. 决定「放行」还是「拦下」

  3. 放行 → 目标方法执行

  4. 拿到结果做后续处理(后置逻辑)

proceed() ≈ 「放行」

不调用 `proceed()` 会怎样?

java 复制代码
@Around("servicePointcut()")
public Object around(ProceedingJoinPoint pjp) {
    // 故意不调用 pjp.proceed()
    log.info("我拦截了目标方法,目标方法没有执行");
    return "被拦截的结果";
}

结果:目标方法完全不会执行。

  • 你的业务逻辑(一整套下单流程)被完全跳过 ;

  • 调用方拿到的是你硬编码的返回值;

-不报任何错,只是业务「莫名其妙没生效」。

这是线上事故的经典来源。我在示例项目的 `DemoTimingAspect` 里专门保留了这个反例方法 `brokenWithoutProceed`,你可以自己动手验证。

>记住这条铁律 :

>`@Around` 里不调用 `proceed()`,等于把这个方法「废掉」了。

> 只有当你要完全替代目标方法(比如重试、限流、熔断)时,才可以不调用它------

> 但那时你必须自己完成原本的业务逻辑。

3.10 常见误区

>误区 1 :「切面能拦截所有 Java 方法」

> → 只有 Spring 容器管理的 Bean 才有代理。你自己 `new` 出来的对象,切面对它完全无效。

> 误区 2 :「`@After` 能在 `@AfterReturning` 之后执行」

> → 它们的执行顺序取决于切面的优先级和注册顺序,不保证。需要确定顺序就用 `@Order`。

> 误区 3 :「切面里能直接调 `this.xxx()` 拿到切面自己」

> → 切面是另一个对象,`this` 指的是切面实例。

四、SpringAOP 的实现原理:代理是怎么工作的

这一章是全文的技术核心。前面所有【为什么没生效】的谜团,答案都在这里。

4.1 一张图看懂整体流程

4.2 Spring AOP为什么要使用代码?

因为Java的对象是已经存在的实体 ,你不能凭空在某个方法执行前后【插一段代码进去------除非你能在中间放一个中间人】

这个中间人就是代理对象(Proxy)。

代理模式是Java里最经典的设计模式之一,JDK自己的`java.lang.reflect.Proxy`就是它的一个实现。

Spring AOP的做法:容器创建Bean之后,发现它需要被切面增强,就不再把原始对象给你,而是生成一个代理对象塞进容器。

java 复制代码
@Service
public class OrderService {
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        return orderRepository.insert(order);
    }
}

容器里的真实情况:

所以代理对象必须和目标对象【长得一样】------否则你`@Autowired OrderService` 就注入不进去。

着引出了两种代理实现方式.

4.3 JDK 动态代理 vs CGLIB 代理

CGLIB 长什么样?

假设你的类是这样

java 复制代码
@Service
public class OrderService {
    public Order createOrder(Request req) { ... }
}

CGLIB 生成的子类大致是这样:

java 复制代码
// 由 CGLIB 在运行时生成(伪代码)
public class OrderService$$SpringCGLIB$$0 extends OrderService {
@Override
public Order createOrder(Request req) {
    // 1. 匹配切点
    // 2. 执行前置通知
    // 3. 执行环绕通知
    Object result = super.createOrder(req);   // ← 调用真实业务逻辑
    // 4. 执行后置通知
    return result;
}
}

关键点:`super.createOrder(req)`。 代理子类调用父类的真实实现,同时在前后插入切面逻辑。

4.4 Spring 如何决定使用哪种代理?

决策逻辑(简化版):

Spring Boot 的默认行为:

Spring Boot 2.0 之后有一个重要变化------`spring.aop.proxy-target-class` 默认是 `true`,也就是默认用 CGLIB。

java 复制代码
# application.yml
spring:
  aop:
    proxy-target-class: true   # 默认值,几乎不需要改

为什么默认改成 CGLIB?

因为 JDK 代理只暴露接口方法。如果你注入的是具体类(而不是接口),用 JDK 代理会导致 `ClassCastException`:

java 复制代码
@Autowired
private OrderService orderService;   // 如果 OrderService 没有接口,JDK 代理会出问题

CGLIB 生成的是子类,类型兼容性好得多。 Spring 官方推荐「面向接口编程」,但在 Spring Boot 里,你基本不需要关心这件事。

>一个必须知道的副作用 :因为 Spring Boot 默认用 CGLIB,所以**`final` 类和 `final` 方法无法被增强。**

> 这直接导致了第 10 节的「失效场景 3」。在示例项目里我显式写了:

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

> 实际上 Spring Boot 引入了 `spring-boot-starter-aop` 后会自动配置它,这里写出来是为了让读者看清「AOP 是一个被显式开启的开关」。

4.5 为什么必须通过代理对象调用,通知才会生效?

这是本文最该记住的一条。

看这段代码:

java 复制代码
@Service
public class OrderService {
@Transactional
public void methodA() {
    methodB();          // ← 自调用
}

@Transactional
public void methodB() {
    // 业务代码
}
}

`methodA()` 里调 `methodB()`,事务会生效吗?

不会。

原因,我们一步步拆:

关键在于this

自调用(self-invocation)是一个「静默失效」------不报错、不告警,事务就是没开。

4.6 三种解决自调用的办法

办法一(推荐):拆成两个 Bean

这是最干净、最推荐的做法,也是示例项目采用的方式。

java 复制代码
@Service
public class OrderEntryService {          // 外层 Bean
private final OrderService orderService;   // 注入的是代理对象

@Transactional(rollbackFor = Exception.class)
public Order createOrderRequired(CreateOrderRequest request) {
    Order order = orderService.createOrder(request);  // 跨 Bean 调用 → 经过代理 
    operationLogService.recordLog(order.getOrderNo(), "CREATE_ORDER");
    return order;
}
}
@Service
public class OrderService {                 // 内层 Bean
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
    // 真正的业务逻辑
}
}

>为什么这是最好的办法 ?因为它顺便还解决了另一个问题:

> 「事务边界该划在哪」变清晰了 ------`OrderEntryService` 是「编排层」,

> `OrderService` 是「原子业务层」。职责一分开,事务边界自然就清楚了。

办法二:注入自己:

java 复制代码
@Service
public class OrderService {
@Autowired
@Lazy   // 必须加 @Lazy,否则会循环依赖
private OrderService self;    // 注入的是代理对象

public void methodA() {
    self.methodB();           // 经过代理 
}

@Transactional
public void methodB() { ... }
}

代价:代码读起来很奇怪(为什么要注入自己?),而且 `@Lazy` 是为了打破循环依赖,不够优雅。

办法三:把不需要事务的方法拆出去

如果 `methodB()` 本来就不需要事务,那自调用根本不是问题------别硬加 `@Transactional`。

>实际开发建议 :优先用办法一。 如果一个类里方法之间互相调用且需要不同的事务策略,

> 说明这个类的职责该拆了。

4.7 一句话总结代理机制

Spring AOP = 生成代理对象 + 在代理方法里做「切点匹配 + 通知执行 + 转发给目标对象」

所以:

① 只有 Spring 容器管理的 Bean 才有代理

② 只有经过代理的调用才会触发通知

③ self-invocation(this.xxx())绕过代理 → 通知失效

五、什么是Spring事务

5.1 从银行转账说起

假设你要给朋友转账1000源。

没有事务时,可能出现:

1. 从你的账户扣了 1000 元 成功
2. 给朋友账户加 1000 元 网络超时失败了

结果:钱凭空消失了。

有事务时:

1. 从你的账户扣了 1000 元
2. 给朋友账户加 1000 元 失败
3. 事务回滚 → 第 1 步也撤销 你的钱还在

> 一句话:事务就是一个「要么全成、要么全败」的原子操作。

5.2 事务解决了什么问题?

5.3 数据库事务 VS Spring声明式事务

数据库事务是【能力】,Spring事务是【用法】。

关键点 :Spring 事务没有发明新的事务机制,它做的是三件事:

  1. 用 `@Transactional` 代替手写的 `BEGIN/COMMIT/ROLLBACK`;

  2. 用 AOP 代理 自动决定在方法前后提交还是回滚;

  3. 提供传播行为、隔离级别、只读优化等高级控制。

5.4 编程式事务 VS 声明式事务

编程式事务(手动控制):

java 复制代码
@Autowired
private PlatformTransactionManager transactionManager;
public void transfer(String from, String to, BigDecimal amount) {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
accountRepository.deduct(from, amount);
accountRepository.add(to, amount);
transactionManager.commit(status);       // 手动提交
} catch (Exception e) {
transactionManager.rollback(status);     // 手动回滚
throw e;
}
}

声明式事务(推荐):

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, BigDecimal amount) {
    accountRepository.deduct(from, amount);
    accountRepository.add(to, amount);
}

> 实际开发建议:默认用声明式 。编程式事务用在「事务边界必须精确到代码块」的场景,

> 比如一个方法里要分三段提交。

> Spring 也提供了更优雅的编程式方案 `TransactionTemplate`(示例项目里有),

> 它比裸用 `PlatformTransactionManager` 好读很多。

六、事务的ACID四大特性

ACID 是数据库事务的四条基本保证。每一个都有对应的实现机制。

6.1 原子性(Atomicity)

定义 :事务种的操作要么全部成功,要么全部回滚,不存在【做了一半】。

类比 :你在餐厅点一份套餐(主食+饮料+甜点)。厨房要么全做了端上来。

要么一样都不做、退单重来。绝不会给你端来【主食 + 一杯白开水】。

业务场景:下单事务的【插订单 + 扣库存 + 扣余额】。

余额不足时,插进去的订单记录和扣掉的库存都必须撤销。

没有原子性会怎样:

// 扣库存成功,扣余额失败

orders 表:多了一条订单

product 表:库存少了 1

account 表:余额没变

// → 商家白送一件商品

实现机制 :数据库用**Undo Log(回滚日志)**实现。

每一步修改前先把【旧值】记下来,事务失败时按Undo Log 反向还原.

6.2 一致性(Consistency)

定义 :事务执行前后,数据库都处于满足业务规则的合法状态。

类比 :银行的「账目守恒」------不管怎么转账**,所有人的钱加起来总数不变**。

业务场景 :转账前 `A 有 1000,B 有 0`;转账后 `A 有 0,B 有 1000`。

总额 1000 始终不变,这就是一致性。

没有一致性会怎样:

// 转账过程中「凭空多钱」或「凭空少钱」

A: 1000 → 0

B: 0 → 0 少了 1000 元,钱蒸发了

实现机制 :一致性是目的 ,原子性、隔离性、持久性是手段 。

上面三个都做到了,一致性自然成立。实际业务中,一致性还依赖:

  • 数据库约束 :外键、唯一索引、CHECK 约束;

  • 应用层校验:余额不能为负、订单号唯一。

6.3 隔离性(Isolation)

定义 :多个事务同时运行时,互相看不见对方的「中间状态」。

类比 :你在自己家客厅里打扫卫生,隔壁邻居也在他家扫地。

你们互相看不见对方的灰尘------这就是隔离。

业务场景:

事务 A:把库存从 10 改成 9
事务 B:读库存

如果 B 读到了 9(中间状态),B 就会以为「只剩 9 件」,
但 A 可能还会回滚,最终 B 的判断就是错的。

没有隔离性会怎样 :脏读、不可重复读、幻读 (第 9 章详述)。

最典型的是「两个请求同时买最后一件商品,都查到库存=1,都下单成功 → 超卖」。

实现机制: 数据库通过MVCC(多版本并发控制)或 锁 来实现。

每种隔离级别对应不同的锁/版本可见性策略。

6.4 持久性(Durability)

定义 :事务一旦提交 ,修改就永久保存,即使数据库立刻崩溃也不丢。

类比 :你在便利店的收银系统里付了钱,小票打出来的那一刻,这笔账就记死了 ,

哪怕收银机下一秒被拔掉电,账也还在。

业务场景:订单支付成功后,即使服务器立刻宕机重启,订单记录依然在。

没有持久性会怎样:

用户:订单显示「支付成功」!

数据库:进程重启,订单没了

// → 钱收了,货没了,用户投诉到崩溃

实现机制 :数据库写WAL(Write-Ahead Log,预写日志) 。

提交时先写日志文件并 `fsync`,数据页可以稍后再落盘。

崩溃后重启,数据库根据日志重放(redo)把已提交的事务恢复出来。

6.5 ACID 速查表

> 一句话:

>原子性管「这次操作做完没」,一致性管「做完之后数据合不合法」,
> 隔离性管「大家互不干扰」,持久性管「做完的结果丢不丢」。

七、`@Transactional` 的基本使用

71.一个完整分层示例

以本文的示例项目为例,我们看一个「规范的下单功能」是怎么写的。

Controller 层(接收请求,不写事务):

java 复制代码
package org.example.springaoptxdemo.controller;
import org.example.springaoptxdemo.common.ApiResponse;
import org.example.springaoptxdemo.model.CreateOrderRequest;
import org.example.springaoptxdemo.model.Order;
import org.example.springaoptxdemo.model.Product;
import org.example.springaoptxdemo.service.BadTransactionDemoService;
import org.example.springaoptxdemo.service.OrderEntryService;
import org.example.springaoptxdemo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
//订单接口
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private OrderEntryService orderEntryService;
@Autowired
private BadTransactionDemoService badTransactionDemoService;
/**
 * 正常下单:全流程在一个事务里。
 */
@PostMapping
public ApiResponse<Order> create(@RequestBody CreateOrderRequest request) {
    Order order = orderEntryService.createOrderRequired(request);
    return ApiResponse.success(order);
}
/**
 * 下单 + 独立事务记日志(REQUIRES_NEW)
 */
@PostMapping("/with-log")
public  ApiResponse<Order> createWithLog(@RequestBody CreateOrderRequest request){
    Order order = orderEntryService.createOrderWithNewLog(request);
    return ApiResponse.success(order);
}

/**
 * 下单 + 审计库留痕(多数据源)。
 */
@PostMapping("/with-audit")
public ApiResponse<Order> createWithAudit(@RequestBody CreateOrderRequest request){
    Order order = orderEntryService.createOrderWithAudit(request);
    return ApiResponse.success(order);
}

/**
 * <b>反例接口</b>:异常被吞,事务会提交,数据会不一致。
 *
 * <p>调用的是 {@code OrderService#createOrderAndSwallowException}------
 * 那个方法把核心业务直接写在<b>自己</b>的方法体里,异常被吞后事务会提交。
 */
@PostMapping("/swallow")
public  ApiResponse<Order> createSwallow(@RequestBody CreateOrderRequest request){
    Order order = orderEntryService.createOrderAndSwallowException(request);
    return  ApiResponse.success("异常被吞了,事务已提交(数据不一致!)", order);
}

/**
 * 反例接口:同类内部调用{@code@Transactional}不生效
 */
@PostMapping("/self-call")
public  ApiResponse<String> selfCall(@RequestBody CreateOrderRequest request){
    badTransactionDemoService.selfCall();
    return ApiResponse.success("ok","调用完成");
}
/**
 * 失败也要留痕(REQUIRES_NEW)
 */
@PostMapping("/with-failure-record")
public ApiResponse<Order> createWithFailureRecord(@RequestBody CreateOrderRequest request){
    Order order = orderEntryService.createWithFailureRecord(request);
    return ApiResponse.success(order);
}

@GetMapping("/list")
public  ApiResponse<List<Order>> list(@RequestParam(required = false) Long userId){
    return  ApiResponse.success(orderService.listOrder(userId == null ? 1L : userId));
}

@GetMapping("/{orderNo}")
public  ApiResponse<Order> get(@PathVariable String orderNo){
    return  ApiResponse.success(orderService.getOrderNo(orderNo));
}

@GetMapping("/product/{productId}")
public ApiResponse<Product> product(@PathVariable Long productId){
    return  ApiResponse.success(orderService.getProduct(productId));
}
}

Service 层(事务边界在这里):

java 复制代码
package org.example.springaoptxdemo.service;
import org.example.springaoptxdemo.model.Account;
import org.example.springaoptxdemo.model.CreateOrderRequest;
import org.example.springaoptxdemo.model.Order;
import org.example.springaoptxdemo.model.Product;
import java.util.List;
public interface OrderService {
// ==================================================================
// 3) 查询类方法(无事务)
// ==================================================================
List<Order> listOrder(Long userId);
Order getOrderNo(String orderNo);

Product getProduct(Long productId);

Order createOrder(CreateOrderRequest request);

Order createOrderAndSwallowException(CreateOrderRequest request);

Account getAccount(Long id);
}
java 复制代码
package org.example.springaoptxdemo.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.example.springaoptxdemo.model.Account;
import org.example.springaoptxdemo.model.CreateOrderRequest;
import org.example.springaoptxdemo.model.Order;
import org.example.springaoptxdemo.model.Product;
import org.example.springaoptxdemo.repository.AccountRepository;
import org.example.springaoptxdemo.repository.OrderRepository;
import org.example.springaoptxdemo.repository.ProductRepository;
import org.example.springaoptxdemo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;

/**
 * 订单核心服务:真正执行「下单」业务,并划定事务边界。
 *
 * 为什么要把核心业务单独放一个类?
 * 因为「同类内部方法调用会绕过代理」是 Spring 事务最常见的坑之一。
 * 如果在一个类里写:
 *   public Order outer()  { return inner(); }   // ← inner 的 @Transactional 失效
 *   @Transactional public Order inner() { ... }
 *
 * 就没有任何办法让 {@code inner()} 生效------除非把自身注入自己。
 *
 * 拆成两个 Bean 之后,{@code OrderEntryService → orderService.createOrder()}
 * 是跨 Bean 调用</b>,会经过代理,事务正常生效。
 */
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    private static final DateTimeFormatter NO_FORMATTER =
            DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private OrderRepository orderRepository;


    @Override
    public List<Order> listOrder(Long userId) {
        return orderRepository.findByUserId(userId);
    }

    // ==================================================================
    // 3) 查询类方法(无事务)
    // ==================================================================
    @Override
    public Order getOrderNo(String orderNo) {
        return orderRepository.findByOrderNo(orderNo);
    }

    @Override
    public Product getProduct(Long productId) {
        return productRepository.findById(productId);
    }

    // ==================================================================
    // 1) 标准事务:下单 = 创建订单 + 扣库存 + 扣余额
    // ==================================================================

    /**
     * 创建订单。
     *
     * 方法上的 {@code @Transactional} 做了三件事:
     *   调用前:开启事务、拿到数据库连接、绑定到当前线程;
     *  方法正常返回:提交事务;
     *   方法抛出异常:回滚事务(默认只对 {@code RuntimeException} 回滚,
     *       这里显式加上 {@code rollbackFor = Exception.class} 扩大范围)。
     *
     *
     * 注意「扣库存」和「扣余额」这两步之间没有 try-catch,
     * 一旦第二步失败抛异常,Spring 会把整个事务回滚------包括第一步已经扣掉的库存。
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public Order createOrder(CreateOrderRequest request) {
//        第一步:校验参数
        if (request.getQuantity() == null || request.getQuantity()<=0){
            throw new IllegalArgumentException("购买数量必须大于0");
        }
//        第二步:查商品、算金额
        Product product = productRepository.findById(request.getProductId());
        if (product ==null){
            throw new IllegalArgumentException("商品不存在:"+request.getProductId());
        }
        BigDecimal amount = product.getPrice().multiply(BigDecimal.valueOf(request.getQuantity()));
//        第三步:创建订单
        Order order = new Order();
        order.setOrderNo(generateOrderNo());
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        order.setAmount(amount);
        order.setStatus(Order.STATUS_CREATED);
        orderRepository.insert(order);
//        第四步:扣库存(乐观锁,防超卖)
        int stockRows = productRepository.deductStock(
                request.getProductId(), request.getQuantity() ,product.getVersion()
        );
        if (stockRows == 0){
//            抛异常->触发回滚_>刚才插入的订单也会被撤销
            throw new IllegalStateException("库存不足或并发冲突:productId="+ request.getProductId());
        }
//        第五步:扣余额(模拟支付)
        int accountRows = accountRepository.deductBalance(request.getUserId(),amount);
        if (accountRows==0){
            //余额不足->抛异常->订单和库存全部回滚
            throw new IllegalStateException("账户余额不足,userId="+request.getUserId());
        }
        log.info("下单成功:orderNo={},amount={}",order.getOrderNo(),amount);
        return order;
    }

    private String generateOrderNo() {
        return "ORD" + LocalDateTime.now().format(NO_FORMATTER)+ UUID.randomUUID().toString().substring(0,6).toUpperCase();
    }


    // ==================================================================
    // 5) 反例:吞掉异常导致事务不回滚
    // ==================================================================

    /**
     * 反面教材:捕获异常后没有继续抛出。
     *
     * Spring 的回滚规则是「方法抛出异常才回滚」。
     * 这里异常被 catch 住且没有再抛出,Spring 认为方法正常完成 → 提交事务。
     *
     * 结果:订单表有记录,但库存没扣,数据不一致。
     *
     * 注意:这里故意把扣库存、扣余额的逻辑直接写在方法体内,
     * 而不是调用另一个 {@code @Transactional} 方法。这是为了让演示最纯粹:
     * 如果内层方法也有 {@code @Transactional}(默认 REQUIRED,加入外层事务),
     * 内层抛异常时会把整个共享事务标记为 rollback-only,
     * 外层即使 catch 住,最终提交时依然会抛
     * {@code UnexpectedRollbackException}------事务照样回滚。
     *
     * 所以「吞异常导致事务提交」的真正前提是:
     * 异常发生在当前事务方法自身的代码里,且没有被重新抛出。
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public Order createOrderAndSwallowException(CreateOrderRequest request) {
        Order order = new Order();
        try {
            if (request.getQuantity() == null || request.getQuantity() <=0){
                throw  new IllegalArgumentException("购买数量必须大于0");
            }
            Product product = productRepository.findById(request.getProductId());
            if (product == null){
                throw new IllegalArgumentException("商品不存在:"+request.getProductId());
            }
            BigDecimal amount = product.getPrice().multiply(BigDecimal.valueOf(request.getQuantity()));
            order.setOrderNo(generateOrderNo());
            order.setUserId(request.getUserId());
            order.setProductId(request.getProductId());
            order.setQuantity(request.getQuantity());
            order.setAmount(amount);
            order.setStatus(Order.STATUS_CREATED);
            orderRepository.insert(order);
//            扣库存
            int stockRows = productRepository.deductStock(
                    request.getProductId(),request.getQuantity(),product.getVersion()
            );
            if (stockRows ==0){
                throw new IllegalStateException("库存不足,productId="+request.getProductId());
            }
            //扣余额:这一步很可能失败(余额不足)
            int accountRows = accountRepository.addBalance(request.getUserId(), amount);
            if (accountRows ==0){
                throw new IllegalStateException("账户余额不足,userId="+request.getUserId());
            }
            return order;
        }catch (RuntimeException e){
            //错误:吞掉异常,Spring会【提交】事务
            //此时订单记录和库存扣减都已经落库,但余额都没扣 ------ 数据不一致
            log.error("下单失败(但异常被吞了,事务会提交!",e);
            return null;
        }
    }

    @Override
    public Account getAccount(Long accountId) {
        return accountRepository.findById(accountId);
    }

    // ==================================================================
    // 6) 隔离级别示例
    // ==================================================================

    /**
     * 演示 {@code REPEATABLE_READ}:同一事务内两次查询同一条数据,结果一致。
     *
     * <p><b>数据库差异</b>:MySQL InnoDB 默认就是 REPEATABLE_READ;
     * PostgreSQL 默认是 READ_COMMITTED;Oracle 默认 READ_COMMITTED。
     * 所以「换了隔离级别到底会不会生效」要以实际数据库为准。
     */
    @Transactional(isolation = Isolation.REPEATABLE_READ,readOnly = true)
    public  Product readProductTwice(Long productId){
        Product first = productRepository.findById(productId);
        Product second = productRepository.findById(productId);
        log.info("隔离级别演示:第一次读取 stock={},第二次读取 stock={}",first.getStock(),second.getStock());
        return second;
    }
}

Repository 层(只管 SQL,不管事务):

java 复制代码
package org.example.springaoptxdemo.repository;
import org.example.springaoptxdemo.model.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.util.List;
/**
账户仓储
*/
@Repository
public class AccountRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public Account findById(Long id){
List<Account> list  = jdbcTemplate.query(
"select  id, username, balance from account where id =?",
(rs,rowNum) -> new Account(
rs.getLong("id"),
rs.getString("username"),
rs.getBigDecimal("balance")),
id
);
return  list.isEmpty() ? null : list.get(0);
}
/**
扣减余额。SQL 自带 {@code balance >= ?} 条件,
保证不会出现负余额(这叫「条件更新」,是防超扣的关键一环)。
@return 影响行数,0 表示余额不足
*/
public  int deductBalance(Long accountId, BigDecimal amount){
return  jdbcTemplate.update(
"update  account set balance = balance - ? where id = ? and balance >= ?",
amount,accountId,amount
);
}
/**
加回余额(订单取消补偿时使用)
*/
public  int addBalance(Long accountId, BigDecimal amount){
return  jdbcTemplate.update(
"update  account set balance = balance + ? where  id =?",
amount,accountId
);
}
}

7.2 为什么事务要加在 Service 层?

7.3 为什么 Service 层最合适?

  1. 事务边界 = 业务边界 。「创建订单」这个业务用例涉及 3 张表、5 个步骤,

它就是一个完整事务。Service 方法正好对应一个业务用例。

  1. Service 层的粒度适中 。一个 Service 方法通常几毫秒到几百毫秒,

是持有数据库连接的理想时长。

  1. Controller 关心的是「协议」 (HTTP 状态码、JSON 格式),

Service 关心的是「业务原子性」。职责要分开。

7.4 为什么不建议把事务加在 Controller 层?

java 复制代码
//不推荐
@RestController
public class OrderController {
@Transactional(rollbackFor = Exception.class)   // ← 别这么写
@PostMapping("/orders")
public ApiResponse&lt;Order&gt; create(@RequestBody CreateOrderRequest request) {
    Order order = orderService.createOrder(request);
    return ApiResponse.success(order);
}
}

四个理由:

> 一条判断标准:

>如果你的事务边界和 HTTP 路由一一对应,说明边界划错了。

> 正确的边界应该是「业务用例」,它可以被 HTTP、MQ、定时任务等多个入口复用。

7.5 `@Transactional` 默认在什么情况下回滚?

默认规则(Spring 的 `DefaultTransactionAttribute`):

回滚 iff(抛出的异常是 RuntimeException 或 Error)

java 复制代码
// 受检异常的典型例子
@Transactional
public void readFileAndSave() throws IOException {
    fileRepository.insert(content);       // 这行会被【提交】!
    throw new IOException("读取失败");     // 受检异常 → 默认不回滚
}

默认规则的逻辑:Spring 认为「受检异常通常是被声明并处理的、可预期的异常」,
所以默认提交。但这个假设在很多业务场景下是错的。

7.6 为什么受检异常默认可能不回滚?

这是一个真实踩过的坑。

java 复制代码
@Service
public class ReportService {
@Transactional
public void generateReport(Order order) throws IOException {
    reportRepository.insert(order);       // 写入报表
    exportToExcel(order);                 // 抛出 IOException
    notifyUser(order);                   // 永远不会执行
}
}

你的预期:导出失败 → 整件事白干 → 报表记录也要撤销。

实际结果 :受检异常默认不回滚 → `reportRepository.insert` 提交了 →

数据库里多了一条没用的报表记录。

更糟的是 :很多框架(如 MyBatis 的 `SQLExceptionTranslator`)

会把 SQLException 转成 `DataAccessException`(RuntimeException),

回滚了 ;但你手写的 `IOException` 就没回滚 。

行为不一致,非常难排查。

7.7 用 `rollbackFor` 配置回滚规则

最推荐写法:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) { ... }

`rollbackFor = Exception.class` 意思是:任何 `Exception`(含受检异常)都触发回滚。

其他配置:

java 复制代码
// 只对指定的受检异常回滚
@Transactional(rollbackFor = IOException.class)
public void save(File file) throws IOException { ... }
// 对多个异常回滚(Java 8+ 可以用数组)
@Transactional(rollbackFor = {IOException.class, SQLException.class})
public void save2(File file) throws IOException, SQLException { ... }
// 声明「这些异常不回滚」(noRollbackFor)
@Transactional(noRollbackFor = BizException.class)
public void process(Order order) throws BizException { ... }

Spring 6 / Spring Boot 3 的新特性:新增了 `rollbackFor` 的数组简写(上面的 `{...}` 写法)。

> 实际开发建议 :

> 在业务项目里,直接统一用 `@Transactional(rollbackFor = Exception.class)`。

> 理由很简单:「只要方法抛了异常,我就要回滚」是绝大多数业务代码的真实意图,

> 明确写出来比依赖默认规则更安全,也更容易让同事看懂。

八、Spring事务的传播行为

8.1什么是传播行为?

传播行为(Propagation)= 当一个事务方法调用另一个事务方法时,两个方法之间「事务怎么处」的规定。

打个比方:事务像一份合同 ,传播行为规定的是「新开一份合同,还是加入现有合同」。

为什么需要它?看这个场景:

java 复制代码
@Transactional
public void createOrder(...) {
    insertOrder();
    recordLog();      // ← 这个方法也想有事务。加入外层?还是自己开一份?
    sendMessage();    // ← 发消息不该在事务里(慢、且不可回滚)
}

7 种传播行为(`org.springframework.transaction.annotation.Propagation`):

8.3 逐个讲解

① `REQUIRED`(默认)
java 复制代码
@Transactional(propagation = Propagation.REQUIRED)

典型场景:绝大多数业务方法。

坑:

  • 加入外层事务意味着共享提交点 ------内层成功不代表数据落库,内层失败会导致整个外层回滚。

  • 嵌套层级越深,异常语义越混乱。

#### ② `REQUIRES_NEW`
java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)

执行流程(这是最容易搞混的地方):

外层方法(事务 T1 已开启)
↓
调用 REQUIRES_NEW 方法
↓
① 挂起 T1(保存 T1 的连接和状态)
↓
② 开启新事务 T2(占用【第二条】数据库连接)
↓
③ 执行方法体
↓
④ 提交 T2 ← 数据立刻落库!
↓
⑤ 恢复 T1
↓
外层方法继续
↓
外层方法结束 → 提交 T1

典型场景:

  • 操作日志 / 审计记录 :用户操作必须留痕,即使主业务回滚了也要记录「他试过」。

  • 失败记录 :业务失败了,也要把「失败原因」写进表里(示例项目的 `FailureRecordService`)。

  • 通知类操作:某些场景下必须立即生效且不能被主事务牵连。

坑(三个,都很致命):

> 实际开发建议 :`REQUIRES_NEW` 是精确的手术刀 ,不是万能锤子 。

> 一年用不到几次是对的,用得越多说明你的事务边界设计越乱。

> 首选方案 :把「写日志」挪到事务方法外面 (在 Controller 层或用 `@TransactionalEventListener`),

> 而不是用 `REQUIRES_NEW` 硬凑。

③ `SUPPORTS`
java 复制代码
@Transactional(propagation = Propagation.SUPPORTS)

典型场景:查询方法。有事务就复用(省一次连接获取),没有也能跑。

配合 `readOnly = true` 使用效果更好:

java 复制代码
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Order queryOrder(String orderNo) {
    return orderService.getOrder(orderNo);
}

`readOnly = true` 的优化效果:

  • Spring 跳过「脏检查」(Hibernate 场景下不生成 update 语句);

  • 数据库可以走只读优化(如 MySQL 的只读事务、连接路由到只读副本)。

④ `NOT_SUPPORTED`
java 复制代码
@Transactional(propagation = Propagation.NOT_SUPPORTED)

典型场景:耗时操作 。比如在一个事务里要调一个很慢的报表接口------

挂起事务,避免长时间持有数据库连接和锁。

坑 :方法里的写操作不受事务保护,可能只写了一半。

⑤ `NEVER`
java 复制代码
@Transactional(propagation = Propagation.NEVER)

典型场景 :「这个方法绝对不能有事务」 的硬性约束。

比如一个只读查询,如果有人把它放在事务方法里调用并期待它参与事务,就直接报错提醒。

这也是一种防御性编程------用运行时异常来暴露设计问题。

⑥ `MANDATORY`
java 复制代码
@Transactional(propagation = Propagation.MANDATORY)

典型场景:「这个方法必须在事务里执行」 。

比如「写操作日志」------你必须保证它和主业务在同一个事务里(要么都成功,要么都回滚)。

和 `NEVER` 正好相反:一个强制「必须有」,一个强制「必须没有」。

⑦ `NESTED`
java 复制代码
@Transactional(propagation = Propagation.NESTED)

典型场景:部分成功的批处理 。比如批量导入 100 条数据,第 50 条失败了,

希望前 49 条保留,只回滚第 50 条。

坑(重要):

> 实际开发建议 :`NESTED` 的可移植性差、语义复杂 。

> 大多数场景下,显式拆成多个独立事务 + 补偿机制比 `NESTED` 更好维护。

8.3 传播行为速查表

8.4 重点对比:`REQUIRED` vs `REQUIRES_NEW`

这是面试和实际开发中最常被问的一对。

用「一篇文章」和「独立发表的另一篇文章」来类比:

  • `REQUIRED`:内层是外层的一部分 ,文章没写完,外层说「这稿子不要了」→ 全部作废。

  • `REQUIRES_NEW`:内层是另一篇文章,已经发布了 → 外层说「这篇不要了」也影响不到它。

代码示例(示例项目已验证:

java 复制代码
@Transactional(rollbackFor = Exception.class)
    @Override
    public Order createOrderWithNewLog(CreateOrderRequest request) {
        Order order = orderService.createOrder(request);
        //operationLogService #recordLog上标注了REQUIRES_NEW
        operationLogService.recordLog(order.getOrderNo(),"CREATE_ORDER","下单成功(独立事务)");
        return order;
    }
java 复制代码
    /**
     * 记录操作日志:<b>独立事务,立即提交</b>。
     *
     * 执行流程:
     *挂起外层事务(如果存在);
     *   开启新事务,用另一条数据库连接;
     *   写日志;
     *   提交新事务;
     *  恢复外层事务。
     *
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    @Override
    public void recordLog(String bizNo, String action, String content) {
        operationLogRepository.insert(bizNo,action,"system",content);
        log.info("【独立事务】操作日志已提交:bizNo={},content={}",bizNo,content);
    }

实验验证:

POST /orders/with-log {"userId":1,"productId":1,"quantity":1,"requestId":"req101"}

→ 200,订单创建成功

GET /demo/log-count → 1 ← 日志已提交

8.5 重点对比:`REQUIRES_NEW` vs `NESTED`

这两个最容易混。它们都是「部分失败不影响整体」的语义,但实现方式完全不同。

关键区别一句话:

> `REQUIRES_NEW` 是「另起炉灶」------内层提交了就是最终结果,外层管不着。

> `NESTED` 是「同一篇文章的段落」------外层说不算数,内层也得跟着作废。

java 复制代码
    /**
     * 最常见的事务边界:外层方法加事务,内部调用统一用 REQUIRED。
     */
    @Override
    @Transactional(rollbackFor =  Exception.class)
    public  Order createOrderRequired(CreateOrderRequest request){
        Order order = orderService.createOrder(request);
        operationLogService.recordLog(order.getOrderNo(),"CREATE_ORDER","下单成功");
        return order;
    }

8.6 场景化选择建议

九、事务隔离级别

9.1什么是隔离级别?

隔离级别 = 多个事务并发时,彼此能看见对方多少中间状态的「严格程度」。

它是一个连续光谱,越严格越安全,但并发性能越差。

并发性能高 ←──────────────────────────→ 数据一致性高

READ_UNCOMMITTED READ_COMMITTED REPEATABLE_READ SERIALIZABLE

9.2 三种并发问题

在讲隔离级别之前,必须先搞懂这三个次。

脏读(Diry Read)

读到了另一个事务【还没提交】的数据.

时间轴:
事务 A(转账) 事务 B(查余额)
────────────────────────────────────────────────
BEGIN
UPDATE account
SET balance = 0
WHERE id = 1;
BEGIN
SELECT balance
FROM account
WHERE id = 1;
→ 读到 0 ❌ 脏读!
(但 A 随后 ROLLBACK 了,
真实余额其实是 100)
ROLLBACK
COMMIT

危害:B 基于错误的数据做了决策(比如「余额为 0,拒绝放行」),而 A 最后回滚了,B 的决策是错的。

不可重复读(Non-Repeatable Read)

同一个事务里,两次读同一条记录,结果不同(因为别人改了并提交了)。

时间轴:
事务 A 事务 B
────────────────────────────────────────────────
BEGIN
SELECT balance
FROM account WHERE id=1;
→ 100
BEGIN
UPDATE account
SET balance = 200 WHERE id=1;
COMMIT
SELECT balance
FROM account WHERE id=1;
→ 200 ❌ 不可重复读!
COMMIT

危害:A 在两次读之间做的计算(比如「余额够买两件商品吗」)可能前后矛盾。

幻读(Phantom Read)

同一个事务里,两次执行同一条 `WHERE` 条件查询,第二次多出了「幻影行」.

时间轴:
事务 A 事务 B
────────────────────────────────────────────────
BEGIN
SELECT COUNT(*) FROM orders
WHERE status = 'PENDING';
→ 5
BEGIN
INSERT INTO orders
(status) VALUES ('PENDING');
COMMIT
SELECT COUNT(*) FROM orders
WHERE status = 'PENDING';
→ 6 ❌ 幻读!(多了一行"幻影")
COMMIT

与不可重复读的区别:

  • 不可重复读 :读的是同一行 的列值 变了;

  • 幻读 :读的是行数变了(多了/少了几行匹配记录)。

9.3 5种隔离级别

`READ_UNCOMMITTED`(读未提交)

规则 :可以读到其他事务尚未提交的数据。

sql 复制代码
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

代价 :脏读、不可重复读、幻读全中。几乎没人用。

`READ_COMMITTED`(读已提交)

规则 :只能读到其他事务已经提交的数据。

sql 复制代码
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

效果:解决脏读,不解决不可重复读和幻读。

这是最常用的级别 ,包括:

  • PostgreSQL 的默认级别

  • Oracle 的默认级别

-SQL Server 的默认级别

`REPEATABLE_READ`(可重复读)

规则 :在同一个事务内 ,多次读取同一条记录,结果始终一致。

sql 复制代码
SET TRANSACTION ISOLATION LEVEL REPEATABLE_READ;

**效果**:解决脏读、不可重复读。

**幻读呢?** ------****取决于数据库实现!***这是本文必须强调的一点:

>重要提醒:

> 「`REPEATABLE_READ` 解决幻读」这个说法只对 MySQL InnoDB 成立 。

> 在 PostgreSQL 上,`REPEATABLE_READ` 仍然可能幻读。

> 不要凭「教科书结论」设计并发控制,要看你实际用的数据库。

`SERIALIZABLE`(串行化)

规则:所有事务完全串行执行,后一个必须等前一个完全结束。

效果:三种问题全解决。

代价 :

  • 并发性能极差 (容易锁等待/死锁);

  • 生产环境很少全表用 ;

  • 常见用法:只对某张热点表的某几个查询单独提级。

`DEFAULT`

**不是一种隔离级别,而是「跟随数据库默认」。**

sql 复制代码
@Transactional(isolation = Isolation.DEFAULT)

实际效果 = 数据库的默认级别

> 这就是「同一份代码,换个数据库行为就变了」的根源。

> 写与并发相关的代码时,一定要知道你连的是哪个数据库。

9.4 在 Spring 中设置隔离级别

java 复制代码
  /**
     * 演示 {@code REPEATABLE_READ}:同一事务内两次查询同一条数据,结果一致。
     *
     * <p><b>数据库差异</b>:MySQL InnoDB 默认就是 REPEATABLE_READ;
     * PostgreSQL 默认是 READ_COMMITTED;Oracle 默认 READ_COMMITTED。
     * 所以「换了隔离级别到底会不会生效」要以实际数据库为准。
     */
    @Transactional(isolation = Isolation.REPEATABLE_READ,readOnly = true)
    public  Product readProductTwice(Long productId){
        Product first = productRepository.findById(productId);
        Product second = productRepository.findById(productId);
        log.info("隔离级别演示:第一次读取 stock={},第二次读取 stock={}",first.getStock(),second.getStock());
        return second;
    }

使用建议:

9.5 「两次查询同一条数据」的例子

假设 `account` 表里 `id=1` 的余额是 100。

sql 复制代码
-- 隔离级别:READ_COMMITTED
BEGIN;
SELECT balance FROM account WHERE id = 1;   -- → 100
-- 此时事务 B 执行:UPDATE account SET balance = 200 WHERE id = 1; COMMIT;
SELECT balance FROM account WHERE id = 1;   -- → 200   两次不一样
COMMIT;
sql 复制代码
-- 隔离级别:REPEATABLE_READ
BEGIN;
SELECT balance FROM account WHERE id = 1;   -- → 100
-- 此时事务 B 执行:UPDATE account SET balance = 200 WHERE id = 1; COMMIT;
SELECT balance FROM account WHERE id = 1;   -- → 100   两次一样(读到的是快照)
COMMIT;

背后的机制(MySQL InnoDB):

事务开始时,记录一个「读视图(ReadView)」,
记录下当时所有活跃事务的版本号。

**后续的 SELECT:

  • 如果数据的版本号 < ReadView 的最小活跃事务版本 → 说明「在我开始前就改完了」
    → 直接读最新版本(因为已提交)
  • 如果数据的版本号 >= ReadView 的最大活跃事务版本 → 说明「在我开始后才有这个版本」
    → 不可见,去读 undo log 里的旧版本**

⇒ 所以整个事务内看到的都是「我开始那一刻」的快照。

这就是 MVCC(Multi-Version Concurrency Control,多版本并发控制) 。

它让「读」不需要加锁,从而在保证隔离的同时保持不错的并发性能。

十、事务失效的 12 个常见场景

**这一章是全文最实用的部分。**

>**每一个场景都遵循同一个格式:错误示例 → 为什么失效 → 正确写法 → 实际建议。**

>

> 示例项目中有一个专门的 `BadTransactionDemoService`,把大部分场景都做成了可调用的接口。

场景 1:`@Transactional` 修饰的方法不是 `public`

错误示例:

java 复制代码
@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    private void createOrderInternal(CreateOrderRequest request) {
        orderRepository.insert(order);
        accountRepository.deductBalance(request.getUserId(), amount);
    }
}

为什么失效:

Spring AOP 基于代理,而 JDK 代理和 CGLIB 都无法拦截 `private` 方法。

  • JDK 代理:代理类实现接口,接口里根本没有 `private` 方法;

  • CGLIB 代理:可以重写 `public`/`protected` 方法,但 `private` 方法子类看不到,无法重写。

正确写法:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderRequest request) {
    // ...
}

实际开发建议:

  • 事务方法一律用**`public`** ;

  • Spring 6 / Spring Boot 3 对 `private` + `@Transactional` 会在启动时直接抛异常 (更友好);

  • `protected` 和 `package-private` 方法在 CGLIB 下可以增强,但在JDK 代理 下不行------

依赖 CGLIB 时它们能用,但别依赖这个行为。

>顺带一提:Spring AOP 只能代理**外部调用 。也就是说,

> `private` 方法上就算加了 `@Transactional` 也永远不会生效------因为它本来就没打算被外部调用。

场景 2:同类内部方法互相调用

错误示例:

java 复制代码
@Service
public class OrderService {

    public void outer() {
        inner();          // ← 自调用,绕过代理
    }

    @Transactional(rollbackFor = Exception.class)
    public void inner() {
        orderRepository.insert(order);
        accountRepository.deductBalance(...);
    }
}

为什么失效:

`this.inner()` 中的 `this` 指向的是「目标对象本身」,不是代理对象。

调用链完全没有经过代理,代理里的拦截逻辑(开启事务)根本不会触发。

(详细原理见第 4.5 节。)

正确写法(推荐:拆成两个 Bean):

java 复制代码
@Service
public class OrderEntryService {
    private final OrderService orderService;

    @Transactional(rollbackFor = Exception.class)
    public void outer() {
        orderService.inner();      // ← 跨 Bean 调用,经过代理 
    }
}

@Service
public class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void inner() { ... }
}

实际开发建议:

-首选拆 Bean ------顺便让职责和事务边界都清晰了;

  • 实在不能拆,用 `AopContext.currentProxy()`(需要 `@EnableAspectJAutoProxy(exposeProxy = true)`);

  • 或者注入自己(`@Autowired @Lazy private OrderService self;`);

  • 最差的做法是到处复制粘贴同一段逻辑。

场景 3:方法内部捕获异常后没有继续抛出

错误示例:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
    try {
        orderRepository.insert(order);
        accountRepository.deductBalance(request.getUserId(), amount);
    } catch (Exception e) {
        log.error("下单失败", e);
        return null;          //  吞掉异常
    }
    return order;
}

**为什么失效**:

**Spring 的回滚规则是「方法抛出异常时才回滚」。**

异常被 catch 住且没再抛出 → 方法「正常返回」→**提交事务**。

**实测结果(示例项目已验证)**:

POST /api/orders/swallow {"userId":2,"productId":3,"quantity":5,"requestId":"V-SWALLOW-2"}

→ 200 {"message":"异常被吞了,事务已提交(数据不一致!)"}

GET /api/demo/product/3 → {"stock":15} ← 扣前是 20,库存被扣了!

GET /api/orders?userId=2 → 1 条订单 ← 订单也落库了

GET /api/demo/account/2 → {"balance":5000.00} ← 但余额没扣!

**订单建了、库存扣了、钱没扣。**这就是典型的数据不一致。

**正确写法(**两种):

java 复制代码
/ 写法一:继续抛出(推荐)
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
    try {
        orderRepository.insert(order);
        accountRepository.deductBalance(request.getUserId(), amount);
    } catch (Exception e) {
        log.error("下单失败", e);
        throw new BizException("下单失败", e);   // ← 重新抛出
    }
    return order;
}
java 复制代码
// 写法二:显式标记回滚(不改变异常语义,但事务会回滚)
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
    try {
        orderRepository.insert(order);
        accountRepository.deductBalance(request.getUserId(), amount);
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        log.error("下单失败", e);
        return null;
    }
    return order;
}

**实际开发建议**:

  • **异常要么处理干净(转换成业务异常并抛出),要么原样抛出**;

  • **绝对不要「catch 一下打个日志就吞掉」**;

  • 如果你只是想「记录日志但不影响事务」,那恰恰说明**这个日志操作应该挪到事务外**(或用 `REQUIRES_NEW`)。

>** 一个容易踩的进阶坑** :

>

> 如果你在****外层事务方法**里 catch 了**内层 `@Transactional` 方法**** 抛出的异常,

> 你可能以为「异常被吞了,事务会提交」------

>**但实际上事务照样会回滚,而且你会收到一个 `UnexpectedRollbackException`!**

>

> **原因** :内层方法用的是默认的 `REQUIRED`,它和外层****共享同一个事务**** 。

> 内层抛异常时,Spring 已经把这个共享事务标记为****rollback-only**** 。

> 外层 catch 住异常后正常返回,Spring 尝试提交时发现事务已被标记为 rollback-only,

>**只能回滚,并抛出 `UnexpectedRollbackException`** 。

UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

> 这就是为什么「吞异常导致事务提交」的真正前提 是:

> 异常发生在当前事务方法自身的代码里,且没有被重新抛出。

> 示例项目里 `OrderService#createOrderAndSwallowException` 就是这个纯粹版本。

场景 4:抛出受检异常但没有配置 `rollbackFor`

错误示例:

java 复制代码
@Transactional
public void generateReport(Order order) throws IOException {
    reportRepository.insert(order);       // 这行会被【提交】
    exportToExcel(order);                 // 抛 IOException
    notifyUser(order);
}

为什么失效:

`@Transactional` 默认只对 `RuntimeException` 和 `Error` 回滚 。

`IOException` 是受检异常(Checked Exception) ,默认提交。

正确写法:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void generateReport(Order order) throws IOException {
    ...
}

实际开发建议:

在业务项目中统一用 `@Transactional(rollbackFor = Exception.class)`。

这让「回滚意图」显式表达在代码里,也避免了团队成员误以为默认规则能覆盖所有异常。

场景 5:使用了错误的事务管理器

**错误示例(**多数据源项目):

java 复制代码
@Service
public class AuditService {

    private final JdbcTemplate auditJdbcTemplate;

    @Transactional   // 没指定 transactionManager
    public void writeAudit(String bizNo) {
        auditJdbcTemplate.update("INSERT INTO audit_log ...");
    }
}

为什么失效:

容器里有多个 `PlatformTransactionManager` 时,如果不指定用哪个 ,

Spring 会抛异常:

NoUniqueBeanDefinitionException: expected single matching bean but found 2

或者(更危险的情况):容器里只有一个事务管理器,但它绑定的是主库数据源 ------

那么审计库的操作**根本不在任何事务里,**看起来「成功」了,实际没有原子性保证。

正确写法:

java 复制代码
@Service
public class AuditService {

    private final JdbcTemplate auditJdbcTemplate;

    @Transactional(transactionManager = "auditTransactionManager",
                   propagation = Propagation.REQUIRES_NEW,
                   rollbackFor = Exception.class)
    public void writeAudit(String bizNo) {
        auditJdbcTemplate.update("INSERT INTO audit_log (biz_no, action) VALUES (?, ?)",
                bizNo, "ORDER_CREATED");
    }
}

实际开发建议:

  • 多数据源项目里**,每个数据源配一个事务管理器** ,并显式指定 `transactionManager`;

  • 给默认的那个打上 `@Primary`,减少其他地方的配置负担;

  • 更重要的坑 :注入 `JdbcTemplate` 时必须用 `@Qualifier`,否则会注入错数据源(见场景 7)。

场景 6:数据库引擎不支持事务

错误示例:

sql 复制代码
CREATE TABLE log_2024 (
    id   INT PRIMARY KEY,
    msg  VARCHAR(255)
) ENGINE = MyISAM;    --  MyISAM 不支持事务

为什么失效:

`@Transactional` 只是一层「指挥」 ,真正执行 `BEGIN` / `COMMIT` / `ROLLBACK` 的是数据库。

如果存储引擎不支持事务,那这些命令要么报错、要么被忽略。

正确写法:建表时用 `ENGINE=InnoDB`。

sql 复制代码
CREATE TABLE log_2024 (
    id   INT PRIMARY KEY,
    msg  VARCHAR(255)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

实际开发建议:

  • 建表脚本一律显式指定 `ENGINE=InnoDB`;

  • 排查「事务不生效」时,第一步就确认表引擎 ;

  • 顺便提一句:MySQL 8.0 起 InnoDB 是默认引擎,但老项目里 MyISAM 表仍然存在。

场景 7:使用多数据源但事务管理配置不正确

错误示例:

java 复制代码
@Bean
public PlatformTransactionManager auditTransactionManager(DataSource auditDataSource) {
    return new DataSourceTransactionManager(auditDataSource);
}

@Bean
public JdbcTemplate auditJdbcTemplate(DataSource auditDataSource) {
    return new JdbcTemplate(auditDataSource);
}

为什么失效(这里有一个非常隐蔽的坑):

假设主库数据源标了 `@Primary`,而你写的是:

java 复制代码
@Bean
public JdbcTemplate auditJdbcTemplate(DataSource auditDataSource) {
    return new JdbcTemplate(auditDataSource);
}

你可能拿到的是主库的 DataSource,而不是审计库的!

原因 :Spring 解析 `DataSource` 类型的依赖时,

`@Primary` 的优先级高于参数名匹配 。参数名 `auditDataSource` 是兜底规则 ,

只有当没有 `@Primary` 候选时才生效。

> 这个坑是我在写示例项目时真实踩到的------诊断代码打印出来是这样:

auditJdbcTemplate 实际URL: jdbc:h2:mem:orderdb ← 错!这是主库

auditDataSource 实际URL: jdbc:h2:mem:auditdb

是否同一个对象: false

症状是「明明往审计库写,结果数据跑到主库去了」------极难排查。

正确写法:

java 复制代码
@Bean
public PlatformTransactionManager auditTransactionManager(
        @Qualifier("auditDataSource") DataSource auditDataSource) {
    return new DataSourceTransactionManager(auditDataSource);
}

@Bean
public JdbcTemplate auditJdbcTemplate(
        @Qualifier("auditDataSource") DataSource auditDataSource) {
    return new JdbcTemplate(auditDataSource);
}

另一个必须知道的坑:

java 复制代码
//  Spring Boot 的 DataSourceAutoConfiguration 上有 @ConditionalOnMissingBean(DataSource.class)
// 只要容器里存在【任何】一个 DataSource,主库的自动配置就【整个退避】了!
// 结果:主库的 DataSource / JdbcTemplate / TransactionManager 全部没有了
// 症状:启动报 No qualifying bean of type 'javax.sql.DataSource'

正确写法(两种):

java 复制代码
// 方式一:排除自动配置,自己全配(行为最明确,示例项目采用)
// application.yml
spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
      - org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
      - org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
java 复制代码
// 方式二:自己声明主库的 DataSource,并标 @Primary
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource")
public DataSourceProperties primaryDataSourceProperties() {
    return new DataSourceProperties();
}

@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public DataSource primaryDataSource(DataSourceProperties properties) {
    return properties.initializeDataSourceBuilder().build();
}

*实际开发建议:

  • 多数据源项目统一用 `@Qualifier` 显式标注 ,不要依赖参数名;

  • 排除自动配置,行为更可控;

  • 诊断技巧 :写个测试打印 `xxxJdbcTemplate.getDataSource().getConnection().getMetaData().getURL()`,

确认注入的到底是哪个库。(示例项目里的 `JdbcTemplateDiagnosticTest` 就是干这个的。)

场景 8:在异步线程中期待继承原线程事务

错误示例:

java 复制代码
@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest request) {
        orderRepository.insert(order);

        //  以为异步线程会继承这个事务
        CompletableFuture.runAsync(() -> {
            notificationRepository.insert(...);
        });
    }
}

**为什么失效**:

Spring 事务把「当前线程的数据库连接」绑定在 `TransactionSynchronizationManager` 的 `ThreadLocal` 上。

切换到线程池线程后,那个 ThreadLocal 是空的。

主线程(事务 T1 活跃) 异步线程(全新 ThreadLocal)

──────────────────────── ─────────────────────────

T1 绑定在主线程的 ThreadLocal 异步线程的 ThreadLocal 是空的

→ 没有事务

→ 自己的写操作要么自动提交,要么开新事务

更麻烦的是 :外层事务回滚时,异步线程的操作不会跟着回滚。

正确写法:

java 复制代码
/ 方案一:把异步操作挪到事务提交之后
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderRequest request) {
    orderRepository.insert(order);
    // 用 Spring 的事务同步机制,在【事务提交后】触发
    TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    asyncService.sendNotification(order.getId());
                }
            });
}
java 复制代码
// 方案二:@TransactionalEventListener(更优雅,推荐)
@Service
public class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest request) {
        orderRepository.insert(order);
        publisher.publishEvent(new OrderCreatedEvent(order.getId()));
    }
}

@Component
public class OrderEventListener {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Async
    public void onOrderCreated(OrderCreatedEvent event) {
        // 只在主事务【提交成功后】才执行,且在异步线程里
        notificationService.send(event.getOrderId());
    }
}

**实际开发建议**:

  • **「事务提交后再做」** 这个需求,用 `@TransactionalEventListener(phase = AFTER_COMMIT)`,

别用 `REQUIRES_NEW` 硬凑;

  • `@Async` 方法上自己加 `@Transactional`,它会在自己的线程 里开一个全新的事务 ------

和原事务毫无关系 ;

  • 需要「跨线程传递上下文」时用 `TaskDecorator` + `TransmittableThreadLocal`(TTL),

但要清楚**:事务本身不能跨线程传递**,TTL 传递的也只是 ThreadLocal 里的值。

场景 9:在事务方法中调用远程服务,误以为远程调用也受本地事务保护

错误示例:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderRequest request) {
    orderRepository.insert(order);
    accountRepository.deductBalance(request.getUserId(), amount);

    //  以为远程调用失败会导致本地回滚
    //  也以为本地回滚会「撤销」远程调用
    remotePaymentClient.pay(request.getUserId(), amount);
}

为什么失效:本地事务只能管住「本地数据库事务」。 远程调用是另一个系统 的事务,两者没有任何关联。

具体地说,存在两个方向的问题:

正确写法:

java 复制代码
//  正确做法:远程调用【不】放在事务里
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
    orderRepository.insert(order);
    accountRepository.deductBalance(request.getUserId(), amount);
    return order;
}

// 事务提交后,再调远程服务
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
    try {
        remotePaymentClient.pay(event.getUserId(), event.getAmount());
    } catch (Exception e) {
        // 失败就走补偿流程(退款/重试/记失败表)
        compensationService.scheduleCompensation(event);
    }
}

实际开发建议:

-事务里绝对不要放远程调用 (HTTP/RPC/MQ 发送);

  • 远程操作用「本地消息表 」或「事务消息 」保证最终一致性;

  • 如果远程调用必须在事务内(比如分布式事务框架 XA/TCC),那要引入专门的

分布式事务方案,不要指望本地 `@Transactional` 能搞定。

场景 10:事务注解加在接口、实现类或方法上的注意事项

三种写法:

java 复制代码
// 写法一:加在接口方法上
public interface OrderService {
    @Transactional(rollbackFor = Exception.class)
    Order createOrder(CreateOrderRequest request);
}

// 写法二:加在实现类的方法上
@Service
public class OrderServiceImpl implements OrderService {
    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(CreateOrderRequest request) { ... }
}

// 写法三:加在实现类上(类级别,作用于所有 public 方法)
@Service
@Transactional(rollbackFor = Exception.class)
public class OrderServiceImpl implements OrderService { ... }

注意事项:

Spring 的查找规则(简化):

1. 目标方法上有 @Transactional → 用它
2. 目标方法所在类上有 @Transactional → 用它
3. 接口方法上有 @Transactional → 用它(仅 JDK 代理时可靠)
4. 接口上有 @Transactional → 用它(仅 JDK 代理时可靠)
5. 都没有 → 无事务

>关键前提 :

> 规则 3、4 之所以有条件,是因为CGLIB 代理的是实现类 ,

> 而 Spring Boot 2.0+默认用 CGLIB 。

> 所以「接口上的 `@Transactional` 会被忽略」这个坑,在 Spring Boot 项目里非常普遍 。

>

> 实测 :很多老项目从 Spring Boot 1.x 迁移到 2.x 后,

> 「事务突然失效了」的原因就是注解加在了接口上。

实际开发建议:

永远把 `@Transactional` 加在具体实现类的方法上(或类上)。

这是唯一在所有代理方式下都确定可靠的做法。

场景 11:事务方法使用 `final`、`private` 等修饰符

错误示例:

java 复制代码
@Service
public class OrderService {

    //  final 方法:CGLIB 无法重写 → 切面织入不进去
    @Transactional(rollbackFor = Exception.class)
    public final void createOrder(CreateOrderRequest request) { ... }
}

*为什么失效**:

**CGLIB 的原理是「生成子类并重写方法」**。

而 `final` 方法****不能被重写**** ,`final` 类****不能被继承****。

OrderService (final 方法)

↓ CGLIB 生成子类

OrderServiceSpringCGLIB0 extends OrderService

↓ 尝试重写 final 方法 createOrder()

→ 编译期就报错了(final 方法不能重写)

→ 所以 Spring 只能「放弃」这个方法,事务不生效

Spring 6 / Spring Boot 3 的表现 :

在 Spring 6 中,如果一个 `@Transactional` 方法是 `private` 或 `final`,

Spring 在启动时就会抛 `InvalidDataAccessApiUsageException` ("Annotation @Transactional is not supported on final methods"),

比 Spring 5 时代「静默失效」要好。

Spring 5 及更早版本:静默失效,不报错,只是事务没生效。

正确写法:

java 复制代码
Service
public class OrderService {

    // 去掉 final
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest request) { ... }
}

实际开发建议:

- 事务方法不要加 `final` ;

-**事务所在的类不要加 `final` ;

  • `private` 方法同样不行(见场景 1);

  • 这类问题在Spring 6 会被启动时拦截 ,但如果你的项目还在 Spring 5,

一定要自己检查。

场景 12:事务方法内部执行耗时操作导致事务持有时间过长

错误示例:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderRequest request) {
    orderRepository.insert(order);
    accountRepository.deductBalance(request.getUserId(), amount);

    //  在事务里调远程服务、发 MQ、生成大 Excel、上传文件
    remoteInventoryService.syncToErp(request.getProductId());
    messageSender.send(order);
    excelExporter.export(order);
}

**为什么危险**:

事务持有期间,**数据库连接被占用**,而且可能**持有行锁/间隙锁**。

务开始 ──────────── 事务结束(可能 30 秒)

│ │

└─ 期间一直持有: └─ 提交/回滚

• 数据库连接

• 行的排他锁(UPDATE ... FOR UPDATE)

• 间隙锁(MySQL RR 隔离级别下)

• MVCC 的 undo log 版本

带来三个问题:

正确写法:

java 复制代码
// ✅ 把耗时操作挪到事务外
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
    orderRepository.insert(order);
    accountRepository.deductBalance(request.getUserId(), amount);
    return order;
}

// 耗时操作放在事务【之后】
public Order createOrderWithNotification(CreateOrderRequest request) {
    Order order = createOrder(request);          // 事务在这里结束
    remoteInventoryService.syncToErp(request.getProductId());  // 事务外
    messageSender.send(order);
    return order;
}

如果耗时操作必须在事务内(比如需要一致性),用 `NOT_SUPPORTED` 挂起事务:

java 复制代码
@Transactional(propagation = Propagation.NOT_SUPPORTED)   // 挂起外层事务
public void generateBigExcel(Order order) {
    // 此时不在事务中,不占用连接和锁
}

实际开发建议:

-事务方法要短小,理想是几十毫秒 ;

  • 事务内禁止 :远程调用、MQ 发送、文件读写、大量数据遍历、复杂计算、`Thread.sleep`;

  • 「事务方法耗时」的监控是生产环境必配项。

10.13 排查清单(速查表)

十一、Spring AOP 与 Spring 事务的关系

11.1 核心结论

`@Transactional` 本质上就是基于 Spring AOP 实现的一个切面。

>

>事务不是一个独立机制,它是「AOP + 一个通知 + 一个事务管理器」的组合。

Spring 内部到底做了什么?

EnableTransactionManagement
↓
注册一个 BeanPostProcessor(InfrastructureAdvisorAutoProxyCreator)
↓
它会扫描所有 Bean,找出带 @Transactional 的方法
↓
给这些 Bean 生成【代理对象】
↓
在代理对象的方法上挂一个拦截器:TransactionInterceptor
↓
TransactionInterceptor 就是那个「通知(Advice)」

所以 `@Transactional` 的执行过程是:

调用者
↓
代理对象
↓
【TransactionInterceptor】← 这是切面的「通知」
│ 1. 开启事务(拿到连接、绑定线程)
│ 2. proceed()
↓
目标方法执行(你的业务代码 + 其他切面的逻辑)
↓
【TransactionInterceptor】继续
│ 3. 正常返回 → 提交事务
│ 3. 抛出异常 → 判断回滚规则 → 回滚事务
↓
返回结果

11.2 开启事务 / 提交 / 回滚的完整流程

调用代理对象

↓

TransactionInterceptor 拦截

↓

开启事务(获取数据库连接、设置 autoCommit=false、绑定到当前线程)

↓

执行目标方法

↓

执行数据库操作

↓

方法成功:提交事务(commit)

方法异常:回滚事务(rollback)

11.3 事务切面和其他切面的执行顺序

这是实际开发中一个很值得知道的细节。

假设你有三个切面:

java 复制代码
@Aspect @Component
public class LogAspect { ... }              // 日志

@Aspect @Component
public class MonitorAspect { ... }           // 监控

@Aspect @Component
public class PermissionAspect { ... }       // 权限

Spring的切面顺序规则:

未指定 `@Order` 时的默认顺序 :Spring AOP 的默认顺序是不确定的

(实际上取决于类扫描顺序、类名哈希等),不要依赖它。

事务切面的位置:

`@EnableTransactionManagement` 注册的事务拦截器,

默认的 `@Order` 是**`Ordered.LOWEST_PRECEDENCE`(最低优先级)。**

这意味着(默认配置下):

进入方法时:

你的其他切面(@Before) ← 先执行

事务拦截器 ← 后执行(所以事务是最内层开的)

退出方法时:

事务拦截器(提交/回滚) ← 先执行

你的其他切面(@After) ← 后执行

含义:

  • 事务是最内层 的 → 事务范围最小 → 这是好事,事务时间短;

  • 你的 `@After` 切面在事务提交之后执行 → 这时数据库已经提交了。

如果需要事务是最外层(让整个切面逻辑都在事务里),可以显式指定:

java 复制代码
@Configuration
@EnableTransactionManagement(order = Ordered.HIGHEST_PRECEDENCE)
public class TransactionConfig { ... }

11.4 一个必须理解的细节

多个切面 + 事务时,异常的回滚判断看的是「最外层抛出的异常」。

@Around 切面 A(包住一切)
└─ 事务切面
└─ 目标方法(抛异常)

  • 目标方法抛异常 → 事务切面看到异常 →判定回滚 → 回滚;

  • 然后异常继续向外传播 → 切面 A 的 `@AfterThrowing` 执行。

如果切面 A 把异常吞了(catch 住不抛):

  • 事务切面在切面 A 的内层 ,它已经 看到了原始异常并完成了回滚;

  • 切面 A 吞掉异常后,外层看到的是「正常返回」;

-但数据已经回滚了------切面 A 以为成功了,实际上没有。

> 这解释了为什么「吞异常」是个危险操作 :它会让调用链上层的代码产生错误认知。

11.5 事务边界设计本质上也是 AOP 的应用

回到最开始的问题:

> 「`@Transactional` 该加在哪个方法上?」

用 AOP 的视角看,答案变得非常清晰:

所以设计事务边界的本质,就是回答三个问题:

1.哪些操作必须一起成功或一起失败? → 决定事务的范围 (切点)

  1. 这个范围对应哪个方法? → 决定在哪加注解 (Service 层)

  2. 失败时怎么处理? → 决定回滚规则和异常处理

> 一句话 :

> 事务边界就是 AOP 的切点在业务语义上的具体化。

> 理解了 AOP,「事务该加在哪」就不需要死记了。

十二、实战案例:电商下单事务

12.1 完整的业务流程

12.2 事务该放在哪一层?

为什么两层都加事务?

因为 `OrderService#createOrder` 可能被其他入口 调用(MQ 消费者、定时任务、RPC 接口),

它自己就该是一个完整的事务。而当它被 `OrderEntryService` 调用时,

`REQUIRED` 让它加入 外层事务------两者合成一个大事务。

> 这是「事务边界」设计的经典模式 :

>每个原子业务方法自带事务(保证独立可用),
> 编排层用 `REQUIRED` 把它们合成一个更大的事务(保证整体原子性)。

12.3 哪些操作应该放在同一个事务中?

12.4 哪些操作不适合直接放在本地数据库事务中?

判断标准:三个问题里有任意一个答「是」,就别放在事务里。

1**. 它是远程调用吗?** (HTTP / RPC / MQ)

  1. 它很慢吗? (超过几百毫秒)

  2. 它失败后能自动重建吗?(缓存、索引、报表、通知)

这类操作的正确处理方式------最终一致性:

java 复制代码
//  方式一:@TransactionalEventListener(推荐)
@Service
public class OrderEntryService {

    private final OrderService orderService;
    private final ApplicationEventPublisher publisher;

    @Transactional(rollbackFor = Exception.class)
    public Order createOrderRequired(CreateOrderRequest request) {
        Order order = orderService.createOrder(request);
        // 发布事件(在事务内)
        publisher.publishEvent(new OrderCreatedEvent(order.getId()));
        return order;
    }
}

@Component
public class OrderEventListener {

    /** 只在事务【提交成功后】执行 */
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderCreated(OrderCreatedEvent event) {
        // 发 MQ、通知用户、同步 ERP 都在这里
        mqTemplate.send("order.created", event);
        notificationService.push(event);
    }
}

为什么 `AFTER_COMMIT` 是对的?

(错误) 在事务里发 MQ:

本地事务提交失败 → MQ 消息已发出 → 下游以为订单成功了 → 数据不一致

本地事务提交成功 → MQ 发送失败 → 消息丢了 → 下游永远收不到

(正确)AFTER_COMMIT 发 MQ:

本地事务提交失败 → MQ 不发 → 一致 (正确)

本地事务提交成功 → MQ 发送失败 → 可以重试 / 靠本地消息表兜底

还有一种更可靠的方式------本地消息表:

在同一个【本地】事务里:

  1. 写订单

  2. 写消息表(message_outbox)

另一个【独立】的定时任务/消费者:

扫描 message_outbox → 发 MQ → 标记已发送

它把「跨系统的原子性」转化成了「本地事务 + 至少一次投递」,

不依赖分布式事务,是业界主流方案。

12.5 如何避免库存扣减出现超卖?

**超卖的根源**:两个请求同时读到 `stock = 1`,都认为「够卖」,都下单成功。

**方案一:乐观锁(示例项目采用)**

sql 复制代码
UPDATE product
SET stock = stock - ?, version = version + 1
WHERE id = ? AND version = ? AND stock >= ?
java 复制代码
int stockRows = productRepository.deductStock(
        request.getProductId(), request.getQuantity(), product.getVersion());
if (stockRows == 0) {
    throw new IllegalStateException("库存不足或并发冲突");
}

工作原理:

请求 A 读到 version=0 请求 B 读到 version=0

│ │

▼ ▼

UPDATE ... WHERE version=0 UPDATE ... WHERE version=0

影响 1 行(version → 1) 影响 0 行(version 已经是 1 了)

│ │

扣减成功 失败 → 抛异常 → 回滚

影响 0 行说明 :要么库存不足,要么别人抢先改了。后者可以重试(重新读 version 再试)。

方案二:悲观锁(`SELECT ... FOR UPDATE`)

java 复制代码
SELECT stock FROM product WHERE id = ? FOR UPDATE

并发请求会排队 ,严格串行**。缺点**:并发性能差,容易锁等待/死锁。

方案三:Redis 预扣减 + 异步落库(秒杀场景)

Redis 原子扣减(Lua 脚本保证原子性)→ 成功才发 MQ → 消费者异步写数据库

优点 :能扛住远超数据库承载能力的并发**。缺点**:复杂度高,需要处理 Redis 与 DB 的一致性。

方案四:数据库唯一索引兜底(必做)

sql 复制代码
-- 同一个 requestId 只能创建一笔订单
CREATE UNIQUE INDEX uk_orders_request_id ON orders (request_id);

这是最后一道防线------即使幂等切面被绕过,数据库也不会出现重复订单。

> 实际开发建议 :

>乐观锁 + 唯一索引 + 幂等切面,三重保险。

> 单靠任何一层都不够可靠。

12.6 完整的事务边界设计

java 复制代码
@Service
public class OrderEntryService {

    private final OrderService orderService;
    private final OperationLogService operationLogService;
    private final ApplicationEventPublisher publisher;

    /**
     * 【编排层】事务边界在这里。
     */
    @Transactional(rollbackFor = Exception.class)
    public Order createOrderRequired(CreateOrderRequest request) {
        // ① 原子业务:加入当前事务
        Order order = orderService.createOrder(request);

        // ② 操作日志:独立事务(必须留痕)
        operationLogService.recordLog(order.getOrderNo(), "CREATE_ORDER", "下单成功");

        // ③ 发布事件(MQ 发送等放在 AFTER_COMMIT 监听器里)
        publisher.publishEvent(new OrderCreatedEvent(order.getId()));

        return order;
    }
}

三步的责任划分:

十三、实际开发中的最佳实践

13.1 十二条建议

① 事务边界放在 Service 层

java 复制代码
@Service
public class OrderService {
    @Transactional(rollbackFor = Exception.class)   // 
    public Order createOrder(CreateOrderRequest request) { ... }
}

② 事务方法要短小

  • 理想:几十毫秒 ;

  • 禁止在事务里:远程调用、MQ 发送、文件读写、大数据量遍历、`Thread.sleep`;

  • 监控「事务耗时」是生产环境必配项。

③ 不要在事务中调用不稳定的远程服务

  • 远程调用会拖长事务,还可能「远程成功但本地回滚」;

  • 正确做法:挪到事务外,或用 `@TransactionalEventListener(AFTER_COMMIT)`。

④ 明确配置回滚规则

java 复制代码
@Transactional(rollbackFor = Exception.class)   // 明确优于依赖默认值

⑤ 不要依赖同类内部调用触发事务

  • 首选:拆成两个 Bean ;

  • 其次:注入自己(`@Autowired @Lazy`)。

⑥ 正确处理异常,不要吞掉异常

java 复制代码
try {
    ...
} catch (Exception e) {
    log.error("失败", e);
    throw new BizException("操作失败", e);     //  重新抛出
}

⑦ 根据业务合理选择传播行为和隔离级别

⑧ 注意多数据源事务问题

  • 每个数据源配一个事务管理器,**显式指定** `transactionManager`;

  • 注入 `DataSource` / `JdbcTemplate` **必须用 `@Qualifier`**;

  • 记住 `@Primary` 的优先级高于参数名匹配。

⑨ 正确认识「本地事务」的边界

  • 本地事务只能管住本地数据库 ;

  • 跨系统一致性***最终一致性(**本地消息表 / 事务消息 / `AFTER_COMMIT` 事件)。

⑩ 对事务日志和慢事务进行监控

建议监控的指标:

⑪ 不要把「加了 `@Transactional`」等同于「事务一定生效」

这是本文最重要的一条建议。

`@Transactional` 生效需要同时满足:

目标对象是 Spring 容器管理的 Bean

方法是 public 且非 final

调用经过代理(不是自调用)

抛出的异常符合回滚规则(或配了 rollbackFor)

使用了正确的事务管理器

数据库引擎支持事务

六个条件,任何一个不满足,事务都会静默失效。

⑫ 用测试验证事务行为

  • 不要假设它生效,要验证它生效 ;

  • 示例项目的 `OrderServiceTransactionTest` 演示了怎么写这类测试:

13.2 一份可以贴在工位上的检查清单

□ 事务在 Service 层,不在 Controller 层

□ 事务方法是 public、非 final

□ 没有同类内部调用触发事务

□ 异常会继续抛出(或显式 setRollbackOnly)

□ 配了 rollbackFor = Exception.class

□ 事务方法里没有远程调用 / MQ / 文件操作

□ 传播行为是深思熟虑选的,不是抄的默认值

□ 隔离级别用 DEFAULT,除非有明确理由

□ 多数据源场景显式指定了 transactionManager

□ 注入 DataSource/JdbcTemplate 用了 @Qualifier

□ 表引擎是 InnoDB

□ 异步操作放在 AFTER_COMMIT

□ 关键业务有集成测试验证回滚行为


十四、总结与排查清单

14.1 一段话总结

Spring AOP 通过「代理」把横切逻辑自动织入到方法执行流程中 ------容器给你的不是目标对象本身,

而是一个包着它的代理对象,调用时先经过代理,代理执行切面逻辑后再转交目标对象。

Spring 事务正是建立在 AOP 之上的一个切面 :`@Transactional` 会被 Spring 识别,

给目标 Bean 生成代理,并在代理方法上挂一个 `TransactionInterceptor`------

它在方法执行前开启事务、方法正常返回时提交、方法抛出符合规则的异常时回滚。

理解这两者的关系,就等于同时掌握了 AOP 和事务 :

AOP 回答「横切逻辑怎么自动生效」,事务回答「数据怎么保持一致」;

而「事务失效」的所有谜团,最终都归结为一个问题------这次调用到底经过代理了吗。

14.2 AOP 与事务对比表

14.3 AOP 核心概念速查

14.4 传播行为速查

14.5 「排查事务不生效」的检查清单

按顺序逐条排查,90% 的问题在前 4 条就能定位。

第 1 步:确认调用路径】

□ 这行代码是【通过注入的 Bean】调用的吗?(不是 new 出来的)

□ 调用经过【代理】吗?(不是 this.xxx() 自调用)

→ 断点打在代理类的方法上,看它是否被命中

【第 2 步:确认方法签名】

□ 方法是 public 吗?

□ 方法是 final 吗?所在类是 final 吗?

□ 是不是 static 方法?(static 方法无法被代理)

【第 3 步:确认回滚规则】

□ 配了 rollbackFor = Exception.class 吗?

□ 异常是【受检异常】吗?(默认不回滚)

□ 异常被【catch 住没重新抛出】吗?

→ 注意:如果是内层 @Transactional 抛的,事务照样会回滚

【第 4 步:确认运行环境】

□ 用的是正确的【事务管理器】吗?(多数据源时)

□ 数据库【引擎】支持事务吗?(MyISAM 不支持)

□ 是不是在【异步线程】里调的?

□ 是不是在【远程调用】的另一个进程里?

【第 5 步:确认数据源绑定】

□ 注入的 JdbcTemplate/DataSource 绑定的【是哪个库】?

→ 打印 jdbcTemplate.getDataSource().getConnection().getMetaData().getURL()

【第 6 步:加日志确认】

□ 打开 org.springframework.transaction 的 DEBUG 日志

→ 能看到事务的开启、提交、回滚

【第 7 步:写测试验证】

□ 写一个集成测试,验证失败时数据【确实没落库】

→ 这比任何猜测都可靠

开启事务日志:

java 复制代码
logging:
  level:
    org.springframework.transaction: DEBUG
    org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG

输出示例:

Creating new transaction

Acquired Connection conn1 for JDBC transaction

Switching JDBC Connection conn1 to manual commit

Transactional method returned: no exception encountered → 提交

Transactional method threw exception → 回滚

> 看到 `Transactional method threw exception` 却没回滚 ?

> 那就检查:是不是内层方法抛的、事务已被标记 rollback-only ,

> 或者外层 catch 住了异常。

写在最后

Spring AOP 和 Spring 事务,是 Spring 生态里最强大也最容易误用的两个工具。

强大 ,因为它们能让你用一行注解解决原本需要几百行样板代码的问题------

事务、权限、日志、幂等,全都自动生效。

容易误用 ,因为它们都是基于代理的隐式 机制。

代理的规则你不了解,就会遇到「不报错、但就是没生效」的诡异问题。

所以本文反复强调的那句话,值得再读一遍:

> AOP 的一切问题,最终都归结为「这次调用经过代理了吗」。

>事务的一切问题,最终都归结为「那六个前提条件都满足了吗」。

把这两个问题变成条件反射式的习惯,

你就从「会用 `@Transactional`」进阶到了「能设计出正确的事务边界」。

相关推荐
YDS82914 分钟前
Small Spring IOC篇:容器事件及其监听器
java·spring
金銀銅鐵22 分钟前
带您看懂字节码指令(三):一行加法里有什么
java·jvm
文丑颜不良啊27 分钟前
LeetCode2921~2940题目解析
java·leetcode
我不会起名字32235 分钟前
未接入最终读上限的参数:`websocketMaxMsgLen` 与 12.5 倍差距
后端·websocket·go·即时通讯·im·故障排查
小此方1 小时前
Re:Mysql数据库基础篇(十二):什么是事务、事务解决了什么问题、事务的四大属性、事务的操作方式
数据库·mysql
一条Go1 小时前
接收方合并去重:同一个人为什么只该收到一次通知
后端·架构·github
北漂人Java1 小时前
基于 Spring AI Alibaba WorkFlow 构建 Java 生产级 AI Agent
java·人工智能·spring
张一凡932 小时前
给 Coding Agent 加一道"架构闸门":Jin 是如何做到"先审架构、再写代码、不漂移"的
前端·后端·agent
Jvav练习生2 小时前
AI Agent 总在关键时刻“失忆”翻车?一文讲透上下文工程(附可运行 Python 代码)
后端