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

原创/朱季谦
曾经在SpringCloudAlibaba的Seata分布式事务搭建过程中,跨节点通过openfeign调用不同服务时,发现全局事务XID在当前节点也就是TM处,是正常能通过RootContext.getXID()获取到分布式全局事务XID的,但在下游节点就出现获取为NULL的情况,导致全局事务失效,出现异常时无法正常回滚。
当时看了一遍源码,才知道问题所在,故而把这个过程了解到的分布式事务XID是如何跨节点传输的原理记录下来。
本文默认是使用Seata的AT模式。
在那一次的搭建过程中,我设置了三个节点,分别是订单节点order,商品库存节点product,账户余额节点account,模拟购买下单逻辑,在分布式环境下,生成一份订单时,通过openfeign远程扣减库存,最后同样通过openfeign去扣减账户(当然,实际场景远不止这些,这里只是简单模拟这个过程)。
正常情况下,其中有一步出错,整个全局分布式事务就会进行回滚。

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

首先是最初调用订单节点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

创建订单成功后,就会执行扣减库存操作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——

这说明了一个问题,TM开启了一个全局事务后,已经从TC那里获取到了一个全局事务ID,但远程传送给product这个RM资源管理器后,没有传送成功,同理,另一个分支事务account模块的,同样获取到的全局事务ID为null。
基于这样一个现象,我就开始尝试研究了一下全局事务是如何在Openfeign跨节点环境进行传输和获取的,主要分为TM节点的全局事务ID发送和远程RM节点的接收。
一、TM节点的全局事务ID发送
通过debug代码去阅读,在调用 productService.decrease(order.getProductId(),order.getCount())时,内部做了反射调用,执行了一系列方案调用,调用核心过程如下——

本文只需要关注在整个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里——

这个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呢?
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)缓存到线程上下文当中——

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

最后总结一下,全局事务ID在SpringCloudAlibaba Seata在Openfeign跨节点环境里的传送方式,是将该全局事务ID放入到HTTP请求头当中,远程传送给分支事务节点,各分支事务节点会在TransactionPropagationInterceptor拦截器当中,取出HTTP请求头大全局事务ID,通过RootContext.bind(rpcXid)将全局事务ID缓存到线程上下文里,这样,分支事务就可以在其执行过程当中,获取到全局事务ID啦。
SpringCloudAlibaba Seata在Openfeign跨节点环境出现全局事务Xid失效原因底层探究的更多相关文章
- 学习OpenStack之(5):在Mac上部署Juno版本OpenStack 四节点环境
0. 前沿 经过一段时间的折腾,终于在自己的Mac上装好了Juno版本的四节点环境.这过程中,花了大量的时间,碰到了许多问题,学到不少知识,折腾过不少其实不需要折腾的东西,本文试着来对这过程做个总结. ...
- kubernetes 单节点和多节点环境搭建
kubernetes单节点环境搭建: 1.在VMWare Workstation中建立一个centos 7虚拟机.虚拟机的配置尽量调大一些 2.操作系统安装完成后,关闭centos 自带的防火墙服务 ...
- 安装Rocky版OpenStack 1控制节点+1计算节点环境部署脚本
在上一篇文章中叙述了具体的安装部署过程,在这里把相应的部署脚本写出来,供大家参考: 一.执行部署的setup.sh脚本: #!/bin/bash ########################### ...
- 脚本安装Rocky版OpenStack 1控制节点+1计算节点环境部署
视频安装指南请访问: http://39.96.203.138/wordpress/document/%E8%84%9A%E6%9C%AC%E5%AE%89%E8%A3%85rocky%E7%89%8 ...
- mysql跨节点join——federated引擎
一. 什么是federated引擎 mysql中的federated类似于oracle中的dblink. federated是一个专门针对远程数据库的实现,一般情况下在本地数据库中建表会在数据库目录中 ...
- ASP.NET Core on K8S学习初探(1)K8S单节点环境搭建
当近期的一个App上线后,发现目前的docker实例(应用服务BFF+中台服务+工具服务)已经很多了,而我司目前没有专业的运维人员,发现运维的成本逐渐开始上来,所以容器编排也就需要提上议程.因此我决定 ...
- dolphinscheduler简单任务定义及复杂的跨节点传参
dolphinscheduler简单任务定义及跨节点传参 转载请注明出处 https://www.cnblogs.com/funnyzpc/p/16395094.html 写在前面 dolphinsc ...
- Apache DolphinScheduler 简单任务定义及复杂的跨节点传参
点亮 ️ Star · 照亮开源之路 GitHub:https://github.com/apache/dolphinscheduler Apache DolphinScheduler是一款非常不 ...
- 用openvswitch配置跨节点的docker网络环境
在一篇随笔中,我们已经尝试了在不依赖工具的情况下设置docker的ip,连我都想吐槽,MD单机都这么麻烦,在多机的环境中岂不是要了我的小命! 本文就是为了多机环境中各个节点的容器通信而做的,网络拓朴如 ...
- 跨多种环境部署 Gearman -改善应用程序性能和降低服务器负载
您可能想要将工作扩散到一个大型机器群体中,或者想要在不同语言和环境之间共享功能,那么开放源码的 Gearman 服务可以让您轻松地将工作分布到网络中的其他机器.本文将介绍 Gearman 的一些典型应 ...
随机推荐
- 即构推出低延迟直播产品L3,可将直播延迟降到1s
近日,全球云通讯服务提供商ZEGO即构科技推出低延迟直播产品Low-Latency Live,简称L3.这款产品对传统CDN直播中"延迟较大.弱网抗性差.观众端内容不同步"等问题进 ...
- 图片转ASCII字符图案的原理(可调整亮度对比度 宽高度)
来, 先看效果哈哈哈哈! 演示地址: http://ascii-picture.imlht.com/ "\` """ . "\`"" ...
- Mybatis(生命周期 )
生命周期和作用域 生命周期和作用域,是至关重要的,因为错误的使用导致非常严重并发问题 对象声明周期和依赖注入框架 依赖注入框架可以创建线程安全的,基于事务的SqlSession和映射器,并将它们直接注 ...
- quarkus实战之二:应用的创建、构建、部署
欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是<quarkus实战>系列 ...
- 使用Docker安装Apollo并使用SpringBoot连接配置中心
上篇文章我们学习了Apollo的本地安装,如果还不会本地安装的朋友可以查看之前的文章进行了解和学习链接地址(https://www.cnblogs.com/eternality/p/17583023. ...
- Pandas: 将dataframe转换为dict
背景 将Dataframe的每一列数据转换成字典并保存.也就是字段名变为key, 数值变为value. 方案 以下是效果图 参考链接 https://blog.csdn.net/hanyunkaka/ ...
- Django 组织json格式
@api_view(['GET', 'POST']) def api_test(request): classes = Classes.objects.all() # classes_data = C ...
- 《深入理解Java虚拟机》读书笔记:判断对象是否存活
本节内容的概要如下; 对象已死吗? 一.判断对象是否存活的算法 1.引用计数器算法 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器为0 ...
- [windows]远程桌面失败提示CredSSP加密修正
前言 windows远程桌面失败,提示"CredSSP加密--" 远程桌面服务器的系统版本:Windows Server 2016 本地电脑的系统版本:Windows 10 方式1 ...
- Go面经 | 成都Go面试这么卷?卷王介绍:游戏行业 3年经验 20k+
Go最新面经分享:算法.并发模型.缓存落盘.etcd.actor模型.epoll等等... 本文先分享2段面经,文末总结了关键问题的复盘笔记.一定要看到最后! 求职者情况 分享一下好友的最新面经. 简 ...