> 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` 注入对象、了解基本的数据库读写。不需要你懂反射、动态代理、代理模式------本文都会从零讲起。
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- [- 前言:那个「加了注解却没回滚」的下午(#前言那个加了注解却没回滚的下午)
- 一、开篇:为什么必须学 AOP 和事务(#一开篇为什么必须学-aop-和事务)
- 二、什么是 Spring AOP(#二什么是-spring-aop)
- 三、AOP 核心概念:一个切面到底由什么组成(#三aop-核心概念一个切面到底由什么组成)
- 四、Spring AOP 的实现原理:代理是怎么工作的(#四spring-aop-的实现原理代理是怎么工作的)
- 五、什么是 Spring 事务(#五什么是-spring-事务)
- 六、事务的 ACID 四大特性(#六事务的-acid-四大特性)
- 七、\`@Transactional\` 的基本使用(#七transactional-的基本使用)
- 八、Spring 事务传播行为(#八spring-事务传播行为)
- 九、事务隔离级别(#九事务隔离级别)
- 十、事务失效的 12 个常见场景(#十事务失效的-12-个常见场景)
- 十一、Spring AOP 与 Spring 事务的关系(#十一spring-aop-与-spring-事务的关系)
- 十二、实战案例:电商下单事务(#十二实战案例电商下单事务)
- 十三、实际开发中的最佳实践(#十三实际开发中的最佳实践)
- 十四、总结与排查清单(#十四总结与排查清单)](#前言那个加了注解却没回滚的下午) - 一、开篇:为什么必须学 AOP 和事务 - 二、什么是 Spring AOP - 三、AOP 核心概念:一个切面到底由什么组成 - 四、Spring AOP 的实现原理:代理是怎么工作的 - 五、什么是 Spring 事务 - 六、事务的 ACID 四大特性 - 七、
@Transactional的基本使用 - 八、Spring 事务传播行为 - 九、事务隔离级别 - 十、事务失效的 12 个常见场景 - 十一、Spring AOP 与 Spring 事务的关系 - 十二、实战案例:电商下单事务 - 十三、实际开发中的最佳实践 - 十四、总结与排查清单)
前言:那个「加了注解却没回滚」的下午
先讲一个真实发生过的场景。
某天下午,测试环境报了个 bug:「用户点了取消订单,订单状态没变,但优惠券已经还回去了。」
开发同学第一反应是:事务没生效啊,加个 `@Transactional` 不就完了?
于是他在 `cancelOrder` 方法上补上了注解,重新发版------bug 依旧。
他不知道的是,那行注解加在了一个 `private` 方法上。
再过两天,同样的问题换个姿势复发:这次方法签名没问题,但方法内部 `try-catch` 把异常吞了,事务「看起来生效了」,可数据库里的数据已经写了一半。
这两个 bug 的根因是同一个:你以为 `@Transactional` 是一句「魔法咒语」,但它其实是一套有明确前提的机制。前提不满足,它会安静地不干活------不报错,不告警,只是让你的数据悄悄错掉。
而这套机制的地基,正是Spring AOP。
>一句话:AOP 是「在不改你业务代码的前提下,偷偷给方法套一层壳」的技术;而 `@Transactional` 就是 Spring 用这层壳实现出来的「要么全成、要么全败」的保险机制。
本文用费曼学习法讲清这两件事:先给结论,再讲原理,最后给能跑的代码。读完后你将能够:
-
说清楚 AOP 解决了什么问题,以及它和面向对象编程的分工;
-
画出 AOP 的 7 个核心概念,并说出每个概念在代码里长什么样;
-
解释 Spring AOP 为什么用代理,以及 JDK 代理和 CGLIB 的区别;
-
解释「同类内部方法调用」为什么会绕过代理;
-
完整讲出 ACID 四个特性,并举出「少了它会怎样」的例子;
-
熟练使用 `@Transactional`,知道事务边界该划在哪一层;
-
区分 `REQUIRED` / `REQUIRES_NEW` / `NESTED` 的差别与代价;
-
说清楚 5 种隔离级别分别防住了什么;
9.背出 12 个事务失效场景 ,遇到问题知道怎么查;
- 设计一个不会被并发打穿的下单流程。
本文所有代码都来自可运行的示例项目 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 就像一个门卫,可以:
先检查(前置逻辑)
决定「放行」还是「拦下」
放行 → 目标方法执行
拿到结果做后续处理(后置逻辑)
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 事务没有发明新的事务机制,它做的是三件事:
-
用 `@Transactional` 代替手写的 `BEGIN/COMMIT/ROLLBACK`;
-
用 AOP 代理 自动决定在方法前后提交还是回滚;
-
提供传播行为、隔离级别、只读优化等高级控制。
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 层最合适?
- 事务边界 = 业务边界 。「创建订单」这个业务用例涉及 3 张表、5 个步骤,
它就是一个完整事务。Service 方法正好对应一个业务用例。
- Service 层的粒度适中 。一个 Service 方法通常几毫秒到几百毫秒,
是持有数据库连接的理想时长。
- Controller 关心的是「协议」 (HTTP 状态码、JSON 格式),
Service 关心的是「业务原子性」。职责要分开。
7.4 为什么不建议把事务加在 Controller 层?
java
//不推荐
@RestController
public class OrderController {
@Transactional(rollbackFor = Exception.class) // ← 别这么写
@PostMapping("/orders")
public ApiResponse<Order> 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.哪些操作必须一起成功或一起失败? → 决定事务的范围 (切点)
-
这个范围对应哪个方法? → 决定在哪加注解 (Service 层)
-
失败时怎么处理? → 决定回滚规则和异常处理
> 一句话 :
> 事务边界就是 AOP 的切点在业务语义上的具体化。
> 理解了 AOP,「事务该加在哪」就不需要死记了。
十二、实战案例:电商下单事务
12.1 完整的业务流程

12.2 事务该放在哪一层?

为什么两层都加事务?
因为 `OrderService#createOrder` 可能被其他入口 调用(MQ 消费者、定时任务、RPC 接口),
它自己就该是一个完整的事务。而当它被 `OrderEntryService` 调用时,
`REQUIRED` 让它加入 外层事务------两者合成一个大事务。
> 这是「事务边界」设计的经典模式 :
>每个原子业务方法自带事务(保证独立可用),
> 编排层用 `REQUIRED` 把它们合成一个更大的事务(保证整体原子性)。
12.3 哪些操作应该放在同一个事务中?

12.4 哪些操作不适合直接放在本地数据库事务中?
判断标准:三个问题里有任意一个答「是」,就别放在事务里。
1**. 它是远程调用吗?** (HTTP / RPC / MQ)
-
它很慢吗? (超过几百毫秒)
-
它失败后能自动重建吗?(缓存、索引、报表、通知)
这类操作的正确处理方式------最终一致性:
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 发送失败 → 可以重试 / 靠本地消息表兜底
还有一种更可靠的方式------本地消息表:
在同一个【本地】事务里:
写订单
写消息表(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`」进阶到了「能设计出正确的事务边界」。