偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

轉(zhuǎn)轉(zhuǎn)門(mén)店基于MQ的Http重試實(shí)踐

開(kāi)發(fā) 前端 網(wǎng)絡(luò)管理
在使用Http請(qǐng)求外部服務(wù)時(shí),由于網(wǎng)絡(luò)的不穩(wěn)定性,第三方接口出現(xiàn)超時(shí)的現(xiàn)象時(shí)有發(fā)生,為了減少對(duì)業(yè)務(wù)造成的影響,我們迫切需要尋找一種Http重試方案。

1 問(wèn)題背景

在線下門(mén)店系統(tǒng)開(kāi)發(fā)中,有很多地方需要使用Http請(qǐng)求和第三方系統(tǒng)進(jìn)行通信,比如將門(mén)店的商品信息同步到第三方的電子價(jià)簽上,再比如需要把門(mén)店店員的打卡信息同步到公司使用的第三方EHR系統(tǒng)中。

但在使用Http請(qǐng)求外部服務(wù)時(shí),由于網(wǎng)絡(luò)的不穩(wěn)定性,第三方接口出現(xiàn)超時(shí)的現(xiàn)象時(shí)有發(fā)生,為了減少對(duì)業(yè)務(wù)造成的影響,我們迫切需要尋找一種Http重試方案。

2 重試方案探索

2.1 簡(jiǎn)單重試

我們最容易想到的一種重試方式是,在請(qǐng)求接口的代碼塊中加入循環(huán),如果請(qǐng)求失敗則繼續(xù)請(qǐng)求,直到請(qǐng)求成功或達(dá)到最大重試次數(shù)。示例代碼如下:

int retryTimes = 3;
  for (int i = 0; i < retryTimes; i++) {
    try {
        // 請(qǐng)求接口的代碼
        break;
      } catch(Exception e) {
        // 處理異常
      }
  }

這種重試方式比較簡(jiǎn)單,只要請(qǐng)求發(fā)生異常就繼續(xù)重試,能在一定程度上解決我們的問(wèn)題,但缺點(diǎn)是對(duì)于異常的捕獲處理邏輯過(guò)于簡(jiǎn)單,重試起來(lái)會(huì)有一定的盲目性。

2.2 Apache HttpClient 重試機(jī)制

我們常用的一些Http客戶端通常也內(nèi)置了一些重試機(jī)制,接下來(lái)我將以我們系統(tǒng)中使用的Apache HttpClient為例,通過(guò)手撕源碼的方式探索一下它內(nèi)部的重試機(jī)制。

通常我們?cè)谑褂肏ttpClient的時(shí)候,都需要以下幾個(gè)步驟;

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

在創(chuàng)建 HttpClient 的過(guò)程中,底層調(diào)用了HttpClientBuilder的build方法,我們直接找到跟重試相關(guān)的邏輯,源碼如下圖:

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

automaticRetriesDisabled默認(rèn)是沒(méi)有禁用的,RetryExec是一個(gè)重試執(zhí)行器,它還需要一個(gè) RetryHandler,如果沒(méi)有指定的話,會(huì)使用DefaultHttpRequestRetryHandler作為默認(rèn)的重試處理器。

我們先來(lái)看一下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;
                }
            }
        }
    }

看到這里,怎么還感覺(jué)到有點(diǎn)眼熟了呢?是不是和我們上面簡(jiǎn)單重試的思路是一樣的呢,有點(diǎn)大道至簡(jiǎn)那個(gè)意思了。

我們來(lái)簡(jiǎn)單總結(jié)一下RetryExec的主要邏輯:在執(zhí)行Http請(qǐng)求的時(shí)候,如果發(fā)生了IOException,會(huì)交給具體的RetryHandler來(lái)處理,然后由它的retryRequest方法來(lái)決定是繼續(xù)重試還是拋出異常。這里可能有的朋友會(huì)有疑問(wèn),為什么是IOException呢?

這就要說(shuō)一下HttpClient的execute方法了,HttpClient執(zhí)行時(shí)可能會(huì)拋出兩種異常:IOException和ClientProtocolException;其中IOException被認(rèn)為是非致命性且可恢復(fù)的,而ClientProtocolException被認(rèn)為是致命性的,不可恢復(fù),所以這里只需要關(guān)注IOException異常即可。

接下來(lái)我們?cè)賮?lái)看一下DefaultHttpRequestRetryHandler,它定義了3個(gè)成員變量:

  • retryCount:重試次數(shù);
  • requestSentRetryEnabled:是否可以在請(qǐng)求成功發(fā)出后重試,這里的成功是指發(fā)送成功,并不指請(qǐng)求成功;
  • nonRetriableClasses:不重試的異常類集合,如果異常為集合中指定的異常時(shí),不會(huì)重試。

DefaultHttpRequestRetryHandler經(jīng)過(guò)一系列構(gòu)造函數(shù),完成了對(duì)三個(gè)成員變量的賦值,其中默認(rèn)的重試次數(shù)是3次,并且默認(rèn)在請(qǐng)求發(fā)送成功之后就不會(huì)再重試,默認(rèn)的不重試異常有以下四類:

  • 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);
    }

然后,我們?cè)賮?lái)看一下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 count
            return 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 idempotent
            return 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 sent
            return true;
        }
        return false;
    }

retryRequest的邏輯也比較簡(jiǎn)單,首先超過(guò)重試次數(shù)就不會(huì)再重試,然后如果是指定不重試的異常也不會(huì)再重試;再然后如果請(qǐng)求方法不是冪等的,也不會(huì)繼續(xù)重試,這里我們熟悉的Post方法顯然是不會(huì)進(jìn)行重試的。不過(guò)還有機(jī)會(huì),這里我們知道requestSentRetryEnabled默認(rèn)是false,也就是說(shuō)只要請(qǐng)求發(fā)送成功之后也不會(huì)進(jìn)行重試。

到這里,我們可以總結(jié)一下了。HttpClient默認(rèn)的RetryHandler中指定了四類異常是不會(huì)進(jìn)行重試的,其中就包含了InterruptedIOException,而實(shí)際上我們經(jīng)常會(huì)遇到的SocketTimeoutException就屬于它的子類。

還有一點(diǎn),如果按照默認(rèn)的重試策略,顯然Post請(qǐng)求也不滿足重試的條件。這里必須說(shuō)一下,從謹(jǐn)慎的角度來(lái)看,Post請(qǐng)求是否應(yīng)該重試,需要具體結(jié)合業(yè)務(wù)場(chǎng)景來(lái)看,如果請(qǐng)求本身不是冪等的,重試確實(shí)可能會(huì)帶來(lái)嚴(yán)重的副作用。

所以在實(shí)際的業(yè)務(wù)場(chǎng)景中,如果想要利用HttpClient的重試機(jī)制來(lái)進(jìn)行重試,這兩個(gè)問(wèn)題都需要解決。

2.3 基于消息隊(duì)列的異步重試方案

考慮到在門(mén)店很多業(yè)務(wù)場(chǎng)景中,執(zhí)行完相關(guān)的邏輯之后都會(huì)發(fā)送MQ消息。那么我們很自然地也想到了通過(guò)引入一個(gè)消費(fèi)者的方式,來(lái)執(zhí)行通過(guò)Http調(diào)用第三方接口的邏輯。

采用這種方式的話,如果在消費(fèi)邏輯中通過(guò)Http調(diào)用第三方接口失敗,我們還可以充分利用MQ的消費(fèi)失敗重試機(jī)制。以我們使用的RocketMQ為例,消息在消費(fèi)失敗重試的時(shí)候會(huì)按照一定的退避時(shí)間來(lái)進(jìn)行重試,這個(gè)特性還能避免第三方服務(wù)因?yàn)槎虝r(shí)間的不可用而造成的重試失敗的情況。

3 門(mén)店業(yè)務(wù)場(chǎng)景中使用的重試方案

經(jīng)過(guò)以上多種方案的調(diào)研,我們最終采用的是方案二和方案三的綜合方案,具體思路如下。

首先,我們整體的重試方案采用基于消息隊(duì)列的異步執(zhí)行方案,一方面是因?yàn)檫@種方案可以充分地做到和業(yè)務(wù)之間解耦,同時(shí)消息隊(duì)列的消費(fèi)失敗重試機(jī)制可以很好地解決第三方服務(wù)短時(shí)間不可用的問(wèn)題,這一點(diǎn)是同步重試方案做不到的,可以保障系統(tǒng)的最終一致性。

其次,因?yàn)槲覀兿到y(tǒng)中已經(jīng)在使用HttpClient 組件,所以我們決定充分利用它的重試機(jī)制,同步重試也可以盡可能保證接口調(diào)用的實(shí)時(shí)性。

考慮到默認(rèn)的重試策略不滿足我們的使用需求,針對(duì)這個(gè)問(wèn)題,我們自定義了一個(gè)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的時(shí)候傳入指定的RetryHandler即可,設(shè)置方式如下:

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

這樣我們就解決了默認(rèn)的重試機(jī)制對(duì)于Post請(qǐng)求默認(rèn)不重試和SocketTimeoutException異常不重試的問(wèn)題,更加貼合我們的使用場(chǎng)景。

這里我舉個(gè)例子來(lái)說(shuō)明一下整個(gè)重試方案的執(zhí)行流程:

  • MQ在消費(fèi)的時(shí)候,會(huì)使用Apache HttpClient請(qǐng)求第三方接口,我們?cè)O(shè)置重試3次,如果請(qǐng)求一直失敗,會(huì)先同步重試3次,如果還是失敗,則本次消息消費(fèi)失敗,等待下一次重試消息繼續(xù)這個(gè)流程。
  • RocketMQ默認(rèn)會(huì)重試16次,那么我們整個(gè)重試方案會(huì)最多進(jìn)行51次重試。
  • Apache HttpClient的同步重試能盡可能保證同步的實(shí)時(shí)性,而如果第三方服務(wù)出現(xiàn)短時(shí)間不可用的現(xiàn)象,RocketMQ的退避重試也能繼續(xù)異步重試只到最終成功。

在我們使用了這種重試方案之后,就再也沒(méi)有聽(tīng)到業(yè)務(wù)關(guān)于電子價(jià)簽未及時(shí)同步或者打卡信息未同步的抱怨了。

以上就是筆者在線下門(mén)店系統(tǒng)中的Http重試實(shí)踐過(guò)程,歡迎大家在評(píng)論區(qū)留言一起交流。

關(guān)于作者

侯萬(wàn)興,轉(zhuǎn)轉(zhuǎn)門(mén)店業(yè)務(wù)后端研發(fā)工程師

責(zé)任編輯:武曉燕 來(lái)源: 轉(zhuǎn)轉(zhuǎn)技術(shù)
相關(guān)推薦

2023-07-27 07:00:01

轉(zhuǎn)轉(zhuǎn)門(mén)店商編程

2024-01-31 22:08:18

分布式重試框架

2024-07-25 19:43:32

2022-11-02 09:02:08

Drools引擎DMN

2023-11-01 07:44:29

轉(zhuǎn)轉(zhuǎn)Flutter業(yè)務(wù)

2023-12-27 19:12:42

OLAP自助分析

2022-11-07 14:45:26

轉(zhuǎn)轉(zhuǎn)價(jià)格DDD

2023-03-22 08:32:35

2022-10-28 09:15:02

2023-03-02 08:54:32

2022-10-28 08:31:43

2023-03-02 08:32:41

2023-02-08 09:42:30

策略方式容量

2022-12-15 08:35:01

用戶畫(huà)像平臺(tái)

2024-06-26 18:58:30

游戲MQ重構(gòu)

2023-06-07 08:32:32

引擎技術(shù)while

2024-06-06 08:18:42

回收業(yè)務(wù)

2023-04-19 13:18:41

動(dòng)態(tài)線程池平臺(tái)

2023-01-04 08:31:10

轉(zhuǎn)轉(zhuǎn)測(cè)試環(huán)境

2024-10-16 21:49:24

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)