FailoverCluster源码
源码位于org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker
中:
等等,不对啊,前面刚刚说的是 3 次,怎么一转眼就是 2 次了呢?
你别急啊
你看第 61 行的最后还有一个 "+1" 呢?
你想一想。我们想要在接口调用失败后,重试 n 次,这个 n 就是 DEFAULT_RETRIES ,默认为 2 。那么我们总的调用次数就是 n+1 次了。
所以这个 "+1" 是这样来的,很小的一个知识点,送给大家。
另外图中标记了红色五角星★的地方,第62到64行。也是很关键的地方。对于 retries 参数,在官网上的描述是这样的:
不需要重试,请设为 0 。我们前面分析了,当设置为 0 的时候,只会调用一次。
但是我也看见过retries配置为 -1 的。-1+1=0。调用0次明显是一个错误的含义。但是程序也正常运行,且只调用一次。
这就是标记了红色五角星的地方的功劳了。
防御性编程。哪怕你设置为 -10000 也只会调用一次。
下面这个图片是我对 doInvoke 方法进行一个全面的解读,基本上每一行主要的代码都加了注释,可以点开大图查看:
如上所示,FailoverClusterInvoker 的 doInvoke 方法主要的工作流程是:
- 首先是获取重试次数,然后根据重试次数进行循环调用,在循环体内,如果失败,则进行重试。
- 在循环体内,首先是调用父类 AbstractClusterInvoker 的 select 方法,通过负载均衡组件选择一个 Invoker,然后再通过这个 Invoker 的 invoke 方法进行远程调用。
- 如果失败了,记录下异常,并进行重试。
注意一个细节:在进行重试前,重新获取最新的 invoker 集合,这样做的好处是,如果在重试的过程中某个服务挂了,可以通过调用 list 方法保证 copyInvokers 是最新的可用的 invoker 列表。
整个流程大致如此,不是很难理解。
HttpClient 使用样例
接下来,我们看看 apache 的 HttpClients 中的重试是怎么回事。
也就是这个类:org.apache.http.impl.client.HttpClients
。
首先,废话少说,弄个 Demo 跑一下。
先看 Controller 的逻辑:
@RestController public class TestController { @PostMapping(value = "/testRetry") public void testRetry() { try { System.out.println("时间:" + new Date() + ",数据库插入成功"); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } }
同样是睡眠 5s,模拟超时的情况。
HttpUtils 封装如下:
public class HttpPostUtils { public static String retryPostJson(String uri) throws Exception { HttpPost post = new HttpPost(uri); RequestConfig config = RequestConfig.custom() .setConnectTimeout(1000) .setConnectionRequestTimeout(1000) .setSocketTimeout(1000).build(); post.setConfig(config); String responseContent = null; CloseableHttpResponse response = null; CloseableHttpClient client = null; try { client = HttpClients.custom().build(); response = client.execute(post, HttpClientContext.create()); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { responseContent = EntityUtils.toString(response.getEntity(), Consts.UTF_8.name()); } } finally { if (response != null) { response.close(); } if (client != null){ client.close(); } } return responseContent; } }
先解释一下其中的三个设置为 1000ms 的参数:
connectTimeout:客户端和服务器建立连接的timeout
connectionRequestTimeout:从连接池获取连接的timeout
socketTimeout:客户端从服务器读取数据的timeout
大家都知道一次http请求,抽象来看,必定会有三个阶段
- 一:建立连接
- 二:数据传送
- 三:断开连接
当建立连接的操作,在规定的时间内(ConnectionTimeOut )没有完成,那么此次连接就宣告失败,抛出 ConnectTimeoutException。
后续的 SocketTimeOutException 就一定不会发生。
当连接建立起来后,才会开始进行数据传输,如果数据在规定的时间内(SocketTimeOut)沒有传输完成,则抛出 SocketTimeOutException。如果传输完成,则断开连接。
测试 Main 方法代码如下:
public class MainTest { public static void main(String[] args) { try { String returnStr = HttpPostUtils.retryPostJson("http://127.0.0.1:8080/testRetry/"); System.out.println("returnStr = " + returnStr); } catch (Exception e) { e.printStackTrace(); } } }
首先我们不启动服务,那么根据刚刚的分析,客户端和服务器建立连接会超时,则抛出 ConnectTimeoutException 异常。
直接执行 main 方法,结果如下:
符合我们的预期。
现在我们把 Controller 接口启动起来。
由于我们的 socketTimeout 设置的时间是 1000ms,而接口里面进行了 5s 的睡眠。
根据刚刚的分析,客户端从服务器读取数据肯定会超时,则抛出 SocketTimeOutException 异常。
Controller 接口启动起来后,我们运行 main 方法输出如下:
这个时候,其实接口是调用成功了,只是客户端没有拿到返回。
这个情况和我们前面说的 Dubbo 的情况一样,超时是针对客户端的。
即使客户端超时了,服务端的逻辑还是会继续执行,把此次请求处理完成。
执行结果确实抛出了 SocketTimeOutException 异常,符合预期。
但是,说好的重试呢?