微服务中 Seata “分支事务不回滚”问题的复盘

本篇记录原写于去年。

背景

一个下单逻辑跨了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 两个项目中分别定义了PassengerServiceWebMvcConfigurerOrderServiceWebMvcConfigurer 两个类并继承了 WebMvcConfigurationSupport,如果一个拦截器要生效的话需要在这里进行配置。

解决办法就是在这两个项目中配置 SeataHandlerInterceptor 这个拦截器生效即可,代码如下:

public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SeataHandlerInterceptor()).addPathPatterns("/**");
}

好的,到这里,“分支事务不回滚”的问题就解决完了,一切都正常了。饶了那么一大圈、花费了那么多时间、分析了一堆源码,结果仅仅是因为这个拦截器没配置。

复盘总结

总体来说,这个问题还是围绕 Seata 分布式事务处理中“全局事务的开启与处理”和“xid的产生与传递”这两个知识。

从前一天下午发现这个问题,然后没处理掉。晚上十一点继续处理这个问题,折腾到第二天凌晨四点左右才处理和验证完成。

其实一开始就进入了误区,我以为代码是正确的、配置是正常的。不过,好在没有一直轴,发现情况不对赶紧换个思路,分析整个过程和源码,最终找到了问题的根由。

其实在真实的业务开发中,也有可能遇到这种情况。比如,写个简单的demo 或者小功能一切都正常,但是真的拿到企业开发的项目里,直接拉闸。毕竟写 demo 不会考虑太多,涉及的代码也少,能跑就行,而真实项目中有些被忽略掉的或者说自己不熟悉的配置,这也是需要注意的点。

另外,扩展一下这个知识点。收集广大网友的踩坑记录,除了“未配置SeataHandlerInterceptor”会导致“分支事务不回滚”的问题之外,全局事务失败的原因一般还有如下几种情形:

  • 代码中的配置错误或者配置项有遗漏,导致报错。处理办法:检查配置,因为粗心或者漏掉了一些,修改正确即可。
  • 数据源未被Seata代理,即未正确配置io.seata.rm.datasource.DataSourceProxy 类。处理办法:修改代码,手动或者自动配置DataSourceProxy。
  • 依赖版本升级导致的全局事务失效,笔者之前遇到过的,从seata-spring-boot-starter 1.3.0 升级到 1.4.2 时,由于 Seata 数据源自动配置逻辑的调整导致的。处理方法就是手动配置一下数据源代理。

你可能感兴趣的:(JAVA-EE,微服务,java,分布式)