Spring Boot:影响事务回滚的几种情况

一、Controller 捕获异常导致事务失效

需求

我们有一个用户注册服务,注册时需要:

  1. 创建用户账户
  2. 分配初始积分
  3. 发送注册通知

这三个操作需要在同一个事务中执行,任何一步失败都要回滚。

错误示例:Controller 捕获异常导致事务失效

@RestController
@RequestMapping("/api/users")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/register")public ApiResponse registerUser(@RequestBody UserRegistrationRequest request) {try {// 调用服务层方法(带有 @Transactional 注解)userService.registerUser(request);return ApiResponse.success();} catch (Exception e) {// 捕获异常并返回自定义错误响应return ApiResponse.error("注册失败");}}
}@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate PointRepository pointRepository;@Autowiredprivate NotificationService notificationService;@Override@Transactionalpublic void registerUser(UserRegistrationRequest request) {// 1. 创建用户User user = new User();user.setUsername(request.getUsername());userRepository.save(user);// 2. 分配初始积分(模拟异常)if (request.getUsername().contains("test")) {throw new RuntimeException("测试异常");}Point point = new Point();point.setUserId(user.getId());point.setAmount(100);pointRepository.save(point);// 3. 发送注册通知(实际项目中可能调用外部服务)notificationService.sendRegistrationNotification(user.getId());}
}

问题分析

  1. 事务注解registerUser 方法使用了 @Transactional,期望三个操作在同一事务中。
  2. 异常捕获:Controller 捕获了所有异常并返回自定义响应,导致事务管理器无法感知异常。
  3. 结果
    • request.getUsername() 包含 “test” 时,抛出异常。
    • Controller 捕获异常并返回 ApiResponse.error(),但事务未回滚。
    • 数据库结果:用户记录被创建,但积分未分配,导致数据不一致。

正确示例:让异常自然抛出触发回滚

@RestController
@RequestMapping("/api/users")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/register")public ApiResponse registerUser(@RequestBody UserRegistrationRequest request) {// 直接调用,不捕获异常userService.registerUser(request);return ApiResponse.success();}
}@Service
public class UserServiceImpl implements UserService {@Override@Transactionalpublic void registerUser(UserRegistrationRequest request) {// 业务逻辑同上...// 任何异常都会导致事务回滚}
}// 全局异常处理器(统一处理异常)
@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(RuntimeException.class)@ResponseBodypublic ApiResponse handleRuntimeException(RuntimeException e) {return ApiResponse.error("系统错误:" + e.getMessage());}
}

关键区别

  1. 移除 try-catch:Controller 不再捕获异常,让异常自然传播到事务管理器。
  2. 全局异常处理:通过 @ControllerAdvice 统一处理异常,返回自定义响应。
  3. 事务生效:当抛出异常时,事务管理器自动回滚所有操作。

另一种方案:手动管理事务

如果你坚持在 Controller 中处理异常,可以使用 TransactionTemplate

@Service
public class UserServiceImpl implements UserService {@Autowiredprivate TransactionTemplate transactionTemplate;@Overridepublic void registerUser(UserRegistrationRequest request) {transactionTemplate.execute(status -> {try {// 业务逻辑...return null;} catch (Exception e) {// 手动标记回滚status.setRollbackOnly();throw e;}});}
}

总结

  • 事务生效条件:异常必须传播到代理方法外部。
  • Controller 捕获异常:会导致事务管理器无法感知异常,从而不回滚。
  • 解决方案
    1. 让异常自然抛出,通过全局异常处理器统一处理。
    2. 使用 TransactionTemplate 手动管理事务。

二、Feign 调用导致事务失效

Service 层使用了 Feign 调用,通常情况下是不能保证在同一个事务里的。

事务的基本原理和范围

  • 本地事务机制:在传统的单体应用中,像基于 Spring 的事务管理(使用 @Transactional 注解等方式),事务是依托于数据库连接来实现的。例如,当一个方法被标记为 @Transactional 时,Spring 会在方法执行前开启事务,获取数据库连接,在方法执行过程中如果出现异常就根据配置决定是否回滚事务,正常执行完则提交事务,整个过程都是围绕着同一个数据库连接进行操作,保证了一组数据库操作的原子性等特性。

  • 事务传播范围:事务的范围通常限定在一个本地的业务方法以及它所调用的其他同层级的本地方法内(前提是满足事务传播行为的相关规则),也就是在同一个应用的内部方法调用之间起作用。

Feign 调用的本质和特点

  • 远程调用:Feign 是用于实现微服务之间的 HTTP 客户端调用的工具,简单来说,它是一种通过 HTTP 协议去调用其他微服务提供的接口的方式。例如,服务 A 通过 Feign 调用服务 B 的某个接口,本质上是向服务 B 发送了一个 HTTP 请求,这和在同一个应用内的方法调用有着本质区别。

  • 不同的运行环境和资源管理:被调用的服务(如服务 B)有自己独立的运行环境、数据库连接等资源管理机制。服务 A 所在的事务上下文没办法直接延伸到服务 B 那边,因为它们是两个独立的微服务实例,各自管理着自己的事务。

示例说明无法保证同一事务

假设我们有两个微服务,一个是 OrderService 微服务,另一个是 InventoryService 微服务。

  • OrderService 中的业务逻辑
@Service
@Transactional
public class OrderServiceImpl implements OrderService {@Autowiredprivate OrderRepository orderRepository;@Autowiredprivate FeignClient inventoryFeignClient;public void createOrder(Order order) {// 保存订单到本地数据库orderRepository.save(order);// 通过 Feign 调用 InventoryService 来扣减库存inventoryFeignClient.reduceInventory(order.getProductId(), order.getQuantity());// 假设后续还有其他本地数据库操作,比如记录订单日志等// orderLogRepository.save(...)}
}
  • InventoryService 中的业务逻辑(被调用方)
@Service
@Transactional
public class InventoryServiceImpl implements InventoryService {@Autowiredprivate InventoryRepository inventoryRepository;public void reduceInventory(Long productId, Integer quantity) {// 从本地数据库扣减库存Inventory inventory = inventoryRepository.findById(productId).orElseThrow(() -> new ResourceNotFoundException("库存不存在"));inventory.setQuantity(inventory.getQuantity() - quantity);inventoryRepository.save(inventory);}
}

在上述例子中,OrderServiceImpl 中虽然整体方法标记了 @Transactional,但当它通过 Feign 调用 InventoryServiceImpl 中的 reduceInventory 方法时:

  • 即使 OrderServiceImpl 这边在执行 orderRepository.save(order) 后出现异常,InventoryService 那边已经接收到请求并执行了 inventoryRepository.save(inventory) 的话,是没办法自动回滚 InventoryService 里的操作的,因为这两个服务的数据库操作处于不同的事务环境中,各自管理自己的事务提交与回滚逻辑。

解决思路(实现分布式事务)

如果要在涉及 Feign 调用的多个微服务操作间保证事务的一致性,通常需要采用分布式事务的解决方案,常见的有以下几种:

  • 基于消息队列的最终一致性方案
    比如使用 RabbitMQ 或 Kafka 等消息队列,在 OrderService 中保存订单成功后,发送一个扣减库存的消息到消息队列,InventoryService 监听这个消息并执行扣减库存操作。两边通过消息的重试、补偿等机制来保证最终数据的一致性,不过这种方式不是强事务一致性,而是最终一致性,即经过一段时间后,各个微服务的数据状态会达到一致状态。

  • 使用分布式事务框架
    像 Seata 这样的分布式事务框架,它提供了多种分布式事务模式,例如 AT 模式(自动补偿模式)、TCC 模式(补偿事务模式)等。以 AT 模式为例,框架会在各个微服务的数据库操作前后进行数据的快照、记录相关的回滚日志等,当出现异常时,根据这些信息自动协调各个微服务回滚操作,从而保证多个微服务间事务的一致性。

所以,单纯的 Feign 调用本身不能保证在同一个事务里,需要借助分布式事务相关的技术手段来实现跨微服务的事务一致性。

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

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

相关文章

如何避免分布式爬虫被目标网站封禁?

在分布式爬虫的大规模数据采集场景中,避免被目标网站封禁的核心逻辑是:通过技术手段模拟真实用户行为,降低爬虫行为的可识别性,同时建立动态适配机制应对网站反爬策略的升级。以下从请求伪装、行为控制、资源管理、反爬对抗四个维…

Maven 打包排除特定依赖的完整指南(详细方法 + 示例)

前言 在使用 Maven 构建 Java 项目时,我们常常需要对项目的打包过程进行精细化控制,尤其是希望排除某些特定的依赖库。这可能是为了减小最终构建产物的体积、避免版本冲突,或者仅仅是为了满足不同环境下的部署需求。 本文将详细介绍如何在 Ma…

Terraform `for_each` 精讲:优雅地自动化多域名证书验证

大家好,在云原生和自动化运维的世界里,Terraform无疑是基础设施即代码(IaC)领域的王者。它强大的声明式语法让我们能够轻松地描述和管理云资源。然而,即使是经验丰富的工程师,在面对某些动态场景时也可能会…

C++标准库中各种互斥锁的用法 mutex

示例 仅供参考学习 #include <mutex> #include <shared_mutex> #include <thread> #include <chrono> #include <iostream> #include <vector>// // 1. std::mutex - 基本互斥锁 // void basic_mutex_example() {std::mutex mtx;int cou…

Android Handler机制与底层原理详解

Android 的 Handler 机制是跨线程通信和异步消息处理的核心框架&#xff0c;它构成了 Android 应用响应性和事件驱动模型的基础&#xff08;如 UI 更新、后台任务协调&#xff09;。其核心思想是 “消息队列 循环处理”。 核心组件及其关系Handler (处理器): 角色: 消息的发送…

jQuery JSONP:实现跨域数据交互的利器

jQuery JSONP&#xff1a;实现跨域数据交互的利器 引言 随着互联网的发展&#xff0c;跨域数据交互的需求日益增加。在Web开发中&#xff0c;由于同源策略的限制&#xff0c;直接通过XMLHttpRequest请求跨域数据会遇到诸多问题。而JSONP&#xff08;JSON with Padding&#xff…

Redis集群和 zookeeper 实现分布式锁的优势和劣势

在分布式系统中&#xff0c;实现分布式锁是确保多个节点间互斥访问共享资源的一种常见需求。Redis 集群 和 zookeeper 都可以用来实现这一功能&#xff0c;但它们有着各自不同的优势和劣势。 CAP 理论&#xff1a; 在设计一个分布式系统时&#xff0c;一致性&#xff08;Consis…

如何备份vivo手机中的联系人?

随着vivo移动设备在全球设立7个研发中心&#xff0c;vivo正在进入更多的国家。如今&#xff0c;越来越多的人开始使用vivo手机。以vivo X100为例&#xff0c;它配备了主摄像头和多个辅助摄像头&#xff0c;提供多样化的拍摄选项&#xff0c;并搭载了最新的FunTouch OS&#xff…

python脚本编程:使用BeautifulSoup爬虫库获取热门单机游戏排行榜

BeautifulSoup是一个便捷的解析html页面元素的python库&#xff0c;此处用来写一个简单的爬虫批量抓取国内游戏资讯网站的近期热门单机游戏排行榜。 网页来源如下所示代码 from bs4 import BeautifulSoup import requests# get web page web_url "https://www.3dmgame.co…

C#配置全面详解:从传统方式到现代配置系统

C#配置全面详解&#xff1a;从传统方式到现代配置系统 在软件开发中&#xff0c;配置是指应用程序运行时可调整的参数集合&#xff0c;如数据库连接字符串、API 地址、日志级别等。将这些参数从代码中分离出来&#xff0c;便于在不修改代码的情况下调整应用行为。C# 提供了多种…

数据中台架构解析:湖仓一体的实战设计

目录 一、数据中台与湖仓一体架构是什么 1. 数据中台 2. 湖仓一体架构 3. 湖仓一体在数据中台里的价值 二、湖仓一体架构的核心部件 1. 数据湖 2. 数据仓库 3. 数据集成工具 4. 数据分析与处理引擎 三、湖仓一体架构实战设计 1. 需求分析与规划 2. 数据湖建设 3. …

SQL Server表分区技术详解

表分区概述 表分区是将大型数据库表物理分割为多个较小单元的技术,逻辑上仍表现为单一实体。该技术通过水平分割数据显著提升查询性能,尤其针对TB级数据表可降低90%的响应时间。典型应用场景包含订单历史表、日志记录表等具有明显时间特征的业务数据,以及需要定期归档的审计…

WHIP(WebRTC HTTP Ingestion Protocol)详解

WHIP&#xff08;WebRTC HTTP Ingestion Protocol&#xff09;详解 WHIP&#xff08;WebRTC HTTP Ingestion Protocol&#xff09;是一种基于 HTTP 的协议&#xff0c;用于将 WebRTC 媒体流推送到媒体服务器&#xff08;如 SRS、Janus、LiveKit&#xff09;。它是为简化 WebRT…

图像噪点消除:用 OpenCV 实现多种滤波方法

在图像处理中&#xff0c;噪点是一个常见的问题。它可能是由于图像采集设备的缺陷、传输过程中的干扰&#xff0c;或者是光照条件不佳引起的。噪点会影响图像的质量和后续处理的效果&#xff0c;因此消除噪点是图像预处理的重要步骤之一。本文将介绍如何使用 OpenCV 实现几种常…

AI的Prompt提示词:英文写好还是中文好?

在与AI人大模型交互时,Prompt(提示词)的质量直接决定了输出的精准度和有效性。一个常见的问题是:究竟是用英文写Prompt好,还是用中文写更好?这并非一个简单的二元选择,而是涉及到语言模型的底层逻辑、表达的精确性以及个人使用习惯的综合考量。 英文Prompt的优势 模型训…

react的条件渲染【简约风5min】

const flag1true; console.log(flag1&&hello); console.log(flag1||hello); const flag20; console.log(flag2&&hello); console.log(flag2||hello); // &&运算符&#xff0c;如果第一个条件为假&#xff0c;则返回第一个条件&#xff0c;否则返回第二…

【RK3568+PG2L50H开发板实验例程】FPGA部分 | 紫光同创 IP core 的使用及添加

本原创文章由深圳市小眼睛科技有限公司创作&#xff0c;版权归本公司所有&#xff0c;如需转载&#xff0c;需授权并注明出处&#xff08;www.meyesemi.com)1.实验简介实验目的&#xff1a;了解 PDS 软件如何安装 IP、使用 IP 以及查看 IP 手册实验环境&#xff1a;Window11 PD…

thinkphp微信小程序一键获取手机号登陆(解密数据)

微信小程序获取手机号登录的步骤相对较为简单,主要分为几个部分: 1.用户授权获取手机号: 微信小程序通过调用 wx.getPhoneNumber API 获取用户授权后,获取手机号。 2.前端获取用户的手机号: 用户在小程序中点击获取手机号时,系统会弹出授权框,用户同意后,你可以通过 …

数据库设计精要:完整性和范式理论

文章目录数据的完整性实体的完整性主键域完整性参照完整性外键多表设计/多表理论一对一和一对多多对多数据库的设计范式第一范式&#xff1a;原子性第二范式&#xff1a;唯一性第三范式&#xff1a;不冗余性数据的完整性 实体的完整性 加主键&#xff0c;保证一个表中每一条数…

智能推荐社交分享小程序(websocket即时通讯、协同过滤算法、时间衰减因子模型、热度得分算法)

&#x1f388;系统亮点&#xff1a;websocket即时通讯、协同过滤算法、时间衰减因子模型、热度得分算法&#xff1b;一.系统开发工具与环境搭建1.系统设计开发工具后端使用Java编程语言的Spring boot框架项目架构&#xff1a;B/S架构运行环境&#xff1a;win10/win11、jdk17小程…