一、背景

由于工作上的业务本人经常与第三方系统交互,所以经常会使用HttpClient与第三方进行通信。对于交易类的接口,订单状态是至关重要的。

这就牵扯到一系列问题:

HttpClient是否有默认的重试策略?重试策略原理?如何禁止重试?

接下来,本文将从源码中探讨这些问题。源码下载地址:http://hc.apache.org/downloads.cgi,版本是4.5.5。

二、一般使用方法

一般而言,获得HttpClient实例的方法有两种:

  1. 1.HttpClients.custom().setXXX().build()
  2. 2.HttpClients.build()

第一种方法用来定制一些HttpClient的属性,比如https证书,代理服务器,http过滤器,连接池管理器等自定义的用法。

第二种方法用来获得一个默认的HttpClient实例。

这两种方法获得都是CloseableHttpClient实例,且都是通过HttpClientBuilder的build()构建的。

三、有没有重试策略

可以看到,上面的两种用法最终都得到了一个InternalHttpClient,是抽象类CloseableHttpClient的一种实现。

  1. public CloseableHttpClient build() {
  2. //省略若干行
  3. return new InternalHttpClient(
  4. execChain,
  5. connManagerCopy,
  6. routePlannerCopy,
  7. cookieSpecRegistryCopy,
  8. authSchemeRegistryCopy,
  9. defaultCookieStore,
  10. defaultCredentialsProvider,
  11. defaultRequestConfig != null ? defaultRequestConfig : RequestConfig.DEFAULT,
  12. closeablesCopy);
  13. }
  14.  
  15. }

这里有很多配置化参数,这里我们重点关注一下execChain这个执行链。

可以看到执行链有多种实现,比如

  1. RedirectExec执行器的默认策略是,在接收到重定向错误码301与307时会继续访问重定向的地址
  2. 以及我们关注的RetryExec可以重试的执行器。

这么多执行器,是怎么用到了重试执行器呢?

  1. public CloseableHttpClient build() {
  2. //省略一些代码
  3. // Add request retry executor, if not disabled
  4. if (!automaticRetriesDisabled) {
  5. HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
  6. if (retryHandlerCopy == null) {
  7. retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
  8. }
  9. execChain = new RetryExec(execChain, retryHandlerCopy);
  10. }
  11. }

可以看到在build() httpclient实例的时候,判断了是否关闭了自动重试,这个automaticRetriesDisabled类型是boolean,默认值是false,所以if这里是满足的。

即如果没有指定执行链,就是用RetryExec执行器,默认的重试策略是DefaultHttpRequestRetryHandler。

前面已经看到我们使用的HttiClient本质上是InternalHttpClient,这里看下他的执行发送数据的方法。

  1. @Override
  2. protected CloseableHttpResponse doExecute(
  3. final HttpHost target,
  4. final HttpRequest request,
  5. final HttpContext context) throws IOException, ClientProtocolException {
  6. //省略一些代码
    return this.execChain.execute(route, wrapper, localcontext, execAware);
  7. }
  8. }

最后一行可以看到,最终的执行execute方式使用的是exeChain的执行方法,而execChain是通过InternalHttpClient构造器传进来的,就是上面看到的RetryExec。

所以,HttpClient有默认的执行器RetryExec,其默认的重试策略是DefaultHttpRequestRetryHandler。

四、重试策略分析

4.1 是否需要重试的判断在哪里?

http请求是执行器执行的,所以先看RetryExec发送请求的部分。

  1. public CloseableHttpResponse execute(
  2. final HttpRoute route,
  3. final HttpRequestWrapper request,
  4. final HttpClientContext context,
  5. final HttpExecutionAware execAware) throws IOException, HttpException {
  6. //参数校验
  7. Args.notNull(route, "HTTP route");
  8. Args.notNull(request, "HTTP request");
  9. Args.notNull(context, "HTTP context");
  10. final Header[] origheaders = request.getAllHeaders();
  11. //这个for循环记录了当前http请求的执行次数
  12. for (int execCount = 1;; execCount++) {
  13. try {
  14.           //调用基础executor执行http请求
  15. return this.requestExecutor.execute(route, request, context, execAware);
  16. } catch (final IOException ex) {
  17.           //发生IO异常的时候,判断上下文是否已经中断,如果中断则抛异常退出
  18. if (execAware != null && execAware.isAborted()) {
  19. this.log.debug("Request has been aborted");
  20. throw ex;
  21. }
  22. //根据重试策略,判断当前执行状况是否要重试,如果是则进入下面逻辑
  23. if (retryHandler.retryRequest(ex, execCount, context)) {
  24.             //日志
  25. if (this.log.isInfoEnabled()) {
  26. this.log.info("I/O exception ("+ ex.getClass().getName() +
  27. ") caught when processing request to "
  28. + route +
  29. ": "
  30. + ex.getMessage());
  31. }
  32.             //日志
  33. if (this.log.isDebugEnabled()) {
  34. this.log.debug(ex.getMessage(), ex);
  35. }
  36.             //判断当前请求是否可以被重复发起
  37. if (!RequestEntityProxy.isRepeatable(request)) {
  38. this.log.debug("Cannot retry non-repeatable request");
  39. throw new NonRepeatableRequestException("Cannot retry request " +
  40. "with a non-repeatable request entity", ex);
  41. }
  42. request.setHeaders(origheaders);
  43. if (this.log.isInfoEnabled()) {
  44. this.log.info("Retrying request to " + route);
  45. }
  46. } else {
  47.             //如果重试策略判断不能重试了,则根据异常状态抛异常,退出当前流程
  48. if (ex instanceof NoHttpResponseException) {
  49. final NoHttpResponseException updatedex = new NoHttpResponseException(
  50. route.getTargetHost().toHostString() + " failed to respond");
  51. updatedex.setStackTrace(ex.getStackTrace());
  52. throw updatedex;
  53. } else {
  54. throw ex;
  55. }
  56. }
  57. }
  58. }
  59. }

关于RetryExec执行器的执行过程,做一个阶段小结:

  1. RetryExec在执行http请求的时候使用的是底层的基础代码MainClientExec,并记录了发送次数
  2. 当发生IOException的时候,判断是否要重试
    1.   首先是根据重试策略DefaultHttpRequestRetryHandler判断,如果可以重试就继续
      1.      判断当前request是否还可以再次发起
    2.   如果重试策略判断不可以重试了,就抛相应异常并退出

4.2 DefaultHttpRequestRetryHandler的重试策略

在上文我们看到了默认的重试策略是DefaultHttpRequestRetryHandler.INSTANCE。

  1. //单例模式
  2. public static final DefaultHttpRequestRetryHandler INSTANCE = new DefaultHttpRequestRetryHandler();
  3.  
  4. //重试次数
  5. private final int retryCount;
  6.  
  7. //如果一个请求发送成功过,是否还会被再次发送
  8. private final boolean requestSentRetryEnabled;
  9.  
  10. private final Set<Class<? extends IOException>> nonRetriableClasses;
  11.  
  12. public DefaultHttpRequestRetryHandler() {
  13. this(3, false);
  14. }
  15.  
  16. public DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) {
  17. this(retryCount, requestSentRetryEnabled, Arrays.asList(
  18. InterruptedIOException.class,
  19. UnknownHostException.class,
  20. ConnectException.class,
  21. SSLException.class));
  22. }
  23. protected DefaultHttpRequestRetryHandler(
  24. final int retryCount,
  25. final boolean requestSentRetryEnabled,
  26. final Collection<Class<? extends IOException>> clazzes) {
  27. super();
  28. this.retryCount = retryCount;
  29. this.requestSentRetryEnabled = requestSentRetryEnabled;
  30. this.nonRetriableClasses = new HashSet<Class<? extends IOException>>();
  31. for (final Class<? extends IOException> clazz: clazzes) {
  32. this.nonRetriableClasses.add(clazz);
  33. }
  34. }

通过构造器可以看到,默认的重试策略是:

  1. 重试3次
  2. 如果请求被成功发送过,就不再重试了
  3. InterruptedIOException、UnknownHostException、ConnectException、SSLException,发生这4中异常不重试

说句题外话,这是一个单例模式,属于饿汉模式。

饿汉模式的缺点是,这个类在被加载的时候就会初始化这个对象,对内存有占用。不过这个对象维护的filed比较小,所以对内存的影响不大。

另外由于这个类所有的field都是final的,所以是一个不可变的对象,是线程安全的。

  1. public boolean retryRequest(
  2. final IOException exception,
  3. final int executionCount,
  4. final HttpContext context) {
  5. //参数校验
  6. Args.notNull(exception, "Exception parameter");
  7. Args.notNull(context, "HTTP context");
  8.      //如果已经执行的次数大于设置的次数,则不继续重试
  9. if (executionCount > this.retryCount) {
  10. return false;
  11. }
  12.      //如果是上面规定的几种异常,则不重试
  13. if (this.nonRetriableClasses.contains(exception.getClass())) {
  14. return false;
  15. } else {
  16.        //如果是上面规定的集中异常的子类,则不重试
  17. for (final Class<? extends IOException> rejectException : this.nonRetriableClasses) {
  18. if (rejectException.isInstance(exception)) {
  19. return false;
  20. }
  21. }
  22. }
  23. final HttpClientContext clientContext = HttpClientContext.adapt(context);
  24. final HttpRequest request = clientContext.getRequest();
  25.      //判断当前请求是否已经被终止了,这个是避免当前请求被放入异步的异步的HttpRequestFutureTask中
  26.      //跟进去可以看到,当这个异步任务被cancel的时候,会通过AtomicBoolean的compareAndSet的方法,保证状态被更改
  27.      //这部分不做详细讨论了
  28. if(requestIsAborted(request)){
  29. return false;
  30. }
  31.      //判断请求是否是幂等请求,跟进去可以看到,所有包含http body的请求都认为是非幂等的,比如post/put等
  32.      //幂等的请求可以直接重试,比如get
  33. if (handleAsIdempotent(request)) {
  34. return true;
  35. }
  36.      //根据上下文判断请求是否发送成功了,或者根据状态为是否永远可以重复发送(默认的是否)
  37.      //这个下面会分析
  38. if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) {
  39. return true;
  40. }
  41. //否则不需要重试
  42. return false;
  43. }
  44. }

关于默认的重试策略,做一个阶段小结:

  1. 如果重试超过3次,则不再重试
  2. 几种特殊异常及其子类,不进行重试
  3. 同一个请求在异步任务重已经被终止,则不进行重试
  4. 幂等的方法可以进行重试,比如Get
  5. 如果请求没有发送成功,可以进行重试。

那么关键问题来了,如何判断请求是否已经发送成功了呢?

  1. public static final String HTTP_REQ_SENT = "http.request_sent";
  2.  
  3. public boolean isRequestSent() {
  4. final Boolean b = getAttribute(HTTP_REQ_SENT, Boolean.class);
  5. return b != null && b.booleanValue();
  6. }

可看到如果当前的httpContext中的http.request_sent属性为true,则认为已经发送成功,否则认为还没有发送成功。

那么就剩下一个问题了,一次正常的http请求中http.request_sent属性是如果设置的?

上面有提到过,RetryExec在底层通信使用了MainClientExec,而MainCLientExec底层调用了HttpRequestExecutor.doSendRequest()

  1. protected HttpResponse doSendRequest(
  2. final HttpRequest request,
  3. final HttpClientConnection conn,
  4. final HttpContext context) throws IOException, HttpException {
  5. Args.notNull(request, "HTTP request");
  6. Args.notNull(conn, "Client connection");
  7. Args.notNull(context, "HTTP context");
  8.  
  9. HttpResponse response = null;
  10.  
  11. context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn);
  12.      //首先在请求发送之前,将http.request_sent放入上下文context的属性中,值为false
  13. context.setAttribute(HttpCoreContext.HTTP_REQ_SENT, Boolean.FALSE);
  14.      //将request的Header放入连接中
  15. conn.sendRequestHeader(request);
  16. //如果是post/put这种有body的请求,需要先判断100-cotinue扩展协议是否支持
  17.      //即发送包含body请求前,先判断服务端是否支持同样的协议如果不支持,则不发送了。除非特殊约定,默认双端是都不设置的。
  18. if (request instanceof HttpEntityEnclosingRequest) {
  19. boolean sendentity = true;
  20. final ProtocolVersion ver =
  21. request.getRequestLine().getProtocolVersion();
  22. if (((HttpEntityEnclosingRequest) request).expectContinue() &&
  23. !ver.lessEquals(HttpVersion.HTTP_1_0)) {
  24. conn.flush();
  25. if (conn.isResponseAvailable(this.waitForContinue)) {
  26. response = conn.receiveResponseHeader();
  27. if (canResponseHaveBody(request, response)) {
  28. conn.receiveResponseEntity(response);
  29. }
  30. final int status = response.getStatusLine().getStatusCode();
  31. if (status < 200) {
  32. if (status != HttpStatus.SC_CONTINUE) {
  33. throw new ProtocolException(
  34. "Unexpected response: " + response.getStatusLine());
  35. }
  36. // discard 100-continue
  37. response = null;
  38. } else {
  39. sendentity = false;
  40. }
  41. }
  42. }
  43.        //如果可以发送,则将body序列化后,写入当前流中
  44. if (sendentity) {
  45. conn.sendRequestEntity((HttpEntityEnclosingRequest) request);
  46. }
  47. }
  48.      //刷新当前连接,发送数据
  49. conn.flush();
  50.      //将http.request_sent置为true
  51. context.setAttribute(HttpCoreContext.HTTP_REQ_SENT, Boolean.TRUE);
  52. return response;
  53. }

上面是一个完成的http通信部分,步骤如下:

  1. 开始前将http.request_sent置为false
  2. 通过流flush数据到服务端
  3. 然后将http.request_sent置为true

显然,对于conn.flush()这一步是会发生异常的,这种情况下就认为没有发送成功。

说句题外话,上面对coon的操作都是基于连接池的,每次都是从池中拿到一个可用连接。

五、重试策略对业务的影响

5.1 我们的业务重试了吗?

  对于我们的场景应用中的get与post,可以总结为:

  1. 只有发生IOExecetion时才会发生重试
  2. InterruptedIOException、UnknownHostException、ConnectException、SSLException,发生这4中异常不重试
  3. get方法可以重试3次,post方法在socket对应的输出流没有被write并flush成功时可以重试3次。

  首先分析下不重试的异常:

  1. InterruptedIOException,线程中断异常
  2. UnknownHostException,找不到对应host
  3. ConnectException,找到了host但是建立连接失败。
  4. SSLException,https认证异常

另外,我们还经常会提到两种超时,连接超时与读超时:

  1. java.net.SocketTimeoutException: Read timed out
  2. java.net.SocketTimeoutException: connect timed out

这两种超时都是SocketTimeoutException,继承自InterruptedIOException,属于上面的第1种线程中断异常,不会进行重试。

5.2 哪些场景会进行重试?

对于大多数系统而言,很多交互都是通过post的方式与第三方交互的。

所以,我们需要知道有哪些情况HttpClient给我们进行了默认重试。

我们关心的场景转化为,post请求在输出流进行write与flush的时候,会发生哪些除了InterruptedIOException、UnknownHostException、ConnectException、SSLException以外的IOExecetion。

可能出问题的一步在于HttpClientConnection.flush()的一步,跟进去可以得知其操作的对象是一个SocketOutputStream,而这个类的flush是空实现,所以只需要看wirte方法即可。

  1. private void socketWrite(byte b[], int off, int len) throws IOException {
  2.  
  3. if (len <= 0 || off < 0 || len > b.length - off) {
  4. if (len == 0) {
  5. return;
  6. }
  7. throw new ArrayIndexOutOfBoundsException("len == " + len
  8. + " off == " + off + " buffer length == " + b.length);
  9. }
  10.  
  11. FileDescriptor fd = impl.acquireFD();
  12. try {
  13. socketWrite0(fd, b, off, len);
  14. } catch (SocketException se) {
  15. if (se instanceof sun.net.ConnectionResetException) {
  16. impl.setConnectionResetPending();
  17. se = new SocketException("Connection reset");
  18. }
  19. if (impl.isClosedOrPending()) {
  20. throw new SocketException("Socket closed");
  21. } else {
  22. throw se;
  23. }
  24. } finally {
  25. impl.releaseFD();
  26. }
  27. }

可以看到,这个方法会抛出IOExecption,代码中对SocketException异常进行了加工。从之前的分析中可以得知,SocketException是不在可以忽略的范围内的。

所以从上面代码上就可以分析得出对于传输过程中socket被重置或者关闭的时候,httpclient会对post请求进行重试。

以及一些其他的IOExecption也会进行重试,不过范围过广不好定位。

六、如何禁止重试?

回到HttpClientBuilder中,其build()方法中之所以选择了RetryExec执行器是有前置条件的,即没有手动禁止。

  1. // Add request retry executor, if not disabled
  2. if (!automaticRetriesDisabled) {
  3. HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
  4. if (retryHandlerCopy == null) {
  5. retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
  6. }
  7. execChain = new RetryExec(execChain, retryHandlerCopy);
  8. }

所以我们在构建httpClient实例的时候手动禁止掉即可。

  1. /**
  2. * Disables automatic request recovery and re-execution.
  3. */
  4. public final HttpClientBuilder disableAutomaticRetries() {
  5. automaticRetriesDisabled = true;
  6. return this;
  7. }

七、本文总结

通过本文分析,可以得知HttpClient默认是有重试机制的,其重试策略是:

  1.只有发生IOExecetion时才会发生重试

  2.InterruptedIOException、UnknownHostException、ConnectException、SSLException,发生这4中异常不重试

  3.get方法可以重试3次,post方法在socket对应的输出流没有被write并flush成功时可以重试3次。

  4.读/写超时不进行重试

  5.socket传输中被重置或关闭会进行重试

  6.以及一些其他的IOException,暂时分析不出来。

关于HttpClient重试策略的研究的更多相关文章

  1. Azure Storage Client Library 重试策略建议

    有关如何配置 Azure Storage Library 重试策略的信息,可参阅 Gaurav Mantri 撰写的一篇不错的文章<SCL 2.0 – 实施重试策略>.但很难找到关于使用何 ...

  2. nodejs异步请求重试策略总结

    对于node开发同学经常要处理异步请求,然后根据请求的结果或请求成功后的状态码做不同的策略处理,众多策略中最常用的一种就是重试策略.针对重试策略我们往往还需要设定一定的规则,如重试次数.重试时间间隔. ...

  3. Polly一种.NET弹性和瞬态故障处理库(重试策略、断路器、超时、隔板隔离、缓存、回退、策略包装)

    下载地址:https://github.com/App-vNext/Polly 该库实现了七种恢复策略. 重试策略(Retry) 重试策略针对的前置条件是短暂的故障延迟且在短暂的延迟之后能够自我纠正. ...

  4. .NET Core 微服务之Polly重试策略

    接着上一篇说,正好也是最近项目里用到了,正好拿过来整理一下,园子里也有一些文章介绍比我详细. 简单介绍一下绍轻量的故障处理库 Polly  Polly是一个.NET弹性和瞬态故障处理库 允许我们以非常 ...

  5. Net中HttpClient 重试

    /// <summary>         /// 重试         /// </summary>         public class RetryHandler : ...

  6. feginclient和ribbon的重试策略

    //自定义重试次数// @Bean// public Retryer feignRetryer(){// Retryer retryer = new Retryer.Default(100, 1000 ...

  7. C# 异常重试策略

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...

  8. Polly 重试策略

    工作原理 Retry 基本重试: public static void Retry() { var random = new Random(); // Policy<> 泛型定义返回值类型 ...

  9. spring的重试策略、发生异常会自动重新调用

    测试类以及测试代码.复制即可 package com.cms.util; import javax.swing.plaf.synth.SynthSpinnerUI; import org.spring ...

随机推荐

  1. python 操作Memcached

    启动Memcached memcached -d -m 10 -u root -l 10.211.55.4 -p 12000 -c 256 -P /tmp/memcached.pid 参数说明: -d ...

  2. RxSwift:ReactiveX for Swift 翻译

    RxSwift:ReactiveX for Swift 翻译 字数1787 阅读269 评论3 喜欢3 图片发自简书App RxSwift | |-LICENSE.md |-README.md |-R ...

  3. aix 6.1系统怎么安装?这里有详细图文教程

    今年六月,我们公司出现了一次非常严重的数据丢失的事故.生产服务器崩溃导致所有的业务都陷于停滞,而且由于涉及到公司机密又无法贸然到数据恢复公司进行恢复,可是自己又无法解决.权衡利弊还是决定找一家有保密资 ...

  4. centos7 编译安装greenplum5.7

    一.配置系统 安装是以一个主节点,三个子节点进行安装.gp是在github上下载的5.7的源码.地址https://github.com/greenplum-db/gpdb/tree/5.7.0. 1 ...

  5. sql 多条记录插入

    --多条记录插入,用逗号分开值. INSERT dbo.studentinfor ( id, name, class, age, hpsw ) ', -- id - nvarchar(50) N'te ...

  6. JAVA_SE基础——19.数组的定义

    数组是一组相关数据的集合,数组按照使用可以分为一维数组.二维数组.多维数组 本章先讲一维数组 不同点: 不使用数组定义100个整形变量:int1,int2,int3;;;;;; 使用数组定义 int ...

  7. python识别验证码——PIL,pytesser,pytesseract的安装

    1.使用Python识别验证码需要安装Python的图像处理模块(PIL.pytesser.pytesseract) (安装过程需要pip,在我的Python中已经安装pip了,pip的安装就不在赘述 ...

  8. spring5——Aop的实现原理(动态代理)

    spring框架的核心之一AOP,面向切面编程是一种编程思想.我对于面向切面编程的理解是:可以让我们动态的控制程序的执行流程及执行结果.spring框架对AOP的实现是为了使业务逻辑之间实现分离,分离 ...

  9. CWMP开源代码研究番外篇——博通方案

    声明:本篇文章来自于某公司Cable Modem产品的文档资料,源码来自于博通公司,只提供参考(为保护产权,本人没有源码). 前文曾提到会写一篇关于博通的tr069,那么福利来了.福利,福利,福利,重 ...

  10. spark2.1:使用df.select(when(a===b,1).otherwise(0))替换(case when a==b then 1 else 0 end)

    最近工作中把一些sql.sh脚本执行hive的语句升级为spark2.1版本,其中遇到将case when 替换为scala操作df的方式实现的问题: 代码数据: scala> import o ...