转转门店基于MQ的Http重试实践

  • 1 问题背景
  • 2 重试方案探索
    • 2.1 简单重试
    • 2.2 Apache HttpClient 重试机制
    • 2.3 基于消息队列的异步重试方案
  • 3 门店业务场景中使用的重试方案

1 问题背景

在线下门店系统开发中,有很多地方需要使用Http 请求和第三方系统进行通信,比如将门店的商品信息同步到第三方的电子价签上,再比如需要把门店店员的打卡信息同步到公司使用的第三方EHR系统中。

但在使用Http请求外部服务时,由于网络的不稳定性,第三方接口出现超时的现象时有发生,为了减少对业务造成的影响,我们迫切需要寻找一种Http重试方案。

2 重试方案探索

2.1 简单重试

我们最容易想到的一种重试方式是,在请求接口的代码块中加入循环,如果请求失败则继续请求,直到请求成功或达到最大重试次数。示例代码如下:

  int retryTimes = 3;for (int i = 0; i < retryTimes; i++) {try {// 请求接口的代码break;} catch(Exception e) {// 处理异常}}

这种重试方式比较简单,只要请求发生异常就继续重试,能在一定程度上解决我们的问题,但缺点是对于异常的捕获处理逻辑过于简单,重试起来会有一定的盲目性。

2.2 Apache HttpClient 重试机制

我们常用的一些Http客户端通常也内置了一些重试机制,接下来我将以我们系统中使用的Apache HttpClient为例,通过手撕源码的方式探索一下它内部的重试机制。

通常我们在使用HttpClient的时候,都需要以下几个步骤;

  CloseableHttpClient httpClient = HttpClientBuilder.create().build();HttpGet httpGet = new HttpGet("url");CloseableHttpResponse response = httpClient.execute(httpGet);HttpEntity entity = response.getEntity();

在创建 HttpClient 的过程中,底层调用了HttpClientBuilder的build方法,我们直接找到跟重试相关的逻辑,源码如下图:

    if (!automaticRetriesDisabled) {HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;if (retryHandlerCopy == null) {retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;}execChain = new RetryExec(execChain, retryHandlerCopy);}

automaticRetriesDisabled默认是没有禁用的,RetryExec是一个重试执行器,它还需要一个 RetryHandler,如果没有指定的话,会使用DefaultHttpRequestRetryHandler作为默认的重试处理器。

我们先来看一下RetryExec的逻辑,源码如下图:

    public CloseableHttpResponse execute(final HttpRoute route,final HttpRequestWrapper request,final HttpClientContext context,final HttpExecutionAware execAware) throws IOException, HttpException {final Header[] origheaders = request.getAllHeaders();for (int execCount = 1;; execCount++) {try {return this.requestExecutor.execute(route, request, context, execAware);} catch (final IOException ex) {if (execAware != null && execAware.isAborted()) {this.log.debug("Request has been aborted");throw ex;}if (retryHandler.retryRequest(ex, execCount, context)) {if (!RequestEntityProxy.isRepeatable(request)) {this.log.debug("Cannot retry non-repeatable request");throw new NonRepeatableRequestException("Cannot retry request " +"with a non-repeatable request entity", ex);}request.setHeaders(origheaders);} else {if (ex instanceof NoHttpResponseException) {final NoHttpResponseException updatedex = new NoHttpResponseException(route.getTargetHost().toHostString() + " failed to respond");updatedex.setStackTrace(ex.getStackTrace());throw updatedex;}throw ex;}}}}

看到这里,怎么还感觉到有点眼熟了呢?是不是和我们上面简单重试的思路是一样的呢,有点大道至简那个意思了。

我们来简单总结一下RetryExec的主要逻辑:在执行Http请求的时候,如果发生了IOException,会交给具体的RetryHandler来处理,然后由它的retryRequest方法来决定是继续重试还是抛出异常。 这里可能有的朋友会有疑问,为什么是IOException呢?这就要说一下HttpClient的execute方法了,HttpClient执行时可能会抛出两种异常:IOException和ClientProtocolException; 其中IOException被认为是非致命性且可恢复的,而ClientProtocolException被认为是致命性的,不可恢复,所以这里只需要关注IOException异常即可。 接下来我们再来看一下DefaultHttpRequestRetryHandler,它定义了3个成员变量:

  • retryCount:重试次数;

  • requestSentRetryEnabled:是否可以在请求成功发出后重试,这里的成功是指发送成功,并不指请求成功;

  • nonRetriableClasses:不重试的异常类集合,如果异常为集合中指定的异常时,不会重试。

DefaultHttpRequestRetryHandler经过一系列构造函数,完成了对三个成员变量的赋值,其中默认的重试次数是3次,并且默认在请求发送成功之后就不会再重试,默认的不重试异常有以下四类:

  • InterruptedIOException

  • UnknownHostException

  • ConnectException

  • SSLException

源码如下图:

    public DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) {this(retryCount, requestSentRetryEnabled, Arrays.asList(InterruptedIOException.class,UnknownHostException.class,ConnectException.class,NoRouteToHostException.class,SSLException.class));}public DefaultHttpRequestRetryHandler() {this(3, false);}

然后,我们再来看一下DefaultHttpRequestRetryHandler中的核心方法retryRequest方法的逻辑,源码逻辑如下图:

    public boolean retryRequest(final IOException exception,final int executionCount,final HttpContext context) {if (executionCount > this.retryCount) {// Do not retry if over max retry countreturn false;}if (this.nonRetriableClasses.contains(exception.getClass())) {return false;}final HttpClientContext clientContext = HttpClientContext.adapt(context);final HttpRequest request = clientContext.getRequest();if (handleAsIdempotent(request)) {// Retry if the request is considered idempotentreturn true;}if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) {// Retry if the request has not been sent fully or if it's OK to retry methods that have been sentreturn true;}return false;}

retryRequest的逻辑也比较简单,首先超过重试次数就不会再重试,然后如果是指定不重试的异常也不会再重试;再然后如果请求方法不是幂等的,也不会继续重试,这里我们熟悉的Post方法显然是不会进行重试的。不过还有机会,这里我们知道requestSentRetryEnabled默认是false,也就是说只要请求发送成功之后也不会进行重试。

到这里,我们可以总结一下了。HttpClient默认的RetryHandler中指定了四类异常是不会进行重试的,其中就包含了InterruptedIOException,而实际上我们经常会遇到的SocketTimeoutException就属于它的子类。

还有一点,如果按照默认的重试策略,显然Post请求也不满足重试的条件。这里必须说一下,从谨慎的角度来看,Post请求是否应该重试,需要具体结合业务场景来看,如果请求本身不是幂等的,重试确实可能会带来严重的副作用。

所以在实际的业务场景中,如果想要利用HttpClient的重试机制来进行重试,这两个问题都需要解决。

2.3 基于消息队列的异步重试方案

考虑到在门店很多业务场景中,执行完相关的逻辑之后都会发送MQ消息。那么我们很自然地也想到了通过引入一个消费者的方式,来执行通过Http调用第三方接口的逻辑。

采用这种方式的话,如果在消费逻辑中通过Http调用第三方接口失败,我们还可以充分利用MQ的消费失败重试机制。以我们使用的RocketMQ为例,消息在消费失败重试的时候会按照一定的退避时间来进行重试,这个特性还能避免第三方服务因为短时间的不可用而造成的重试失败的情况。

3 门店业务场景中使用的重试方案

经过以上多种方案的调研,我们最终采用的是方案二和方案三的综合方案,具体思路如下。 首先,我们整体的重试方案采用基于消息队列的异步执行方案,一方面是因为这种方案可以充分地做到和业务之间解耦,同时消息队列的消费失败重试机制可以很好地解决第三方服务短时间不可用的问题,这一点是同步重试方案做不到的,可以保障系统的最终一致性。

其次,因为我们系统中已经在使用HttpClient 组件,所以我们决定充分利用它的重试机制,同步重试也可以尽可能保证接口调用的实时性。

考虑到默认的重试策略不满足我们的使用需求,针对这个问题,我们自定义了一个RetryHandler,源码如下图:

    public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {if (executionCount > this.retryCount) {RequestLine requestLine = null;if (context instanceof HttpClientContext) {requestLine = ((HttpClientContext)context).getRequest().getRequestLine();}return false;} else if (exception instanceof NoHttpResponseException) {return true;} else if (exception instanceof SSLHandshakeException) {return false;} else if (exception instanceof InterruptedIOException) {return true;} else if (exception instanceof UnknownHostException) {return false;} else if (exception instanceof ConnectTimeoutException) {return false;} else if (exception instanceof SSLException) {return false;} else {HttpClientContext clientContext = HttpClientContext.adapt(context);HttpRequest request = clientContext.getRequest();return !(request instanceof HttpEntityEnclosingRequest);}}

完成RetryHandler的自定义之后,只需要在初始化HttpClient的时候传入指定的RetryHandler即可,设置方式如下:

  CloseableHttpClient httpClient = HttpClientBuilder.create().setRetryHandler(StoreRequestRetryHandler.INSTANCE).build();

这样我们就解决了默认的重试机制对于Post请求默认不重试和SocketTimeoutException异常不重试的问题,更加贴合我们的使用场景。

这里我举个例子来说明一下整个重试方案的执行流程:

  • MQ在消费的时候,会使用Apache HttpClient请求第三方接口,我们设置重试3次,如果请求一直失败,会先同步重试3次,如果还是失败,则本次消息消费失败,等待下一次重试消息继续这个流程。

  • RocketMQ默认会重试16次,那么我们整个重试方案会最多进行51次重试。

  • Apache HttpClient的同步重试能尽可能保证同步的实时性,而如果第三方服务出现短时间不可用的现象,RocketMQ的退避重试也能继续异步重试只到最终成功。

在我们使用了这种重试方案之后,就再也没有听到业务关于电子价签未及时同步或者打卡信息未同步的抱怨了。

以上就是笔者在线下门店系统中的Http重试实践过程,欢迎大家在评论区留言一起交流。


关于作者

侯万兴,转转门店业务后端研发工程师

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~

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

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

相关文章

嵌入式开发服务器与客户端交互 日志2024/7/31

嵌入式开发服务器与客户端交互 客户端 网页 操作 请求相关代码: 这里为了适配 低版本浏览器 用的不是fetch 当然用fetch更好 var curUlr window.location.href; //获取当前网页地址var newURL curUlr.lastIndexOf("/");//截取到最后一个斜杠索引var pathUrl…

mysql 数据库空间统计sql

mysql 数据库空间统计 文章目录 mysql 数据库空间统计说明一、数据库存储代码二、查询某个数据库的所有表的 代码三、列出所有已经产生碎片的表总结 说明 INFORMATION_SCHEMA Table Reference 表参考 information_schema是‌MySQL中的一个特殊数据库&#xff0c;它存储了关于…

MLP多层感知机与Pytorch实现

参考文章&#xff1a; 1.动手学深度学习——多层感知机&#xff08;原理解释代码详解&#xff09;_多层感知机 代码-CSDN博客 2.4.1. 多层感知机 — 动手学深度学习 2.0.0 documentation 3.深度理解多层感知机&#xff08;MLP&#xff09; | 米奇妙妙屋 1. 神经网络由来 神经网…

Scrapy 爬取旅游景点相关数据(七):利用指纹实现“不重复爬取”

本期学习&#xff1a; 利用网页指纹去重 众所周知&#xff0c;代理是要花钱的&#xff0c;那么在爬取&#xff08;测试&#xff09;巨量网页的时候&#xff0c;就不可能对已经爬取过的网站去重复的爬&#xff0c;这样会消耗大量的时间&#xff0c;更重要的是会消耗大量的IP (金…

vite instanceof 失效

背景&#xff1a;给一个巨石单体项目进行标准化模块拆分&#xff0c;封装出来的模块代码用 vite 进行构建&#xff0c;但模块启动后页面上的表现一直和 webpack 那版不一致 一步步 debug 后&#xff0c;发现问题出在下面这个判断条件 const GeneratorFunction function* () …

【Golang 面试 - 基础题】每日 5 题(七)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

Vue 3 中使用 InMap 绘制热力图

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 Vue 3 中使用 InMap 绘制热力图 应用场景介绍 InMap 是一款强大的地图组件库&#xff0c;它提供了一系列丰富的可视化功能&#xff0c;包括热力图。热力图可以将数据点在地图上以颜色编码的方式可视化&#x…

微软:警惕利用VMware ESXi进行身份验证绕过攻击

微软于7月29日发布警告&#xff0c;称勒索软件团伙正在积极利用 VMware ESXi 身份验证绕过漏洞进行攻击。 该漏洞被追踪为 CVE-2024-37085&#xff0c;由微软安全研究人员 Edan Zwick、Danielle Kuznets Nohi 和 Meitar Pinto 发现&#xff0c;并在 6 月 25 日发布的 ESXi 8.0 …

如何学习自动化测试工具!

要学习和掌握自动化测试工具的使用方法&#xff0c;可以按照以下步骤进行&#xff1a; 一、明确学习目标 首先&#xff0c;需要明确你想要学习哪种自动化测试工具。自动化测试工具种类繁多&#xff0c;包括但不限于Selenium、Appium、JMeter、Postman、Robot Framework等&…

docker环境安装kafka/Flink/clickhouse镜像

1、安装Kafka服务 1、将一下三个tar文件复制到ubuntu指定目录下 2、进入到/home/cl/app目录&#xff0c;使用docker命令加载tar镜像文件 # cd /home/cl/app # docker load -i kafka.tar # docker load -i kafka-manager.tar # docker load -i kafka-zookeeper.tar3、查看d…

分布式:RocketMQ/Kafka总结(附下载链接)

文章目录 下载链接思维导图 本文总结的是关于消息队列的常见知识总结。消息队列和分布式系统息息相关&#xff0c;因此这里就将消息队列放到分布式中一并进行处理关联 下载链接 链接: https://pan.baidu.com/s/1hRTh7rSesikisgRUO2GBpA?pwdutgp 提取码: utgp 思维导图

web学习笔记(八十三)git

目录 1.Git的基本概念 2.gitee常用的命令 3.解决两个人操作不同文件造成的冲突 4.解决两个人操作同一个文件造成的冲突 1.Git的基本概念 git是一种管理代码的方式&#xff0c;广泛用于软件开发和版本管理。我们通常使用gitee&#xff08;码云&#xff09;来云管理代码。 …

《Linux运维总结:基于x86_64架构CPU使用docker-compose一键离线部署zookeeper 3.8.4容器版分布式集群》

总结&#xff1a;整理不易&#xff0c;如果对你有帮助&#xff0c;可否点赞关注一下&#xff1f; 更多详细内容请参考&#xff1a;《Linux运维篇&#xff1a;Linux系统运维指南》 一、部署背景 由于业务系统的特殊性&#xff0c;我们需要面对不同的客户部署业务系统&#xff0…

前端如何实现更换项目主题色的功能?

1、场景 有一个换主题色的功能&#xff0c;如下图&#xff1a; 切换颜色后&#xff0c;将对页面所有部分的色值进行重新设置&#xff0c;符合最新的主题色。 2、实现思路 因为色值比较灵活&#xff0c;可以任意选取&#xff0c;所以最好的实现方式是&#xff0c;根据设置的…

全面整理人工智能(AI)学习路线图及资源推荐

在人工智能&#xff08;AI&#xff09;飞速发展的今天&#xff0c;掌握AI技术已经成为了许多高校研究者和职场人士的必备技能。从深度学习到强化学习&#xff0c;从大模型训练到实际应用&#xff0c;AI技术的广度和深度不断拓展。作为一名AI学习者&#xff0c;面对浩瀚的知识海…

递归方法清空多维数组中的null元素(对象)

源码 //【递归】说明&#xff1a;递归方法清空多维数组中的null元素&#xff08;对象&#xff09; let clearNullElementsInArray (arr) > {return (arr || []).filter(v > {if (v null) {return false;} else {if (v.children) {v.children clearNullElementsInArra…

【C语言】Linux 飞翔的小鸟

【C语言】Linux 飞翔的小鸟 零、环境部署 安装Ncurses库 sudo apt-get install libncurses5-dev壹、编写代码 代码如下&#xff1a; bird.c #include<stdio.h> #include<time.h> #include<stdlib.h> #include<signal.h> #include<curses.h>…

科普文:Linux目录详解

在 Linux/Unix 操作系统中&#xff0c;一切都是文件&#xff0c;甚至目录也是文件&#xff0c;文件是文件&#xff0c;鼠标、键盘、打印机等设备也是文件。 这篇文章&#xff0c;我们将一起学习 Linux 中的目录结构及文件。 Linux 的文件类型 Linux系统中的文件系统&#xf…

【初阶数据结构】11.排序(2)

文章目录 2.3 交换排序2.3.1 冒泡排序2.3.2 快速排序2.3.2.1 hoare版本2.3.2.2 挖坑法2.3.2.3 lomuto前后指针2.3.2.4 非递归版本 2.4 归并排序2.5 测试代码&#xff1a;排序性能对比2.6 非比较排序2.6.1 计数排序 3.排序算法复杂度及稳定性分析 2.3 交换排序 交换排序基本思想…

【包邮送书】码农职场:IT人求职就业手册

欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和技术。关…