SpringCloudAlibaba Seata在Openfeign跨节点环境出现全局事务Xid失效原因底层探究

原创/朱季谦

曾经在SpringCloudAlibaba的Seata分布式事务搭建过程中,跨节点通过openfeign调用不同服务时,发现全局事务XID在当前节点也就是TM处,是正常能通过RootContext.getXID()获取到分布式全局事务XID的,但在下游节点就出现获取为NULL的情况,导致全局事务失效,出现异常时无法正常回滚。

当时看了一遍源码,才知道问题所在,故而把这个过程了解到的分布式事务XID是如何跨节点传输的原理记录下来。

本文默认是使用Seata的AT模式。

在那一次的搭建过程中,我设置了三个节点,分别是订单节点order,商品库存节点product,账户余额节点account,模拟购买下单逻辑,在分布式环境下,生成一份订单时,通过openfeign远程扣减库存,最后同样通过openfeign去扣减账户(当然,实际场景远不止这些,这里只是简单模拟这个过程)。

正常情况下,其中有一步出错,整个全局分布式事务就会进行回滚。

image

这三个节点在Seata AT模式下,流程图是这样的,order充当TM/RM角色,product和充当RM角色,按照在Linux服务器上的Seats Service就充当TC角色。

image

首先是最初调用订单节点order业务逻辑——

@Override
@Transactional
@GlobalTransactional(name = "zjq-create-order",rollbackFor = Exception.class)
public RestResponse createOrder(Orders order) {
    log.info("当前的XID:"+ RootContext.getXID());
    log.info("------>开始新建订单");
    //1、新建订单
    orderMapper.insert(order);

    //2、扣减库存
    productService.decrease(order.getProductId(),order.getCount());

    //3、扣减账户
    accountService.decrease(order.getUserId(),order.getMoney());

    ......
}

在Seata,order充当了TM角色,负责生成一个全局事务注册到TC,TC会返回一个全局事务ID给TM。

在该全局事务流程里,每一个分支模块理应都能获取到这一个共同的全局事务ID,在该全局事务ID统筹下,完成分支事务的提交或者回滚。

通过RootContext.getXID()获取到一个全局事务ID为:192.168.1.152:8091:458311058765479936

image

创建订单成功后,就会执行扣减库存操作productService.decrease(order.getProductId(),order.getCount())。

在该代码案例里,productService.decrease()内部是通过openfeign远程去调用的——

@FeignClient(contextId = "remoteProductService",value = "zjq-product",fallbackFactory = RemoteProductServiceFallbackFactory.class)
public interface RemoteProductService {
    @PostMapping(value = "/product/decrease")
    RestResponse decrease(@RequestParam("productId")Long productId, @RequestParam("count") Integer count);
}

最终decrease的服务层伪代码大概如下——

@Transactional(propagation = Propagation.REQUIRES_NEW)
public int decrease(Long productId, Integer count) {
    log.info("当前的XID:"+ RootContext.getXID());
    log.info("---------->开始查询商品是否存在");
    log.info("---------->开始扣减库存"); 
    ......
}

然而,到这一步,发现了一个问题,这里获取的全局事务ID为null——

image

这说明了一个问题,TM开启了一个全局事务后,已经从TC那里获取到了一个全局事务ID,但远程传送给product这个RM资源管理器后,没有传送成功,同理,另一个分支事务account模块的,同样获取到的全局事务ID为null。

基于这样一个现象,我就开始尝试研究了一下全局事务是如何在Openfeign跨节点环境进行传输和获取的,主要分为TM节点的全局事务ID发送和远程RM节点的接收。

一、TM节点的全局事务ID发送

通过debug代码去阅读,在调用 productService.decrease(order.getProductId(),order.getCount())时,内部做了反射调用,执行了一系列方案调用,调用核心过程如下——

image

本文只需要关注在整个HTTP调用过程,全局事务ID是如何放进来的,这个调用链涉及的类及方法,在后续学习中再进一步研究。

最终在SeataFeignClient的execute方法里,可以看到以下源码——

public Response execute(Request request, Request.Options options) throws IOException {
    Request modifiedRequest = this.getModifyRequest(request);
    return this.delegate.execute(modifiedRequest, options);
}

其中,在Request modifiedRequest = this.getModifyRequest(request)这行代码里,对请求头做了一些补充操作。

private Request getModifyRequest(Request request) {
    String xid = RootContext.getXID();
    if (StringUtils.isEmpty(xid)) {
        return request;
    } else {
        Map<String, Collection<String>> headers = new HashMap(16);
        headers.putAll(request.headers());
        List<String> seataXid = new ArrayList();
        seataXid.add(xid);
        headers.put("TX_XID", seataXid);
        return Request.create(request.method(), request.url(), headers, request.body(), request.charset());
    }
}

debug到这里,可以看到,这里将一个全局事务ID存储到了headers里——

image

这个headers其实是HTTP组装的请求头,可以看到,这里是将全局事务ID放到了HTTP请求头里,传送给了远程机器。

Request(HttpMethod method, String url, Map<String, Collection<String>> headers, Body body, RequestTemplate requestTemplate) {
    this.httpMethod = (HttpMethod)Util.checkNotNull(method, "httpMethod of %s", new Object[]{method.name()});
    this.url = (String)Util.checkNotNull(url, "url", new Object[0]);
    this.headers = (Map)Util.checkNotNull(headers, "headers of %s %s", new Object[]{method, url});
    this.body = body;
    this.requestTemplate = requestTemplate;
}

通过debug,可以发现,在HTTP组装过程中,已经将全局事务ID放到了请求头里,说明在HTTP发生成功后,是会携带全局事务到远程product模块的,但是为何product模块打印RootContext.getXID()得到的是null呢?

二、跨节点分支事务获取全局事务ID

HTTP请求传送到远程product模块后,在调用具体的Controller前,会流转到MVC进行拦截转发,在这过程当中,涉及到seata分布式事务时,理应会有这样一个叫TransactionPropagationInterceptor的拦截器,用来处理分布式事务的传播,有两个方法,分别是preHandle()和afterCompletion(),暂时只需要关注preHandle方法即可:

  • preHandle()

​ 在处理远程请求之前被调用,在该方法中,通过RootContext.getXID()获取到当前线程上下文中的全局事务ID和通过request.getHeader("TX_XID")获取HTTP请求头中的事务ID。这里的请求头里的事务ID,正是前面发送HTTP时放到请求头里的。

若RootContext.getXID()获取到当前线程上下文中的全局事务ID为空并且HTTP请求头的事务ID不为空,就会将该HTTP请求头里的事务ID绑定到该线程上下文当中,用于确保全局事务的传播和关联。

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    String xid = RootContext.getXID();
    String rpcXid = request.getHeader("TX_XID");
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("xid in RootContext[{}] xid in HttpContext[{}]", xid, rpcXid);
    }

    if (StringUtils.isBlank(xid) && StringUtils.isNotBlank(rpcXid)) {
        RootContext.bind(rpcXid);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("bind[{}] to RootContext", rpcXid);
        }
    }

    return true;
}

进入到bind方法当中,可以看到,这里是HTTP请求头里的事务ID缓存到了 CONTEXT_HOLDER.put("TX_XID", xid),它本质其实是一个ThreadLocal,可以存储线程隔离的变量。

public static void bind(@Nonnull String xid) {
    if (StringUtils.isBlank(xid)) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("xid is blank, switch to unbind operation!");
        }

        unbind();
    } else {
        MDC.put("X-TX-XID", xid);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("bind {}", xid);
        }

        CONTEXT_HOLDER.put("TX_XID", xid);
    }

}

缓存成功后,下一次通过 RootContext.getXID()就能获取到该线程缓存的全局事务ID了, RootContext.getXID()本质就是——

public static String getXID() {
    return (String)CONTEXT_HOLDER.get("TX_XID");
}

在本次搭建seata环境中,发现该TransactionPropagationInterceptor过滤器当中的preHandle方法一直没有执行,这就造成全局事务当中,远程跨环境的分支事务节点一直无法获取到全局事务ID。

于是,我尝试手动将该TransactionPropagationInterceptor拦截器加入到Spring MVC流程中——

@Configuration
public class WebMvcInterceptorsConfig extends WebMvcConfigurationSupport {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TransactionPropagationInterceptor());
    }

    @Bean
    public ServerCodecConfigurer serverCodecConfigurer() {
        return ServerCodecConfigurer.create();
    }
    
}

重新运行后,这次拦截器TransactionPropagationInterceptor终于生效里,可以debug到了preHandle方法里,将HTTP请求头的全局事务ID取出,然后通过RootContext.bind(rpcXid)缓存到线程上下文当中——

image

这时,product节点终于能拿到从TM远程传送过来的全局事务ID了——

image

最后总结一下,全局事务ID在SpringCloudAlibaba Seata在Openfeign跨节点环境里的传送方式,是将该全局事务ID放入到HTTP请求头当中,远程传送给分支事务节点,各分支事务节点会在TransactionPropagationInterceptor拦截器当中,取出HTTP请求头大全局事务ID,通过RootContext.bind(rpcXid)将全局事务ID缓存到线程上下文里,这样,分支事务就可以在其执行过程当中,获取到全局事务ID啦。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ros2+gazebo+urdf:ros2机器人使用gazebo的urdf文件中的<gazebo>部分官网资料

原文链接SDFormat extensions to URDF (the gazebo tag) — Documentation 注意了ros2的gazebo部分已经跟ros1的gazebo部分不一样了&#xff1a; Toggle navigation SpecificationAPIDocumentationDownload Back Edit Version: 1.6 Table of C…

我在代码随想录|写代码Day-Day之总结篇

我是用笔手写的我觉得这样可以对个人记忆会更好,而且理解更深解释也更清楚 下面是手写笔记 总结部分----- 第一章 二分 二分模版 图片可能反了下不过没有关系 图形打印模版题 第二章 链表 链表基本操作和疑问 链表代码操作和解析----5大操作 删除部分 对结点的操作 反了反了…

关于个人Git学习记录及相关

前言 可以看一下猴子都能懂的git入门&#xff0c;图文并茂不枯燥 猴子都能懂的git入门 学习东西还是建议尽可能的去看官方文档 权威且详细 官方文档 强烈建议看一下GitHub漫游指南及开源指北&#xff0c;可以对开源深入了解一下&#xff0c;打开新世界的大门&#xff01; …

若依SQL Server开发使用教程

1. sys_menu表中的将菜单ID修改为自动ID,解决不能增加菜单的问题&#xff0c;操作流程如下&#xff1a; 解决方案如下 菜单栏->工具->选项 点击设计器&#xff0c;去掉阻止保存要求更新创建表的更改选项&#xff0c;点确认既可以保存了 2 自动生成代码找不表的解决方案…

C语言--直接插入排序【排序算法|图文详解】

一.直接插入排序介绍&#x1f357; 直接插入排序又叫简单插入排序&#xff0c;是一种简单直观的排序算法&#xff0c;它通过构建有序序列&#xff0c;对于未排序的数据&#xff0c;在已排序序列中从后向前扫描&#xff0c;找到相应位置并插入。 算法描述&#xff1a; 假设要排序…

idea 找不到 Free MyBatis plugin

idea 找不到 free mybatis plugin 可以使用mybatisX替换&#xff1a; 插件安装成功后&#xff0c;重启idea。

【English】水果单词小小汇总~~

废物研究生&#xff0c;只要不搞科研干啥都是开心的&#xff0c;啊啊啊啊啊科研要命。作为一个水果怪&#xff08;每天不吃水果就要命的那种哈哈哈哈&#xff09;突然发现竟然就知道什么apple、banana、orange&#xff01;惭愧惭愧&#xff0c;正好兴致正浓&#xff0c;来整理一…

Java HashMap在遍历时删除元素

文章目录 1. HashMap数据结构1.1 数组单向链表红黑树1.2 指定初始容量&#xff0c;省去多次扩容步骤1.3 获取map内容&#xff1a;Map.Entry 2. 遍历集合时删除元素3. computeIfAbsent()方法 1. HashMap数据结构 jdk是1.8版本 HashMap 线程不安全 ConcurrentHashMap 线程安全 1.…

jvm_下篇_补充_MAT从入门到精通

MAT从入门到精通 概述安装mac安装指定jdk配置内存 使用配置获取dump文件Overview下功能解释HistogramDominator TreeLeak SuspectsOverview功能说明结尾Thread_Overview OQLHeap Dump OverviewFind Object by address 概述 尽管JVM提供了自动内存管理的机制&#xff0c;试图降…

golang的jwt学习笔记

文章目录 初始化项目加密一步一步编写程序另一个参数--加密方式关于StandardClaims 解密解析出来的怎么用关于`MapClaims`上面使用结构体的全代码实战项目关于验证这个项目的前端初始化项目 自然第一步是暗转jwt-go的依赖啦 #go get github.com/golang-jwt/jwt/v5 go get githu…

小狐狸ChatGPT系统 H5前端底部菜单导航文字修改方法

小狐狸ChatGPT系统后端都前端都是编译过的&#xff0c;需要改动点什么非常难处理&#xff0c;开源版修改后也需要编译后才能使用&#xff0c;大部分会员也不会使用&#xff0c;像简单的修改下底部菜单文字、图标什么的可以对照处理。这里以小狐狸ChatGPT系统1.9.2版本H5端为例&…

Unity向量按照某一点进行旋转

Unity向量按照某一点进行旋转 一、unity的旋转二、向量按照原点进行旋转注意案例 三、向量按照指定位置进行旋转案例 一、unity的旋转 首先要知道一点就是在Unity的旋转中使用过四元数进行旋转的&#xff0c;如果对一个物体的rotation直接赋值你会发现结果不是你最终想要的结果…

Kubectl 部署有状态应用(上)

前面介绍了Deployment以及如何部署无状态应用。 Kubectl 部署无状态应用Deployment Controller详解&#xff08;上&#xff09;Deployment Controller详解&#xff08;下&#xff09; 本文将继续介绍如何在k8s上部署有状态应用。 有状态和无状态服务的区别 无状态&#xff…

数组元素反序

和前面的字符串逆向输出有异曲同工之妙 第一位和最后一位交换位置&#xff0c;然后用比大小循环 那么接下来修改一下这个程序&#xff0c;我们接下来解释一下p的概念 画图解释&#xff1a; 在最前面的 定义的时候&#xff0c;我们将p&#xff08;0&#xff09;定义在了1上&…

四、Spring IoC实践和应用(基于配置类方式管理 Bean)

本章概要 基于配置类方式管理 Bean 完全注解开发理解实验一&#xff1a;配置类和扫描注解实验二&#xff1a;Bean定义组件实验三&#xff1a;高级特性&#xff1a;Bean注解细节实验四&#xff1a;高级特性&#xff1a;Import扩展实验五&#xff1a;基于注解配置类方式整合三层…

抓包工具Fiddler的常用操作

文章目录 Fiddler概述Fiddler页面介绍常用功能介绍端口号的修改设置抓HTTPS数据Fillder过滤请求数据 接口相关Fiddler中查看请求信息Fiddler中查看响应信息 Fiddler模拟弱网测试Fiddler模拟mock数据Fillder篡改数据 Fiddler概述 fiddler是一款http协议调试代理工具&#xff0c;…

redis的那些事(二)——布隆过滤器

什么是布隆过滤器&#xff1f; 布隆过滤器&#xff08;Bloom Filter&#xff09;是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。 布隆过滤器实现原理 布隆过滤器是一个bit向量或者说是一个b…

【linux提权】利用setuid进行简单提权

首先先来了解一下setuid漏洞&#xff1a; SUID (Set UID)是Linux中的一种特殊权限,其功能为用户运行某个程序时&#xff0c;如果该程序有SUID权限&#xff0c;那么程序运行为进程时&#xff0c;进程的属主不是发起者&#xff0c;而是程序文件所属的属主。但是SUID权限的设置只…

构建深度学习模型:原理与实践

构建深度学习模型&#xff1a;原理与实践 引言 随着人工智能技术的飞速发展&#xff0c;深度学习已经成为当今最为炙手可热的研究领域之一。深度学习通过模拟人脑神经网络的工作原理&#xff0c;使得计算机能够具备更强大的学习和识别能力。本文将深入探讨深度学习的基本原理…

【模式识别】探秘分类奥秘:K-近邻算法解密与实战

​&#x1f308;个人主页&#xff1a;Sarapines Programmer&#x1f525; 系列专栏&#xff1a;《模式之谜 | 数据奇迹解码》⏰诗赋清音&#xff1a;云生高巅梦远游&#xff0c; 星光点缀碧海愁。 山川深邃情难晤&#xff0c; 剑气凌云志自修。 目录 &#x1f30c;1 初识模式识…