本篇记录原写于去年。
背景
一个下单逻辑跨了3个服务,采用 Seata AT 模式做分布式事务。
发现问题
分布式事务的处理并未成功,具体表现为:在出现异常后,3 个数据库里的表谁也没回滚。
本来以为是自己看错了,但是经过笔者的多次验证后,得到的结果都是如此,分支事务并未被正常处理。
好的,发现问题后该怎么办呢?
尝试解决问题
“小问题,轻轻松松~”
刚开始看到这个问题,笔者并没有觉得是个大问题,此时我并没有意识到严重性,也可以说是“轻敌”了。
当时,主要觉得问题可能出现配置和整合步骤上,于是做了如下4个事情:
- 检查整合步骤,是不是漏掉了哪个步骤,或者少了哪些配置,亦或者是哪个配置项因为粗心没配置好。
- 数据库中的表是否有问题。
- 项目重启(遇事不决,重启试试)。
- 查看日志,确认与Seata是否连接通信,是否正常注册TM、RM等。
由于觉得是小问题,所以从下午4点左右到6点,一直在做上述的4个事情。
结果就是,没发现步骤问题,也没发现配置问题,与Seata Server也正常通信,但是“分支事务不回滚”的问题还在。这个时候我其实有点意识到这可能不是个小问题了,有点小慌。但是搞了一段时间没头绪,脑子也乱了索性就下班回家了。
“坏了!见鬼了!”
本来想着第二天上班再处理的,但是被这个问题搞得实在睡不着,晚上11点半打开电脑继续处理。处理的过程和下午一样,我还是觉得可能是哪里不小心漏掉了步骤或者配置不对,扩大了检查范围,除了检查代码中的配置、数据库,还检查了Nacos Server的配置、Seata Server的配置,结果发现配置没问题,也没有遗漏什么。
为什么我觉得一开始是个小问题,然后主要是在检查配置项之类的。其原因就是之前在上一版已经整合过、配置过,而且验证通过,源码也没问题,分布式事务的处理结果是正确的。因此,我肯定会觉得只要整合步骤没有遗漏、配置项正确,分布式事务肯定会被正常处理。同样的代码、同样的配置、同样的测试环境,一个正常,一个不正常,这有点出乎意料了。这个时候我意识到了问题的严重性了,我觉得可能是遇见鬼了。确切地说,当时有点麻了,一份代码中分布式事务正常处理,一份完全没反应,说不麻是假的。
凌晨了,换个思路吧。
冷静下来后,我也不骗自己了,这代码肯定是有问题的,不然分支事务怎么会不回滚。但是我确实不知道问题在哪,怎么同样的东西在这一版项目里就不能用了呢?
不过,再去查配置、对比代码已经没意义了。既然确认代码有问题(不嘴硬了),那就开始根据Seata运行流程查一下哪里出了问题吧,主要是根据微服务实例的运行日志和Seata Server的运行日志来查的。
- 与Seata Server通信正常。
- 三个微服务实例都正常注册TM、RM等。
- 全局事务正常开启。
- 两个分支事务开启的日志一行都没出现。
不管是微服务实例的运行日志和Seata Server的运行日志,都没有看到两个分支事务的开启和处理,是的,没有任何信息和踪迹。再去数据库中确认了一下,undo_log表中也并没有数据。虽然不知道哪里出了问题,但是至少有方向了。
全局事务能够正常开启和回滚,而两个分支事务不正常(与Seata Server正常通信,但是都没有生效)。到这里已经大致有了眉目,乘客微服务和订单微服务两个服务实例的运行日志和Seata Server的运行日志,都没有看到任何关于全局事务的信息,这也说明了两个分支事务可能根本就没有注册成功。全局事务正常开启和处理,而两个本应出现的分支事务没有出现,它们之间“失联”了。
从代码层面来说,全局事务和分支事务的联系主要在一个变量上,这个变量就是全局事务的ID——xid。现在它们“失联”了,只能通过这个变量的产生、传递、接收、处理等几个步骤来确认问题在哪里了。
此时的要检查的内容就确定了下来:
- 全局事务是否正常开启?xid是否正确地生成了?
- xid是否正确地传递给下游的调用实例中?
- 下游的调用实例是否正确地接到了xid?
- 接到xid后是否正确处理并且开启分支事务?
“问题不清晰,看源码分析”。
为了确认上述的几个检查内容,还是要用debug模式看一看Seata处理分布式事务过程中所涉及到的源码,由于牵涉的源码太多,这里笔者挑几个重要节点介绍一下。
对于“全局事务是否正常开启?xid是否正确地生成了?”,主要跟进了下方两个类的源码:
io.seata.spring.annotation.GlobalTransactionalInterceptor.java
io.seata.tm.api.TransactionalTemplate.java
这两个类主要涉及全局事务的开启和处理,感兴趣的读者可以仔细地去探索一下。当然,结果是这个步骤并没有问题,全局事务正常开启,xid正确生成。
难道是xid生成了却没有传递给下游?对于这个问题,笔者主要在debug模式下跟进了com.alibaba.cloud.seata.feign.SeataFeignClient.java这个类的源码:
@Override
public Response execute(Request request, Request.Options options) throws IOException {
Request modifiedRequest = getModifyRequest(request);
return this.delegate.execute(modifiedRequest, options);
}
private Request getModifyRequest(Request request) {
String xid = RootContext.getXID();
if (StringUtils.isEmpty(xid)) {
return request;
}
Map<String, Collection<String>> headers = new HashMap<>(MAP_SIZE);
headers.putAll(request.headers());
List<String> seataXid = new ArrayList<>();
seataXid.add(xid);
// 把xid放入请求头中
headers.put(RootContext.KEY_XID, seataXid);
return Request.create(request.httpMethod(), request.url(), headers, request.body(),
request.charset(), null);
}
向下游微服务实例发送请求时是由 SeataFeignClient 来完成的,在这个类中会对 Request 对象进一步包装,把 xid 放进请求的 header 参数中并传递给下游方法。即在 saverOrder() 方法中使用 OpenFeign 调用乘客微服务和订单微服务中的方法前会对 Request 做进一步的包装然后才发起请求。当然,结果是这个步骤并没有问题,xid 被放入header 参数中并传递给下游了。
在找问题的过程中,笔者还在订单服务的方法中添加了 request 参数,主要是为了查看该对象中是否有 xid 参数,如下所示:
@DeleteMapping("/xxx")
public Result<Boolean> deleteItemIds(@RequestParam("Ids") List<Long> Ids, HttpServletRequest request) {
System.out.println("RootContext.getXID()="+ RootContext.getXID());
//...
}
在debug 模式下看了 request 对象中的内容,最终也是确认了 header 参数中是有 xid 参数的,也进一步确认了上游微服务实例(订单微服务)是正确地把 xid 传递到下游微服务中了,而且下游微服务实例也接收了 xid 参数,证明接收也没问题。
xid 的产生、传递、接收都没问题。到这里又卡住了,几个步骤好像都正常,怎么可能?怎么可能!怎么可能!!我还是有些不敢相信这个结果,如果这些步骤都正常的话,全局事务和分支事务怎么会“失联”呢?
“柳暗花明又一村。”
于是我赶紧在代码中又加上了打印 RootContext.getXID()
的语句,如果正确接收到上游微服务实例中传递的 xid 的话,这个变量肯定不会有问题。重新启动项目并请求 /saveOrder 验证整个分布式事务流程,打印RootContext.getXID() 的结果是 null,证明下游微服务实例确实没有正确地拿到 xid。
为什么会这样呢?
此时,答案已经呼之欲出了。全局事务ID——xid 正常地产生和传递到下游微服务实例了,然而看似是成功被下游微服务实例接收了,但是只是接收,并没有接到。上游微服务实例传了,下游微服务实例接了,但是没接到。“没接到”的意思就是到达下游微服务实例的请求中是有xid参数的,但是xid参数并没有被正常处理。xid的传递在终点出现了问题,导致了全局事务和分支事务“失联”了。
那么,下游服务实例中xid参数接收和处理的类在哪里呢?在com.alibaba.cloud.seata.web.SeataHandlerInterceptor
类中,源码及注释如下:
public class SeataHandlerInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory
.getLogger(SeataHandlerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
// 获取绑定后的xid
String xid = RootContext.getXID();
// 获取请求头中的xid
String rpcXid = request.getHeader(RootContext.KEY_XID);
if (log.isDebugEnabled()) {
log.debug("xid in RootContext {} xid in RpcContext {}", xid, rpcXid);
}
// 如果未绑定
if (StringUtils.isBlank(xid) && rpcXid != null) {
// 绑定xid
RootContext.bind(rpcXid);
if (log.isDebugEnabled()) {
log.debug("bind {} to RootContext", rpcXid);
}
}
return true;
}
... 省略部分代码
这个拦截器可以说是 xid 传递过程的终点,下游微服务实例会在这里接收请求头中的 xid 参数并进行绑定操作。如果这个拦截器中的方法正常运行的话,那么 xid 的传递就不会出问题,全局事务和分支事务也不会“失联”了。
查找问题过程中,我在这个拦截器的 preHandle() 方法中打了断点,然后在验证过程中根本没有进入过这些断点上,也就是说这个拦截器根本没起作用。为什么这个拦截器没起作用呢?因为没有根本配置这个拦截器。
passenger-service 和 order-service 两个项目中分别定义了PassengerServiceWebMvcConfigurer
和 OrderServiceWebMvcConfigurer
两个类并继承了 WebMvcConfigurationSupport
,如果一个拦截器要生效的话需要在这里进行配置。
解决办法就是在这两个项目中配置 SeataHandlerInterceptor 这个拦截器生效即可,代码如下:
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SeataHandlerInterceptor()).addPathPatterns("/**");
}
好的,到这里,“分支事务不回滚”的问题就解决完了,一切都正常了。饶了那么一大圈、花费了那么多时间、分析了一堆源码,结果仅仅是因为这个拦截器没配置。
复盘总结
总体来说,这个问题还是围绕 Seata 分布式事务处理中“全局事务的开启与处理”和“xid的产生与传递”这两个知识。
从前一天下午发现这个问题,然后没处理掉。晚上十一点继续处理这个问题,折腾到第二天凌晨四点左右才处理和验证完成。
其实一开始就进入了误区,我以为代码是正确的、配置是正常的。不过,好在没有一直轴,发现情况不对赶紧换个思路,分析整个过程和源码,最终找到了问题的根由。
其实在真实的业务开发中,也有可能遇到这种情况。比如,写个简单的demo 或者小功能一切都正常,但是真的拿到企业开发的项目里,直接拉闸。毕竟写 demo 不会考虑太多,涉及的代码也少,能跑就行,而真实项目中有些被忽略掉的或者说自己不熟悉的配置,这也是需要注意的点。文章来源:https://www.toymoban.com/news/detail-523414.html
另外,扩展一下这个知识点。收集广大网友的踩坑记录,除了“未配置SeataHandlerInterceptor”会导致“分支事务不回滚”的问题之外,全局事务失败的原因一般还有如下几种情形:文章来源地址https://www.toymoban.com/news/detail-523414.html
- 代码中的配置错误或者配置项有遗漏,导致报错。处理办法:检查配置,因为粗心或者漏掉了一些,修改正确即可。
- 数据源未被Seata代理,即未正确配置
io.seata.rm.datasource.DataSourceProxy
类。处理办法:修改代码,手动或者自动配置DataSourceProxy。 - 依赖版本升级导致的全局事务失效,笔者之前遇到过的,从seata-spring-boot-starter 1.3.0 升级到 1.4.2 时,由于 Seata 数据源自动配置逻辑的调整导致的。处理方法就是手动配置一下数据源代理。
到了这里,关于微服务中 Seata “分支事务不回滚”问题的复盘的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!