Spring 事务核心知识点全梳理(编程式 + 声明式 + 注解详解)

一、事务的基础概念

1. 什么是事务?

事务是一组不可分割的操作集合,这组操作要么 “同时成功”,要么 “同时失败”(即 “原子性”)。比如转账时 “扣 A 账户钱 + 加 B 账户钱”,这两步必须作为一个整体执行,不能只成功其中一步

2. 为什么需要事务?

解决 “操作部分成功、部分失败” 的数据不一致问题

  • 例子 1(转账):如果扣 A 账户成功,但加 B 账户失败,A 的钱会 “凭空消失”;
  • 例子 2(秒杀):如果下单成功,但库存扣减失败,会出现 “超卖”。事务能保证这组操作 “要么全成,要么全败”,避免数据错乱。
3. 事务的核心操作(以 MySQL 为例)

事务有 3 个关键步骤:

  1. 开启事务start transaction / begin(操作前执行);
  2. 提交事务commit(所有操作都成功时执行);
  3. 回滚事务rollback(任意操作失败时执行,恢复到操作前状态)。

二、Spring 编程式事务的具体实现与效果演示

2.1Spring 编程式事务的核心组件

Spring Boot 提供了两个关键对象来手动管理事务:

  1. DataSourceTransactionManager(事务管理器)

    • 作用:负责开启事务、提交事务、回滚事务的具体操作。
    • 可以理解为 “事务的执行者”,需要依赖数据库连接(数据源)来操作事务。
  2. TransactionDefinition(事务属性)

    • 作用:定义事务的规则(比如隔离级别、超时时间、传播行为等)。
    • 在开启事务时,需要将它传递给事务管理器,从而得到一个具体的事务状态(TransactionStatus)。

2.2操作步骤(手动写代码控制)

在业务流程中,手动完成 3 步:

  1. 开启事务:通过事务管理器 + 事务属性,获取事务状态(TransactionStatus);
  2. 执行业务:调用业务方法(比如用户注册);
  3. 提交 / 回滚事务:业务成功则提交,失败则回滚。

2.3代码逻辑(以用户注册为例)

// 1. 注入事务管理器、事务属性、业务Service @Autowired private DataSourceTransactionManager dataSourceTransactionManager; @Autowired private TransactionDefinition transactionDefinition; @Autowired private UserService userService; // 2. 手动控制事务 public String registry(String name, String password) { // 步骤1:开启事务 TransactionStatus status = dataSourceTransactionManager.getTransaction(transactionDefinition); try { // 步骤2:执行业务(用户注册) userService.registryUser(name, password); // 步骤3:业务成功 → 提交事务 dataSourceTransactionManager.commit(status); return "注册成功"; } catch (Exception e) { // 步骤3:业务失败 → 回滚事务 dataSourceTransactionManager.rollback(status); return "注册失败"; } }

效果演示

  • 提交事务:业务执行成功 → 数据库插入用户数据;
  • 回滚事务:业务执行失败(比如抛异常)→ 数据库无新增数据。

最后:编程式事务的缺点

手动写 “开启 / 提交 / 回滚” 代码太繁琐,所以实际开发更常用声明式事务(@Transactional注解)替代。

三、Spring 声明式事务(@Transactional)的核心用法与细节

3.1声明式事务的基础使用

实现声明式事务只需要两步

1. 添加依赖

pom.xml中引入 Spring 事务的依赖(Spring Boot 项目一般会自动引入,也可以显式添加):

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency>
2. 加@Transactional注解

在需要事务的方法 / 类上添加@Transactional,Spring 会自动管理事务:

  • 方法执行前:自动开启事务;
  • 方法执行成功:自动提交事务;
  • 方法执行中抛未捕获的异常:自动回滚事务。
代码示例(用户注册)
@RestController @RequestMapping("/trans") public class TransactionalController { @Autowired private UserService userService; // 加@Transactional,自动管理事务 @Transactional @RequestMapping("/registry") public String registry(String name, String password) { // 执行业务(插入用户) userService.registryUser(name, password); return "注册成功"; } }

3.2异常与事务回滚的规则

@Transactional的回滚逻辑是 “未捕获的异常才会触发回滚”,这是核心细节:

1. 未捕获异常 → 事务回滚

比如在方法中手动抛异常(int a = 10/0),且不捕获:

@Transactional @RequestMapping("/registry") public String registry(String name, String password) { userService.registryUser(name, password); log.info("用户数据插入成功"); // 强制抛异常(未捕获) int a = 10/0; return "注册成功"; }
  • 效果:日志显示 “插入成功”,但数据库没有新增数据(事务回滚)。
2. 捕获异常 → 事务提交

如果异常被try-catch捕获,Spring 会认为方法 “执行成功”,自动提交事务:

@Transactional @RequestMapping("/registry") public String registry(String name, String password) { userService.registryUser(name, password); log.info("用户数据插入成功"); try { // 捕获异常 int a = 10/0; } catch (Exception e) { e.printStackTrace(); } return "注册成功"; }
  • 效果:程序报错,但数据库会新增数据(事务提交)。
3. 捕获后想回滚?两种方式

如果想捕获异常后仍回滚事务,可以:

  • 方式 1:重新抛出异常catch中把异常重新抛出去,让 Spring 感知到异常:

@Transactional public String registry(...) { try { int a = 10/0; } catch (Exception e) { e.printStackTrace(); throw e; // 重新抛异常,触发回滚 } return "注册成功"; }

方式 2:手动回滚事务通过TransactionAspectSupport获取当前事务,手动标记为 “需要回滚”:

@Transactional public String registry(...) { try { int a = 10/0; } catch (Exception e) { e.printStackTrace(); // 手动标记事务回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return "注册成功"; }

3.3@Transactional的作用范围

@Transactional可以修饰方法

  • 修饰方法:仅对public方法生效(非 public 方法加了也不报错,但事务不生效);
  • 修饰:对类中所有public方法生效。
注意:事务的 “正确位置”

案例中把@Transactional加在 Controller 层,只是为了演示方便;实际开发中,事务应该加在 Service 层(因为 Service 层是业务逻辑层,一个业务可能包含多个数据操作,事务需要覆盖这些操作)。

再次强调:

@Transactional替代了编程式的 “手动开启 / 提交 / 回滚”,而异常是否被捕获是事务是否回滚的关键开关:

  • 没捕获异常 → Spring 感知到异常 → 自动回滚;
  • 主动捕获异常 → Spring 认为方法 “执行成功” → 自动提交。
  • 如果业务需要 “捕获异常 + 事务回滚”,不能只写try-catch,必须用这两种方式之一:

  1. catch里重新抛异常(让 Spring 感知);
  2. TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记回滚。

3.4注意:

捕获异常” 和 “事务回滚” 是两个独立的诉求,只是 Spring 的默认规则把它们绑在了一起

1.你捕获异常的目的 ≠ Spring 判断事务的规则

你捕获异常的目的:

  • 是为了友好处理异常(比如返回 “注册失败,请重试”,而不是直接抛 500 错误让系统崩掉);
  • 不是为了 “让事务提交”,只是 Spring 默认把 “捕获异常 = 事务成功”,这是规则问题,不是你的业务意图。

Spring 的默认规则:

  • 它只看 “方法有没有抛未捕获的异常”—— 没抛 = 业务成功 = 提交事务;抛了 = 业务失败 = 回滚事务;
  • 它不管你捕获异常是为了啥,只认 “异常是否逃逸出方法” 这个信号。
2.举个实际场景:为什么 “捕获异常还需要回滚”

比如用户注册场景:

@Transactional public String registry(String name, String password) { try { // 操作1:插入用户 userInfoMapper.insert(name, password); // 操作2:插入日志(模拟失败,抛异常) logService.insertLog(name, "注册"); // 这里抛SQLException } catch (Exception e) { // 你的诉求:捕获异常,返回友好提示,不让系统崩 log.error("注册失败", e); return "注册失败,请稍后重试"; // Spring的默认行为:捕获了异常 → 事务提交 → 数据库里有用户、无日志(数据不一致) } }
  • 你的预期:注册失败 → 数据库里既没有用户,也没有日志(事务回滚);
  • 实际结果:数据库里有用户、无日志(事务提交了),这就是 bug—— 用户没注册成功,但数据已经插进去了。

这时候就需要:既捕获异常返回友好提示,又让事务回滚(这才是符合业务逻辑的)。

3.怎么实现 “捕获异常 + 事务回滚”(解决你的核心诉求)

方法 1:捕获后重新抛异常(让 Spring 感知):

@Transactional public String registry(String name, String password) { try { userInfoMapper.insert(name, password); logService.insertLog(name, "注册"); } catch (Exception e) { log.error("注册失败", e); throw new RuntimeException(e); // 重新抛异常,Spring感知到 → 回滚事务 } return "注册成功"; }
  • 效果:既捕获了异常(记录日志),又重新抛出去让 Spring 回滚,同时可以在 Controller 层统一拦截异常,返回友好提示。

方法 2:手动标记回滚(更灵活):

@Transactional public String registry(String name, String password) { try { userInfoMapper.insert(name, password); logService.insertLog(name, "注册"); } catch (Exception e) { log.error("注册失败", e); // 手动告诉Spring:事务要回滚(不管有没有捕获异常) TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return "注册失败,请稍后重试"; } return "注册成功"; }
  • 效果:不用抛异常,既返回友好提示,又让事务回滚,完美兼顾两个诉求。
总结
  1. 你捕获异常是为了友好处理(不让系统崩、给用户提示),这是合理的;
  2. 但业务上 “操作失败就该回滚”(比如注册失败就不能留脏数据),这也是合理的;
  3. Spring 的默认规则(捕获异常 = 事务提交)是 “一刀切”,所以需要我们手动调整,让 “捕获异常” 和 “事务回滚” 两个诉求都满足。

简单说:捕获异常是为了 “用户体验 / 系统稳定性”,回滚是为了 “数据一致性”,两者都要要,所以才需要在捕获后手动触发回滚

四、@Transactional详解

Spring 事务的默认回滚规则

@Transactional默认只对 “运行时异常(RuntimeException)” 和 “错误(Error)” 回滚,对 “非运行时异常(编译时异常,比如IOException)” 不回滚。

异常的继承关系:

Throwable ├─ Error(错误,如内存溢出) └─ Exception(异常) ├─ RuntimeException(运行时异常,如空指针、数组越界) └─ 非RuntimeException(编译时异常,如IOException、SQLException)
默认规则的 “坑”:非运行时异常不回滚

比如代码中抛IOException(非运行时异常):

@Transactional // 无rollbackFor属性 public String r2(String name, String password) throws IOException { userService.registryUser(name, password); // 插入用户 throw new IOException(); // 抛非运行时异常 }
  • 结果:程序报错,但数据库新增了用户数据(事务提交了);
  • 原因:IOException是 “非运行时异常”,不在 Spring 默认回滚范围内。

4.1rollbackFor

通过rollbackFor属性,可以强制让指定的异常(包括非运行时异常)触发回滚

1. 指定单个异常

比如让IOException触发回滚:

@Transactional(rollbackFor = IOException.class) public String r2(...) throws IOException { userService.registryUser(name, password); throw new IOException(); }
2. 指定所有异常(最常用)

如果想让所有异常都回滚,直接指定Exception.class(所有异常都是它的子类):

@Transactional(rollbackFor = Exception.class) public String r2(...) throws IOException { userService.registryUser(name, password); throw new IOException(); }
  • 效果:程序报错后,数据库无新增数据(事务回滚)。
总结
  • rollbackFor的核心作用:打破 Spring 的默认规则,让非运行时异常也能触发事务回滚
  • 实际开发中,几乎都会加rollbackFor = Exception.class,确保 “只要有异常,事务就回滚”,避免数据不一致。

4.2事务隔离级别

1. MySQL事务隔离级别(回顾)

事务隔离级别是为了解决多个事务并发时的 “数据一致性问题”(脏读、不可重复读、幻读),SQL 标准定义了 4 种级别,MySQL 全部支持:

场景是否要写 throw new触发哪个异常处理方法
主动抛自定义细分异常(比如 UsernameDuplicateException)全局类里对应的子类处理方法
主动抛自定义父类异常(BusinessException)全局类里的父类兜底方法
没抛自定义异常,系统自动抛异常(比如空指针)全局类里的系统异常兜底方法(Exception.class)
漏写自定义细分异常的处理方法是(抛子类)全局类里的父类兜底方法
核心问题解释
  • 脏读(读了 “没敲定、还能撤回” 的东西,白读了):读到其他事务 “未提交且可能回滚” 的数据(比如 A 扣钱没提交,B 读到扣后金额,A 回滚后 B 的数据错了);
  • 不可重复读(同一件事,重复做,结果不一样):同一事务中,多次查同一数据结果不同(比如 A 查余额 100,B 改余额 200 并提交,A 再查变成 200);
  • 幻读(重复数同一类东西,数量变了,像幻觉):同一事务中,多次查 “符合条件的数据集” 行数不同(比如 A 查 “年龄 < 20” 有 2 个用户,B 新增 1 个并提交,A 再查变成 3 个)。
2. MySQL 默认隔离级别

MySQL 默认是可重复读(REPEATABLE READ),可以用 SQL 查询:

-- 查全局+当前连接的隔离级别 select @@global.tx_isolation, @@tx_isolation;

3.Spring事务隔离级别

Spring 对 MySQL 的隔离级别做了封装,提供 5 种选项(通过@Transactionalisolation属性配置):

Spring 隔离级别常量对应 MySQL 隔离级别含义
Isolation.DEFAULT数据库默认(MySQL 是可重复读)跟随数据库的隔离级别(日常开发最常用)
Isolation.READ_UNCOMMITTED读未提交对应 SQL 标准的 READ UNCOMMITTED
Isolation.READ_COMMITTED读已提交对应 SQL 标准的 READ COMMITTED
Isolation.REPEATABLE_READ可重复读对应 SQL 标准的 REPEATABLE READ
Isolation.SERIALIZABLE串行化对应 SQL 标准的 SERIALIZABLE

这些脏读、不可重复读、幻读,本质就是没做好事务隔离导致的 —— 多个事务 “挤在一起” 并发执行,互相干扰,才会出现这些问题。

事务隔离级别,其实就是超市给你和店员定的 “规矩”:

  • 规矩松(读未提交):店员刚碰货你就能看 → 脏读、不可重复读、幻读都有;
  • 规矩严一点(读已提交):店员摆好确认了,你才能看 → 解决脏读,但还会不可重复读、幻读;
  • 规矩更严(可重复读):你开始数商品后,店员不能改价格 / 加货 → 解决脏读、不可重复读,只剩幻读;
  • 规矩最严(串行化):你数完所有东西,店员才能动货架 → 啥问题都没,但超市效率极低(排队等你)。

事务隔离就是 “给并发的事务划界限”,界限越清,数据越准,就是慢点;界限越松,数据越容易乱,就是快点。

Spring 中配置隔离级别

@Transactional中通过isolation属性指定,比如配置为 “读已提交”:

@Transactional( rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED // 配置隔离级别 ) public String r3(String name, String password) { userService.registryUser(name, password); return "注册成功"; }
总结
  1. 隔离级别越高,数据越一致,但并发性能越低;
  2. 日常开发用Isolation.DEFAULT(跟随 MySQL 默认的 “可重复读”)即可,特殊场景(比如金融)才需要调整;
  3. Spring 的隔离级别只是 “调用数据库的隔离级别”,本质还是依赖 MySQL 的实现。
注意:
1.隔离级别能不能脱离事务注解生效?

绝对不能!事务隔离级别是 “事务的属性”,就像 “调料是做菜的属性”—— 你没做菜(没加@Transactional),放调料(配隔离级别)完全没用。

通俗说:

  • @Transactional是 “开启事务” 的开关,隔离级别是 “这个事务的规矩”;
  • 开关没开(没加注解),规矩(隔离级别)就没地方生效,写了也白写。
2.只给单个方法加隔离级别,作用范围是啥?

只对这个方法里的事务生效,其他方法完全不受影响。

举个超市的例子:你给 “查牛奶价格” 的方法(事务 A)配了 “读已提交” 隔离级别,给 “数薯片” 的方法(事务 B)用默认隔离级别:

  • 事务 A 执行时,按 “读已提交” 的规矩来(只能看店员摆好的价格);
  • 事务 B 执行时,按 MySQL 默认的 “可重复读” 来(数完薯片后,店员暂时不能加新薯片);
  • 两个方法的规矩互不干扰,各自管各自的事务。
3.核心结论
  • 日常开发直接省略隔离级别配置,用默认的(Isolation.DEFAULT)就行,不用多写;
  • 单个方法特殊需求:只在这个方法的@Transactional里加isolation = 目标级别,只影响这个方法的事务;
  • 整个 Service 统一规则:在 Service 类上的@Transactional里加隔离级别,这个类里所有加了事务的方法都会继承这个级别(方法级别的配置会覆盖类级别的)。
4.不加事务注解 = 没有事务 ≠ “用默认隔离级别”隔离级别是 “事务的属性”,没有事务,就谈不上 “用什么级别”—— 这就像 “没有车,就谈不上车的颜色是默认白色还是红色”。
5.为啥 “不加事务注解,就不是默认可重复读”?

MySQL 的 “默认隔离级别是可重复读”,指的是:当你开启一个事务时,这个事务默认用可重复读规则。但如果你没加@Transactional,你的代码根本不会开启 “一个事务”—— 而是把每个 SQL 操作当成 “独立的迷你事务”(MySQL 默认是autocommit=1,执行一条 SQL 就自动提交一次)。

举个超市的例子:

  • 加注解:你 “数商品” 的整个过程被包装成 “一个事务”,按 “可重复读” 规矩来(数的时候店员不能改);
  • 不加注解:你数 1 瓶牛奶(执行 1 条 SQL,自动提交)、数 1 包薯片(又执行 1 条 SQL,又自动提交)—— 这是两个独立的 “迷你操作”,没有 “整个数商品的事务”,自然谈不上 “用可重复读级别”。

不加事务注解,你的操作完全没有 “防护”,别人的操作想怎么干扰就怎么干扰

举例子:
// 无事务注解 public int countGoods() { int milk = milkMapper.count(); // 第一步:数牛奶,查到10瓶 // 这时候店员(另一个事务)偷偷拿走2瓶牛奶、加了3包薯片并提交 int chip = chipMapper.count(); // 第二步:数薯片,查到15包 return milk + chip; // 结果:10+15=25,但实际牛奶只剩8瓶,总数该是23 }
  • 你的两步操作之间,别人想改就改、想删就删,完全无遮挡
  • 脏读、不可重复读、幻读会全找上门,数据算出来全是错的。
加了事务(默认可重复读)= 有了 “防护罩”

给方法加上@Transactional后:

// 加事务注解,默认可重复读 @Transactional(rollbackFor = Exception.class) public int countGoods() { int milk = milkMapper.count(); // 第一步:数牛奶,查到10瓶 // 这时候店员想改?没用!你的事务没结束,他的修改对你“不可见” int chip = chipMapper.count(); // 第二步:数薯片,查到12包 return milk + chip; // 结果:10+12=22,和你开始数时的真实数据一致 }
  • 你开始数商品的那一刻,相当于给货架套了个 “防护罩”;
  • 只要你的事务没结束,别人的新增 / 修改 / 删除,都干扰不到你查到的数据。
总结
  • 不加事务:无防护,别人的操作随时干扰你,查的数据全是 “实时乱数”;
  • 加了事务:有防护(默认可重复读),事务期间别人的操作对你 “隐身”,保证你查的数据前后一致。

简单说:事务就是给你的一串操作 “画个圈”,圈里的操作要么全成、要么全败,而且圈里查的数据不会被圈外人干扰 —— 没这个圈,你就是裸奔

6.加了事务(可重复读),别人能操作数据,操作也能生效—— 只是这个生效的结果,对你当前的事务 “隐身”

隔离不是 “禁止别人干活”,而是 “不让你看见别人干的活”。

分两步讲清楚:
1. 别人的操作:正常执行 + 正常生效(对他自己、对其他新事务可见)

比如你(事务 A)加了@Transactional,正在数货架上的牛奶(10 瓶):

  • 店员(事务 B)过来拿走 2 瓶牛奶,扫码确认(提交事务);
  • 对店员来说:操作成功,系统里牛奶数量变成 8 瓶;
  • 对刚过来的另一个顾客(事务 C):他数牛奶,看到的就是 8 瓶(因为他的事务是新开启的)。
2. 对你的事务 A:店员的修改 “隐身”(你看到的始终是 10 瓶)

你的事务 A 没结束之前,不管店员怎么改,你再数牛奶,还是 10 瓶 ——这就是 “可重复读” 的核心:你事务内的读取结果,不受外部已提交事务的影响

不是店员不能改,是你 “看不见” 他改的结果。

再对比 “禁止操作” 的情况(串行化级别)

只有隔离级别调到串行化时,才会变成 “你操作时,别人不能操作”:

  • 你(事务 A)开始数牛奶,货架就被 “锁死” 了;
  • 店员(事务 B)想拿牛奶?不行!必须等你数完、事务 A 结束,他才能碰货架;
  • 这种情况才是 “禁止别人操作”,但代价是超市效率暴跌(所有人排队等你)。
  • 可重复读:你和店员各干各的,互不打扰你的视线;
  • 串行化:你先干,店员站旁边等你干完再动手。

4.3Spring事务传播机制

什么是事务传播机制?

事务传播机制解决的是:多个带事务的方法互相调用时,事务该 “共享” 还是 “独立”。比如 A 方法(带事务)调用 B 方法(带事务),B 是 “加入 A 的事务” 还是 “自己开新事务”?这就是传播机制要管的事。

此时A部⻔有⼀项⼯作,需要B部⻔的⽀援,此时B部⻔是直接使⽤A部⻔的⽂档,还是新建⼀个⽂档呢?事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题

⽽事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题

1.事务的传播机制有哪些?

Spring 定义了 7 种传播机制(通过@Transactional(propagation = 机制名)配置),核心是 3 类逻辑:

机制名通俗理解(用 “租房” 类比)核心规则
REQUIRED(默认)你有房我就住你的,没房咱俩一起买有事务则加入,无则新建
SUPPORTS你有房我就住,没房我就租房有事务则加入,无则非事务运行
MANDATORY必须住你的房,没房我就不活了有事务则加入,无则抛异常
REQUIRES_NEW不管你有没有房,我必须买新房强制新建独立事务,挂起原有事务
NOT_SUPPORTED不管你有没有房,我必须租房非事务运行,挂起原有事务
NEVER我讨厌房子,有房我就不活了非事务运行,有事务则抛异常
NESTED你有房,我就在你家次卧住(带独立房间)有事务则嵌套(带保存点),无则同 REQUIRED一样
2.Spring事务传播机制使⽤和各种场景演⽰
2.1REQUIRED(加⼊事务)

核心逻辑:被调用方法会加入调用方的事务,共用同一个事务(同生共死)

// Controller层:开启事务 @RestController @RequestMapping("/propaga") public class PropagationController { @Autowired private UserService userService; @Autowired private LogService logService; // 配置REQUIRED(默认,可省略) @Transactional(propagation = Propagation.REQUIRED) @RequestMapping("/p1") public String r3(String name, String password) { // 调用UserService(注册用户) userService.registryUser(name, password); // 调用LogService(记录日志,故意抛异常) logService.insertLog(name, "用户注册"); return "r3"; } } // UserService层:加入事务 @Service public class UserService { @Autowired private UserInfoMapper userInfoMapper; @Transactional(propagation = Propagation.REQUIRED) public void registryUser(String name, String password) { // 插入用户数据 userInfoMapper.insert(name, password); } } // LogService层:加入事务 @Service public class LogService { @Autowired private LogInfoMapper logInfoMapper; @Transactional(propagation = Propagation.REQUIRED) public void insertLog(String name, String op) { // 故意抛异常 int a = 10 / 0; // 插入日志数据(不会执行到这步) logInfoMapper.insertLog(name, op); } }
运行结果

数据库中用户表和日志表都没有插入任何数据

原因
  1. Controller 的p1方法开启了一个事务;
  2. UserServiceLogService加入这个事务,三者共用同一个事务;
  3. LogService抛异常,整个事务回滚,所以用户和日志的数据都被撤回。
2.2REQUIRES_NEW(新建事务)

核心逻辑:被调用方法会新建独立事务,和调用方事务互不干扰(各自提交 / 回滚)。

只需修改UserServiceLogServicepropagation属性:

// UserService层:新建独立事务 @Service public class UserService { @Autowired private UserInfoMapper userInfoMapper; @Transactional(propagation = Propagation.REQUIRES_NEW) public void registryUser(String name, String password) { userInfoMapper.insert(name, password); } } // LogService层:新建独立事务 @Service public class LogService { @Autowired private LogInfoMapper logInfoMapper; @Transactional(propagation = Propagation.REQUIRES_NEW) public void insertLog(String name, String op) { int a = 10 / 0; // 抛异常 logInfoMapper.insertLog(name, op); } }
运行结果

数据库中用户表插入成功,日志表插入失败

原因
  1. Controller 的p1方法开启事务后,UserService新建独立事务,执行完直接提交(用户数据入库);
  2. LogService新建另一个独立事务,抛异常后回滚(日志数据不入库);
  3. 两个 Service 的事务是 “独立的”,一个失败不影响另一个。
2.3NEVER(不⽀持当前事务,抛异常)

核心逻辑:被调用方法不允许当前存在事务,有事务则抛异常。

代码实现

修改UserServicepropagation属性:

@Service public class UserService { @Autowired private UserInfoMapper userInfoMapper; // 配置NEVER @Transactional(propagation = Propagation.NEVER) public void registryUser(String name, String password) { userInfoMapper.insert(name, password); } }
运行结果

程序直接报错,数据库没有任何数据插入

原因

Controller 的p1方法已开启事务,而UserServiceNEVER要求 “当前不能有事务”,所以调用时直接抛异常,流程终止。

2.4NESTED(嵌套事务)

核心逻辑:被调用方法会嵌套在调用方事务中(带保存点),可实现 “子事务单独回滚”。

代码实现

修改UserServiceLogServicepropagation属性:

// UserService层:嵌套事务 @Service public class UserService { @Autowired private UserInfoMapper userInfoMapper; @Transactional(propagation = Propagation.NESTED) public void registryUser(String name, String password) { userInfoMapper.insert(name, password); } } // LogService层:嵌套事务(默认情况) @Service public class LogService { @Autowired private LogInfoMapper logInfoMapper; @Transactional(propagation = Propagation.NESTED) public void insertLog(String name, String op) { int a = 10 / 0; // 抛异常 logInfoMapper.insertLog(name, op); } }
运行结果(默认情况)

数据库中用户表和日志表都没有插入数据

原因
  1. Controller 的p1方法是 “父事务”;
  2. UserServiceLogService是 “子事务”,嵌套在父事务中;
  3. LogService抛异常,子事务回滚时会触发父事务也回滚,所以所有数据都被撤回。
代码实现(子事务单独回滚)

LogService中捕获异常,并手动标记子事务回滚:

@Service public class LogService { @Autowired private LogInfoMapper logInfoMapper; @Transactional(propagation = Propagation.NESTED) public void insertLog(String name, String op) { try { int a = 10 / 0; logInfoMapper.insertLog(name, op); } catch (Exception e) { // 手动标记子事务回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } } }

据库中用户表插入成功,日志表插入失败

原因

嵌套事务会在父事务中创建 “保存点”,子事务回滚时只回滚到保存点,不会影响父事务已执行的内容(用户数据已入库)。

总结

  • REQUIRED:适合 “业务强关联”(必须同成功 / 失败);
  • REQUIRES_NEW:适合 “业务弱关联”(一个失败不影响另一个);
  • NESTED:适合 “需要局部回滚” 的场景(子事务失败,父事务可继续)。
2.5NESTED和REQUIRED有什么区别?
1.核心差异:是否支持 “局部事务回滚”
传播机制事务关系局部回滚能力核心原理
NESTED子事务嵌套在父事务中(带保存点)支持:子事务回滚不影响父事务已执行内容子事务对应父事务的 “保存点”,回滚仅到当前保存点
REQUIRED所有方法共用同一个事务不支持:一个方法回滚,整个事务全回滚无保存点,事务是 “整体”,回滚则全部撤回
代码演示与结果对比

1. NESTED(嵌套事务)

代码(LogService 捕获异常并标记回滚):

@Service public class LogService { @Autowired private LogInfoMapper logInfoMapper; @Transactional(propagation = Propagation.NESTED) public void insertLog(String name, String op) { try { int a = 10 / 0; // 抛异常 } catch (Exception e) { // 标记当前子事务回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } logInfoMapper.insertLog(name, op); } }

结果:用户表数据插入成功,日志表数据插入失败。原因:子事务(LogService)回滚时,仅回滚自身,父事务(UserService)的操作不受影响。

2. REQUIRED(加入事务)

代码(将 NESTED 改为 REQUIRED):

@Service public class LogService { @Autowired private LogInfoMapper logInfoMapper; @Transactional(propagation = Propagation.REQUIRED) public void insertLog(String name, String op) { try { int a = 10 / 0; // 抛异常 } catch (Exception e) { // 标记事务回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } logInfoMapper.insertLog(name, op); } }

结果:用户表和日志表数据都插入失败。原因:所有方法共用一个事务,标记回滚后,整个事务的所有操作都被撤回。

总结
  • 当事务全部成功时,NESTED 与 REQUIRED 效果一致;
  • 当事务部分失败时,NESTED 可实现 “局部回滚”,REQUIRED 会 “整体回滚”;
  • NESTED 的核心是 “保存点(savepoint)”,这是它能局部回滚的关键。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1160224.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【源码 + 文档】SpringBoot+Vue 开发网上购物商城系统(附完整设计文档)

【源码 文档】SpringBootVue 开发网上购物商城系统&#xff08;附完整设计文档&#xff09; 哈喽&#xff0c;我是你们的技术干货博主&#xff5e;今天给大家分享一个基于 SpringBootVue 开发的网上购物商城系统&#xff0c;包含前端商城 后端管理系统&#xff0c;还附带了完…

OpenCode:开源AI代理如何改变你的编程工作流

OpenCode是开源AI编码代理工具&#xff0c;代表AI编程从"补全"到"对话"再到"代理"的演进。它采用终端优先设计&#xff0c;支持多会话并行、MCP生态扩展&#xff0c;强调隐私与可控性。文章详解了其核心优势&#xff1a;开源可控、终端优先工作流…

对 两种不同AI范式——Transformer 和 LSTM 进行解剖和对比

这不仅仅是两个模型的比较&#xff0c;更是两种AI范式的对话。一、 设计哲学&#xff1a;两种世界观维度LSTM的世界观Transformer的世界观核心隐喻时间的诗人&#xff1a;认为世界是动态的、连续的流。理解当下&#xff0c;必须回顾过去&#xff0c;记忆在时间中流淌和演变。空…

支持向量机 (SVM) 通俗解读

想象一下&#xff0c;你是一个老师&#xff0c;要把一群调皮的学生分成两队&#xff1a;一队爱踢足球的&#xff0c;一队爱打篮球的。你不想让他们混在一起打架&#xff0c;所以你需要在操场上画一条线&#xff0c;把两队分开。但不是随便画一条&#xff0c;你要画得尽可能宽敞…

【正点原子STM32MP157学习篇】A7和M4联合调试(通过STM32CubeIDE)

文章目录1 概要2 Remoteproc 框架简介2.1 Remoteproc 框架2.2 实验准备2.2.1 硬件连接2.2.2 启动 Linux 操作系统3 使用 STM32CubeIDE 进行调试3.1 操作步骤3.2 fw_cortex_m4.sh 脚本分析1 概要 本章节将介绍如何A7和M4进行联合调试&#xff0c;我们在分析 STM32CubeMP1 固件包…

提示工程架构师别再等!Agentic AI的3大市场优势,已经让同行抢先一步了

提示工程架构师别再等&#xff01;Agentic AI的3大市场优势&#xff0c;已经让同行抢先一步了关键词&#xff1a;Agentic AI 提示工程 自主决策 工具协同 商业效率 摘要&#xff1a;当你还在为“如何写更好的提示”绞尽脑汁时&#xff0c;同行已经用Agentic AI&#xff08;智能…

【MIMO OFDM】小波变换MIMO OFDM通信仿真【含Matlab源码 14928期】

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;Matlab领域博客之家&#x1f49e;&…

深度测评8个AI论文工具:本科生毕业论文写作全解析

深度测评8个AI论文工具&#xff1a;本科生毕业论文写作全解析 2026年AI论文工具测评&#xff1a;为何需要一份精准指南 随着人工智能技术的不断进步&#xff0c;越来越多的本科生开始依赖AI论文工具来提升写作效率和质量。然而&#xff0c;面对市场上琳琅满目的选择&#xff0c…

如何用云服务器搭建PUBG服务器?

云服务器搭建PUBG服务器完整指南一、服务器配置要求硬件配置推荐根据PUBG游戏的性能需求&#xff0c;建议选择以下配置&#xff1a;最低配置&#xff1a;CPU&#xff1a;Intel Core i5-4430 / AMD FX-6300内存&#xff1a;8GB RAM存储&#xff1a;50GB可用空间&#xff08;推荐…

租赁Anritsu MT8852B蓝牙测试仪

收到&#xff01;&#x1f44d; 我来帮你梳理 Anritsu MT8852B蓝牙测试仪 的核心参数与典型应用场景&#xff0c;接下来会从功能、支持标准到实际用途全面展开。 该设备支持蓝牙BR/EDR/BLE多种模式的射频与音频测试&#xff0c;并兼容最新蓝牙5.x标准&#xff0c;广泛用于研发验…

Excel VBA 编程基础学习笔记 · 第六章:字典技术 - 掌握高级数据映射与处理的王牌工具

目录 第6章&#xff1a;VBA与字典技术 1、字典技术的魅力 2、字典语法基础 3、字典实例(第1次与最后一次采购价提取) 4、字典实例&#xff08;多表求不重复值&#xff09; 5、字典实例(字典与数组经典结合) 6、字典实例(分类计算) 7、字典应用&#xff08;多列合并计算…

大模型产业价值链深度解析:从基础设施到应用层的全景指南

大模型产业呈现四层价值链架构&#xff0c;当前超60%价值集中于基础设施层&#xff0c;未来将向MaaS服务及应用层传递。应用层分为软件平台(轻量化高毛利)和硬件载体(重投资长周期)。大模型厂商可选择开源、闭源或混合策略。基础设施层整合IDC承载、硬件算力、系统集成及软件调…

Excel VBA 编程基础学习笔记 · 第四章:事件编程 - 打造智能交互的自动化引擎

目录 第四课&#xff1a;Excel VBA事件过程 1、EXCEL事件程序定义与作用 2、事件程序基础 3、工作表事件实例1&#xff08;自选计算与投票统计&#xff09; 4、工作表事件实例2&#xff08;状态栏地址与防工作表名更改) 5、工作表事件实例3&#xff08;自动列出工作表名与…

【MIMO通信】MIMO检测器(ZF、MMSE、SIC、ML)在瑞利衰落下的BER性能比较【含Matlab源码 14929期】含报告

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;Matlab领域博客之家&#x1f49e;&…

BUUCTF-[ACTF2020 新生赛]Upload

打开靶机后发现是一个文件上传的题目随便上传一个文件观察路径和返回的方式是什么发现只能上传jpg,png,gif的文件上传一个jpg文件发现页面会回显返回的路径既然有路径了我们就可以尝试上传一句话木马了构造一句话木马<?phpeval($_POST[rc]);?>导入字典观察过滤的哪些后…

幂等性设计指南:从数据库唯一索引到 Redis Token,如何防止用户“手抖”重复提交?

标签: #架构设计 #幂等性 #Redis #数据库 #分布式系统 #面试必问 💣 前言:即使前端置灰了按钮,后端也必须防重 很多初级开发者认为:“我在前端点击按钮后,把按钮置灰(Disabled)不就行了吗?” 太天真。 懂点技术的用户可以直接调 API 接口。 弱网环境下,请求发出去了…

发刊不用愁:paperxie 期刊论文功能,一键匹配普通刊 / 核心刊的学术标准

paperxie-免费查重复率aigc检测/开题报告/毕业论文/智能排版/文献综述/aippt https://www.paperxie.cn/ai/journalArticleshttps://www.paperxie.cn/ai/journalArticles 对于科研人员和学生来说&#xff0c;“期刊论文投稿” 从来不是 “写完文字” 这么简单 —— 普通刊要符…

PointMAE的代码配环境+运行

PointMAE的笔记 PointMAE的代码地址 1. 环境配置 我的cuda是115安装Pointnet2_PyTorch总是失败 所以我在隔离环境中创建了cuda113的环境 参考pip 隔离环境内 安装 cuda 113 不覆盖原有的全局 cuda 115 1.1 安装torch pip install torch1.12.1cu113 torchvision0.13.1cu113…

CMake:现代C/C++项目的构建中枢

CMake&#xff1a;现代C/C项目的构建中枢 引言&#xff1a;从构建混乱到标准化 想象你正在开发一个跨平台的C库&#xff0c;需要在Windows、Linux、macOS上都能构建。在CMake出现之前&#xff0c;这意味着&#xff1a;为Visual Studio编写.vcxproj文件为Linux编写复杂的Mak…

【MIMO通信】基于matlab MIMO检测器(ZF、MMSE、SIC、ML)在瑞利衰落下的BER性能比较【含Matlab源码 14929期】含报告

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到海神之光博客之家&#x1f49e;&#x1f49e;&#x1f49e;&#x1f49…