learn.lianglianglee.com/专栏/Dubbo源码解读与实战-完/14 重试机制是网络操作的基本保证.md.html
2022-05-11 19:04:14 +08:00

830 lines
35 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>14 重试机制是网络操作的基本保证.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/Dubbo源码解读与实战-完/00 开篇词 深入掌握 Dubbo 原理与实现,提升你的职场竞争力.md.html">00 开篇词 深入掌握 Dubbo 原理与实现,提升你的职场竞争力.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/01 Dubbo 源码环境搭建:千里之行,始于足下.md.html">01 Dubbo 源码环境搭建:千里之行,始于足下.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/02 Dubbo 的配置总线:抓住 URL就理解了半个 Dubbo.md.html">02 Dubbo 的配置总线:抓住 URL就理解了半个 Dubbo.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/03 Dubbo SPI 精析,接口实现两极反转(上).md.html">03 Dubbo SPI 精析,接口实现两极反转(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/04 Dubbo SPI 精析,接口实现两极反转(下).md.html">04 Dubbo SPI 精析,接口实现两极反转(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/05 海量定时任务,一个时间轮搞定.md.html">05 海量定时任务,一个时间轮搞定.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/06 ZooKeeper 与 Curator求你别用 ZkClient 了(上).md.html">06 ZooKeeper 与 Curator求你别用 ZkClient 了(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/07 ZooKeeper 与 Curator求你别用 ZkClient 了(下).md.html">07 ZooKeeper 与 Curator求你别用 ZkClient 了(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/08 代理模式与常见实现.md.html">08 代理模式与常见实现.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/09 Netty 入门,用它做网络编程都说好(上).md.html">09 Netty 入门,用它做网络编程都说好(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/10 Netty 入门,用它做网络编程都说好(下).md.html">10 Netty 入门,用它做网络编程都说好(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/11 简易版 RPC 框架实现(上).md.html">11 简易版 RPC 框架实现(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/12 简易版 RPC 框架实现(下).md.html">12 简易版 RPC 框架实现(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/13 本地缓存:降低 ZooKeeper 压力的一个常用手段.md.html">13 本地缓存:降低 ZooKeeper 压力的一个常用手段.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/Dubbo源码解读与实战-完/14 重试机制是网络操作的基本保证.md.html">14 重试机制是网络操作的基本保证.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/15 ZooKeeper 注册中心实现,官方推荐注册中心实践.md.html">15 ZooKeeper 注册中心实现,官方推荐注册中心实践.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/16 Dubbo Serialize 层:多种序列化算法,总有一款适合你.md.html">16 Dubbo Serialize 层:多种序列化算法,总有一款适合你.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?.md.html">17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/18 Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工.md.html">18 Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/19 Transporter 层核心实现:编解码与线程模型一文打尽(上).md.html">19 Transporter 层核心实现:编解码与线程模型一文打尽(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/20 Transporter 层核心实现:编解码与线程模型一文打尽(下).md.html">20 Transporter 层核心实现:编解码与线程模型一文打尽(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/21 Exchange 层剖析:彻底搞懂 Request-Response 模型(上).md.html">21 Exchange 层剖析:彻底搞懂 Request-Response 模型(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/22 Exchange 层剖析:彻底搞懂 Request-Response 模型(下).md.html">22 Exchange 层剖析:彻底搞懂 Request-Response 模型(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/23 核心接口介绍RPC 层骨架梳理.md.html">23 核心接口介绍RPC 层骨架梳理.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/24 从 Protocol 起手,看服务暴露和服务引用的全流程(上).md.html">24 从 Protocol 起手,看服务暴露和服务引用的全流程(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/25 从 Protocol 起手,看服务暴露和服务引用的全流程(下).md.html">25 从 Protocol 起手,看服务暴露和服务引用的全流程(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/26 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html">26 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/27 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html">27 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/28 复杂问题简单化,代理帮你隐藏了多少底层细节?.md.html">28 复杂问题简单化,代理帮你隐藏了多少底层细节?.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/29 加餐HTTP 协议 + JSON-RPCDubbo 跨语言就是如此简单.md.html">29 加餐HTTP 协议 + JSON-RPCDubbo 跨语言就是如此简单.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/30 Filter 接口,扩展 Dubbo 框架的常用手段指北.md.html">30 Filter 接口,扩展 Dubbo 框架的常用手段指北.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/31 加餐:深潜 Directory 实现,探秘服务目录玄机.md.html">31 加餐:深潜 Directory 实现,探秘服务目录玄机.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/32 路由机制:请求到底怎么走,它说了算(上).md.html">32 路由机制:请求到底怎么走,它说了算(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/33 路由机制:请求到底怎么走,它说了算(下).md.html">33 路由机制:请求到底怎么走,它说了算(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/34 加餐:初探 Dubbo 动态配置的那些事儿.md.html">34 加餐:初探 Dubbo 动态配置的那些事儿.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/35 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上).md.html">35 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/36 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(下).md.html">36 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/37 集群容错:一个好汉三个帮(上).md.html">37 集群容错:一个好汉三个帮(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/38 集群容错:一个好汉三个帮(下).md.html">38 集群容错:一个好汉三个帮(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/39 加餐多个返回值不用怕Merger 合并器来帮忙.md.html">39 加餐多个返回值不用怕Merger 合并器来帮忙.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/40 加餐模拟远程调用Mock 机制帮你搞定.md.html">40 加餐模拟远程调用Mock 机制帮你搞定.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/41 加餐:一键通关服务发布全流程.md.html">41 加餐:一键通关服务发布全流程.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/42 加餐:服务引用流程全解析.md.html">42 加餐:服务引用流程全解析.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/43 服务自省设计方案:新版本新方案.md.html">43 服务自省设计方案:新版本新方案.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/44 元数据方案深度剖析,如何避免注册中心数据量膨胀?.md.html">44 元数据方案深度剖析,如何避免注册中心数据量膨胀?.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/45 加餐:深入服务自省方案中的服务发布订阅(上).md.html">45 加餐:深入服务自省方案中的服务发布订阅(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/46 加餐:深入服务自省方案中的服务发布订阅(下).md.html">46 加餐:深入服务自省方案中的服务发布订阅(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/47 配置中心设计与实现:集中化配置 and 本地化配置,我都要(上).md.html">47 配置中心设计与实现:集中化配置 and 本地化配置,我都要(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/48 配置中心设计与实现:集中化配置 and 本地化配置,我都要(下).md.html">48 配置中心设计与实现:集中化配置 and 本地化配置,我都要(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/49 结束语 认真学习,缩小差距.md.html">49 结束语 认真学习,缩小差距.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>14 重试机制是网络操作的基本保证</h1>
<p>在真实的微服务系统中, ZooKeeper、etcd 等服务发现组件一般会独立部署成一个集群,业务服务通过网络连接这些服务发现节点,完成注册和订阅操作。但即使是机房内部的稳定网络,也无法保证两个节点之间的请求一定成功,因此 Dubbo 这类 RPC 框架在稳定性和容错性方面,就受到了比较大的挑战。<strong>为了保证服务的可靠性,重试机制就变得必不可少了</strong></p>
<p>所谓的 <strong>“重试机制”就是在请求失败时,客户端重新发起一个一模一样的请求,尝试调用相同或不同的服务端,完成相应的业务操作</strong>。能够使用重试机制的业务接口得是“幂等”的,也就是无论请求发送多少次,得到的结果都是一样的,例如查询操作。</p>
<h3>核心设计</h3>
<p>在上一课时中,我们介绍了 AbstractRegistry 中的 register()/unregister()、subscribe()/unsubscribe() 以及 notify() 等核心操作,详细分析了通过<strong>本地缓存</strong>实现的容错功能。其实,这几个核心方法同样也是<strong>重试机制</strong>的关注点。</p>
<p><strong>dubbo-registry 将重试机制的相关实现放到了 AbstractRegistry 的子类—— FailbackRegistry 中</strong>。如下图所示,接入 ZooKeeper、etcd 等开源服务发现组件的 Registry 实现,都继承了 FailbackRegistry也就都拥有了失败重试的能力。</p>
<p><img src="assets/Ciqc1F9bIqGAH2BVAAHKapYWDoE565.png" alt="Registry继承关系.png" /></p>
<p><strong>FailbackRegistry 设计核心是</strong>:覆盖了 AbstractRegistry 中 register()/unregister()、subscribe()/unsubscribe() 以及 notify() 这五个核心方法,结合前面介绍的时间轮,实现失败重试的能力;真正与服务发现组件的交互能力则是放到了 doRegister()/doUnregister()、doSubscribe()/doUnsubscribe() 以及 doNotify() 这五个抽象方法中,由具体子类实现。这是典型的模板方法模式的应用。</p>
<h3>核心字段介绍</h3>
<p>分析一个实现类的第一步就是了解其核心字段,那 FailbackRegistry 的核心字段有哪些呢?</p>
<ul>
<li><strong>retryTimerHashedWheelTimer 类型)</strong>:用于定时执行失败重试操作的时间轮。</li>
<li><strong>retryPeriodint 类型)</strong>:重试操作的时间间隔。</li>
<li><strong>failedRegisteredConcurrentMap&lt;URL, FailedRegisteredTask&gt;类型)</strong>:注册失败的 URL 集合,其中 Key 是注册失败的 URLValue 是对应的重试任务。</li>
<li><strong>failedUnregisteredConcurrentMap&lt;URL, FailedUnregisteredTask&gt;类型)</strong>:取消注册失败的 URL 集合,其中 Key 是取消注册失败的 URLValue 是对应的重试任务。</li>
<li><strong>failedSubscribedConcurrentMap&lt;Holder, FailedSubscribedTask&gt;类型)</strong>:订阅失败 URL 集合,其中 Key 是订阅失败的 URL + Listener 集合Value 是相应的重试任务。</li>
<li><strong>failedUnsubscribedConcurrentMap&lt;URL, Set&gt;类型)</strong>:取消订阅失败的 URL 集合,其中 Key 是取消订阅失败的 URL + Listener 集合Value 是相应的重试任务。</li>
<li><strong>failedNotifiedConcurrentMap&lt;Holder, FailedNotifiedTask&gt;类型)</strong>:通知失败的 URL 集合,其中 Key 是通知失败的 URL + Listener 集合Value 是相应的重试任务。</li>
</ul>
<p>在 FailbackRegistry 的构造方法中,首先会调用父类 AbstractRegistry 的构造方法完成本地缓存相关的初始化操作,然后从传入的 URL 参数中获取重试操作的时间间隔即retry.period 参数)来初始化 retryPeriod 字段,最后初始化 retryTimer****时间轮。整个代码比较简单,这里就不展示了。</p>
<h3>核心方法实现分析</h3>
<p>FailbackRegistry 对 register()/unregister() 方法和 subscribe()/unsubscribe() 方法的具体实现非常类似,所以这里我们就只介绍其中<strong>register() 方法</strong>的具体实现流程。</p>
<ol>
<li>根据 registryUrl 中 accepts 参数指定的匹配模式,决定是否接受当前要注册的 Provider URL。</li>
<li>调用父类 AbstractRegistry 的 register() 方法,将 Provider URL 写入 registered 集合中。</li>
<li>调用 removeFailedRegistered() 方法和 removeFailedUnregistered() 方法,将该 Provider URL 从 failedRegistered 集合和 failedUnregistered 集合中删除,并停止相关的重试任务。</li>
<li>调用 doRegister() 方法,与服务发现组件进行交互。该方法由子类实现,每个子类只负责接入一个特定的服务发现组件。</li>
<li>在 doRegister() 方法出现异常的时候,会根据 URL 参数以及异常的类型,进行分类处理:待注册 URL 的 check 参数为 true默认值为 true待注册的 URL 不是 consumer 协议registryUrl 的 check 参数也为 true默认值为 true。若满足这三个条件或者抛出的异常为 SkipFailbackWrapperException则直接抛出异常。否则就会创建重试任务并添加到 failedRegistered 集合中。</li>
</ol>
<p>明确 register() 方法的核心流程之后,我们再来看 register() 方法的具体代码实现:</p>
<pre><code>public void register(URL url) {
if (!acceptable(url)) {
logger.info(&quot;...&quot;); // 打印相关的提示日志
return;
}
super.register(url); // 完成本地文件缓存的初始化
// 清理failedRegistered集合和failedUnregistered集合并取消相关任务
removeFailedRegistered(url);
removeFailedUnregistered(url);
try {
doRegister(url); // 与服务发现组件进行交互,具体由子类实现
} catch (Exception e) {
Throwable t = e;
// 检测check参数决定是否直接抛出异常
boolean check = getUrl().getParameter(Constants.CHECK_KEY,
true) &amp;&amp; url.getParameter(Constants.CHECK_KEY, true)
&amp;&amp; !CONSUMER_PROTOCOL.equals(url.getProtocol());
boolean skipFailback = t instanceof
SkipFailbackWrapperException;
if (check || skipFailback) {
if (skipFailback) {
t = t.getCause();
}
throw new IllegalStateException(&quot;Failed to register&quot;);
}
// 如果不抛出异常则创建失败重试的任务并添加到failedRegistered集合中
addFailedRegistered(url);
}
}
</code></pre>
<p>从以上代码可以看出,当 Provider 向 Registry 注册 URL 的时候,如果注册失败,且未设置 check 属性,则创建一个定时任务,添加到时间轮中。</p>
<p>下面我们再来看看创建并添加这个重试任务的相关方法——<strong>addFailedRegistered() 方法</strong>,具体实现如下:</p>
<pre><code>private void addFailedRegistered(URL url) {
FailedRegisteredTask oldOne = failedRegistered.get(url);
if (oldOne != null) { // 已经存在重试任务,则无须创建,直接返回
return;
}
FailedRegisteredTask newTask = new FailedRegisteredTask(url,
this);
oldOne = failedRegistered.putIfAbsent(url, newTask);
if (oldOne == null) {
// 如果是新建的重试任务则提交到时间轮中等待retryPeriod毫秒后执行
retryTimer.newTimeout(newTask, retryPeriod,
TimeUnit.MILLISECONDS);
}
}
</code></pre>
<h3>重试任务</h3>
<p>FailbackRegistry.addFailedRegistered() 方法中创建的 FailedRegisteredTask 任务以及其他的重试任务,<strong>都继承了 AbstractRetryTask 抽象类</strong>,如下图所示:</p>
<p><img src="assets/CgqCHl9bIseASX_6AAEchEJzpew190.png" alt="重试任务.png" /></p>
<p>在 AbstractRetryTask 中维护了当前任务关联的 URL、当前重试的次数等信息在其 run() 方法中,会根据重试 URL 中指定的重试次数retry.times 参数,默认值为 3、任务是否被取消以及时间轮的状态决定此次任务的 doRetry() 方法是否正常执行。</p>
<pre><code>public void run(Timeout timeout) throws Exception {
if (timeout.isCancelled() || timeout.timer().isStop() || isCancel()) { // 检测定时任务状态和时间轮状态
return;
}
if (times &gt; retryTimes) { // 检查重试次数
logger.warn(&quot;...&quot;);
return;
}
try {
doRetry(url, registry, timeout); // 执行重试
} catch (Throwable t) {
reput(timeout, retryPeriod); // 重新添加定时任务,等待重试
}
}
</code></pre>
<p>如果任务的 doRetry() 方法执行出现异常AbstractRetryTask 会通过 reput() 方法将当前任务重新放入时间轮中,并递增当前任务的执行次数。</p>
<pre><code>protected void reput(Timeout timeout, long tick) {
if (timeout == null) { // 边界检查
throw new IllegalArgumentException();
}
Timer timer = timeout.timer(); // 检查定时任务
if (timer.isStop() || timeout.isCancelled() || isCancel()) {
return;
}
times++; // 递增times
// 添加定时任务
timer.newTimeout(timeout.task(), tick, TimeUnit.MILLISECONDS);
}
</code></pre>
<p>AbstractRetryTask 将 doRetry() 方法作为抽象方法,留给子类实现具体的重试逻辑,这也是模板方法的使用。</p>
<p>在子类 FailedRegisteredTask 的 doRetry() 方法实现中,会再次执行关联 Registry 的 doRegister() 方法,完成与服务发现组件交互。如果注册成功,则会调用 removeFailedRegisteredTask() 方法将当前关联的 URL 以及当前重试任务从 failedRegistered 集合中删除。如果注册失败,则会抛出异常,执行上文介绍的 reput ()方法重试。</p>
<pre><code>protected void doRetry(URL url, FailbackRegistry registry, Timeout timeout) {
registry.doRegister(url); // 重新注册
registry.removeFailedRegisteredTask(url); // 删除重试任务
}
public void removeFailedRegisteredTask(URL url) {
failedRegistered.remove(url);
}
</code></pre>
<p>另外,在 register() 方法入口处,会主动调用 removeFailedRegistered() 方法和 removeFailedUnregistered() 方法来清理指定 URL 关联的定时任务:</p>
<pre><code>public void register(URL url) {
super.register(url);
removeFailedRegistered(url); // 清理FailedRegisteredTask定时任务
removeFailedUnregistered(url); // 清理FailedUnregisteredTask定时任务
try {
doRegister(url);
} catch (Exception e) {
addFailedRegistered(url);
}
}
</code></pre>
<h3>其他核心方法</h3>
<p>unregister() 方法以及 unsubscribe() 方法的实现方式与 register() 方法类似,只是调用的 do*() 抽象方法、依赖的 AbstractRetryTask 有所不同而已,这里就不再展开细讲。</p>
<p>你还记得上一课时我们介绍的 AbstractRegistry 通过本地文件缓存实现的容错机制吗FailbackRegistry.subscribe() 方法在处理异常的时候,会先获取缓存的订阅数据并调用 notify() 方法,如果没有缓存相应的订阅数据,才会检查 check 参数决定是否抛出异常。</p>
<p>通过上一课时对 AbstractRegistry.notify() 方法的介绍,我们知道其核心逻辑之一就是回调 NotifyListener。下面我们就来看一下 FailbackRegistry 对 notify() 方法的覆盖:</p>
<pre><code>protected void notify(URL url, NotifyListener listener,
List&lt;URL&gt; urls) {
... // 检查url和listener不为空(略)
try {
// FailbackRegistry.doNotify()方法实际上就是调用父类
// AbstractRegistry.notify()方法,没有其他逻辑
doNotify(url, listener, urls);
} catch (Exception t) {
// doNotify()方法出现异常,则会添加一个定时任务
addFailedNotified(url, listener, urls);
}
}
</code></pre>
<p>addFailedNotified() 方法会创建相应的 FailedNotifiedTask 任务,添加到 failedNotified 集合中,同时也会添加到时间轮中等待执行。如果已存在相应的 FailedNotifiedTask 重试任务,则会更新任务需要处理的 URL 集合。</p>
<p>在 FailedNotifiedTask 中维护了一个 URL 集合,用来记录当前任务一次运行需要通知的 URL每执行完一次任务就会清空该集合具体实现如下</p>
<pre><code>protected void doRetry(URL url, FailbackRegistry registry,
Timeout timeout) {
// 如果urls集合为空则会通知所有Listener该任务也就啥都不做了
if (CollectionUtils.isNotEmpty(urls)) {
listener.notify(urls);
urls.clear();
}
reput(timeout, retryPeriod); // 将任务重新添加到时间轮中等待执行
}
</code></pre>
<p>从上面的代码可以看出FailedNotifiedTask 重试任务一旦被添加,就会一直运行下去,但真的是这样吗?在 FailbackRegistry 的 subscribe()、unsubscribe() 方法中,可以看到 removeFailedNotified() 方法的调用,这里就是清理 FailedNotifiedTask 任务的地方。我们以 FailbackRegistry.subscribe() 方法为例进行介绍:</p>
<pre><code>public void subscribe(URL url, NotifyListener listener) {
super.subscribe(url, listener);
removeFailedSubscribed(url, listener); // 关注这个方法
try {
doSubscribe(url, listener);
} catch (Exception e) {
addFailedSubscribed(url, listener);
}
}
// removeFailedSubscribed()方法中会清理FailedSubscribedTask、FailedUnsubscribedTask、FailedNotifiedTask三类定时任务
private void removeFailedSubscribed(URL url, NotifyListener listener) {
Holder h = new Holder(url, listener); // 清理FailedSubscribedTask
FailedSubscribedTask f = failedSubscribed.remove(h);
if (f != null) {
f.cancel();
}
removeFailedUnsubscribed(url, listener);// 清理FailedUnsubscribedTask
removeFailedNotified(url, listener); // 清理FailedNotifiedTask
}
</code></pre>
<p>介绍完 FailbackRegistry 中最核心的注册/订阅实现之后,我们再来关注其实现的恢复功能,也就是 recover() 方法。该方法会直接通过 FailedRegisteredTask 任务处理 registered 集合中的全部 URL通过 FailedSubscribedTask 任务处理 subscribed 集合中的 URL 以及关联的 NotifyListener。</p>
<p>FailbackRegistry 在生命周期结束时,会调用自身的 destroy() 方法,其中除了调用父类的 destroy() 方法之外,还会调用时间轮(即 retryTimer 字段)的 stop() 方法,释放时间轮相关的资源。</p>
<h3>总结</h3>
<p>本课时重点介绍了 AbstractRegistry 的实现类——FailbackRegistry 的核心实现,它主要是在 AbstractRegistry 的基础上,提供了重试机制。具体方法就是通过之前课时介绍的时间轮,在 register()/ unregister()、subscribe()/ unsubscribe() 等核心方法失败时,添加重试定时任务,实现重试机制,同时也添加了相应的定时任务清理逻辑。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Dubbo源码解读与实战-完/13 本地缓存:降低 ZooKeeper 压力的一个常用手段.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Dubbo源码解读与实战-完/15 ZooKeeper 注册中心实现,官方推荐注册中心实践.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70996f3038f13d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>