learn.lianglianglee.com/专栏/Dubbo源码解读与实战-完/44 元数据方案深度剖析,如何避免注册中心数据量膨胀?.md.html
2022-09-06 22:30:37 +08:00

900 lines
58 KiB
HTML
Raw 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>44 元数据方案深度剖析,如何避免注册中心数据量膨胀?.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 原理与实现,提升你的职场竞争力</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/01 Dubbo 源码环境搭建:千里之行,始于足下.md.html">01 Dubbo 源码环境搭建:千里之行,始于足下</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/02 Dubbo 的配置总线:抓住 URL就理解了半个 Dubbo.md.html">02 Dubbo 的配置总线:抓住 URL就理解了半个 Dubbo</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/03 Dubbo SPI 精析,接口实现两极反转(上).md.html">03 Dubbo SPI 精析,接口实现两极反转(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/04 Dubbo SPI 精析,接口实现两极反转(下).md.html">04 Dubbo SPI 精析,接口实现两极反转(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/05 海量定时任务,一个时间轮搞定.md.html">05 海量定时任务,一个时间轮搞定</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/06 ZooKeeper 与 Curator求你别用 ZkClient 了(上).md.html">06 ZooKeeper 与 Curator求你别用 ZkClient 了(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/07 ZooKeeper 与 Curator求你别用 ZkClient 了(下).md.html">07 ZooKeeper 与 Curator求你别用 ZkClient 了(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/08 代理模式与常见实现.md.html">08 代理模式与常见实现</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/09 Netty 入门,用它做网络编程都说好(上).md.html">09 Netty 入门,用它做网络编程都说好(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/10 Netty 入门,用它做网络编程都说好(下).md.html">10 Netty 入门,用它做网络编程都说好(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/11 简易版 RPC 框架实现(上).md.html">11 简易版 RPC 框架实现(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/12 简易版 RPC 框架实现(下).md.html">12 简易版 RPC 框架实现(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/13 本地缓存:降低 ZooKeeper 压力的一个常用手段.md.html">13 本地缓存:降低 ZooKeeper 压力的一个常用手段</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/14 重试机制是网络操作的基本保证.md.html">14 重试机制是网络操作的基本保证</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/15 ZooKeeper 注册中心实现,官方推荐注册中心实践.md.html">15 ZooKeeper 注册中心实现,官方推荐注册中心实践</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/16 Dubbo Serialize 层:多种序列化算法,总有一款适合你.md.html">16 Dubbo Serialize 层:多种序列化算法,总有一款适合你</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?.md.html">17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/18 Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工.md.html">18 Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/19 Transporter 层核心实现:编解码与线程模型一文打尽(上).md.html">19 Transporter 层核心实现:编解码与线程模型一文打尽(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/20 Transporter 层核心实现:编解码与线程模型一文打尽(下).md.html">20 Transporter 层核心实现:编解码与线程模型一文打尽(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/21 Exchange 层剖析:彻底搞懂 Request-Response 模型(上).md.html">21 Exchange 层剖析:彻底搞懂 Request-Response 模型(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/22 Exchange 层剖析:彻底搞懂 Request-Response 模型(下).md.html">22 Exchange 层剖析:彻底搞懂 Request-Response 模型(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/23 核心接口介绍RPC 层骨架梳理.md.html">23 核心接口介绍RPC 层骨架梳理</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/24 从 Protocol 起手,看服务暴露和服务引用的全流程(上).md.html">24 从 Protocol 起手,看服务暴露和服务引用的全流程(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/25 从 Protocol 起手,看服务暴露和服务引用的全流程(下).md.html">25 从 Protocol 起手,看服务暴露和服务引用的全流程(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/26 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html">26 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/27 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html">27 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/28 复杂问题简单化,代理帮你隐藏了多少底层细节?.md.html">28 复杂问题简单化,代理帮你隐藏了多少底层细节?</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/29 加餐HTTP 协议 + JSON-RPCDubbo 跨语言就是如此简单.md.html">29 加餐HTTP 协议 + JSON-RPCDubbo 跨语言就是如此简单</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/30 Filter 接口,扩展 Dubbo 框架的常用手段指北.md.html">30 Filter 接口,扩展 Dubbo 框架的常用手段指北</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/31 加餐:深潜 Directory 实现,探秘服务目录玄机.md.html">31 加餐:深潜 Directory 实现,探秘服务目录玄机</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/32 路由机制:请求到底怎么走,它说了算(上).md.html">32 路由机制:请求到底怎么走,它说了算(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/33 路由机制:请求到底怎么走,它说了算(下).md.html">33 路由机制:请求到底怎么走,它说了算(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/34 加餐:初探 Dubbo 动态配置的那些事儿.md.html">34 加餐:初探 Dubbo 动态配置的那些事儿</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/35 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上).md.html">35 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/36 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(下).md.html">36 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/37 集群容错:一个好汉三个帮(上).md.html">37 集群容错:一个好汉三个帮(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/38 集群容错:一个好汉三个帮(下).md.html">38 集群容错:一个好汉三个帮(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/39 加餐多个返回值不用怕Merger 合并器来帮忙.md.html">39 加餐多个返回值不用怕Merger 合并器来帮忙</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/40 加餐模拟远程调用Mock 机制帮你搞定.md.html">40 加餐模拟远程调用Mock 机制帮你搞定</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/41 加餐:一键通关服务发布全流程.md.html">41 加餐:一键通关服务发布全流程</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/42 加餐:服务引用流程全解析.md.html">42 加餐:服务引用流程全解析</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/43 服务自省设计方案:新版本新方案.md.html">43 服务自省设计方案:新版本新方案</a>
</li>
<li>
<a class="current-tab" href="/专栏/Dubbo源码解读与实战-完/44 元数据方案深度剖析,如何避免注册中心数据量膨胀?.md.html">44 元数据方案深度剖析,如何避免注册中心数据量膨胀?</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/45 加餐:深入服务自省方案中的服务发布订阅(上).md.html">45 加餐:深入服务自省方案中的服务发布订阅(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/46 加餐:深入服务自省方案中的服务发布订阅(下).md.html">46 加餐:深入服务自省方案中的服务发布订阅(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/47 配置中心设计与实现:集中化配置 and 本地化配置,我都要(上).md.html">47 配置中心设计与实现:集中化配置 and 本地化配置,我都要(上)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/48 配置中心设计与实现:集中化配置 and 本地化配置,我都要(下).md.html">48 配置中心设计与实现:集中化配置 and 本地化配置,我都要(下)</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/49 结束语 认真学习,缩小差距.md.html">49 结束语 认真学习,缩小差距</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>44 元数据方案深度剖析,如何避免注册中心数据量膨胀?</h1>
<p>在上一课时,我们详细介绍了 Dubbo 传统架构面临的挑战,以及 Dubbo 2.7.5 版本引入的服务自省方案是如何应对这些挑战的。</p>
<p>本课时我们将从服务自省方案的基础设施开始介绍其具体实现。我们首先会介绍元数据相关的基础类的定义,然后介绍元数据的上报以及元数据服务的相关内容,同时还会介绍 Service ID 与 Service Name 是如何映射的。</p>
<h3>ServiceInstance</h3>
<p><strong>Service Instance 唯一标识一个服务实例</strong>,在 Dubbo 的源码中对应 ServiceInstance 接口,该接口的具体定义如下:</p>
<pre><code>public interface ServiceInstance extends Serializable {
// 唯一标识
String getId();
// 获取当前ServiceInstance所属的Service Name
String getServiceName();
// 获取当前ServiceInstance的host
String getHost();
// 获取当前ServiceInstance的port
Integer getPort();
// 当前ServiceInstance的状态
default boolean isEnabled() {
return true;
}
// 检测当前ServiceInstance的状态
default boolean isHealthy() {
return true;
}
// 获取当前ServiceInstance关联的元数据这些元数据以KV格式存储
Map&lt;String, String&gt; getMetadata();
// 计算当前ServiceInstance对象的hashCode值
int hashCode();
// 比较两个ServiceInstance对象
boolean equals(Object another);
}
</code></pre>
<p><strong>DefaultServiceInstance 是 ServiceInstance 的唯一实现</strong>DefaultServiceInstance 是一个普通的 POJO 类,其中的核心字段如下。</p>
<ul>
<li>idString 类型ServiceInstance 唯一标识。</li>
<li>serviceNameString 类型ServiceInstance 关联的 Service Name。</li>
<li>hostString 类型ServiceInstance 的 host。</li>
<li>portInteger 类型ServiceInstance 的 port。</li>
<li>enabledboolean 类型ServiceInstance 是否可用的状态。</li>
<li>healthyboolean 类型ServiceInstance 的健康状态。</li>
<li>metadataMap&lt;String, String&gt; 类型ServiceInstance 关联的元数据。</li>
</ul>
<h3>ServiceDefinition</h3>
<p>Dubbo 元数据服务与我们业务中发布的 Dubbo 服务无异,<strong>Consumer 端可以调用一个 ServiceInstance 的元数据服务获取其发布的全部服务的元数据</strong></p>
<p>说到元数据,就不得不提到 ServiceDefinition 这个类,它可以来描述一个服务接口的定义,其核心字段如下。</p>
<ul>
<li>canonicalNameString 类型):接口的完全限定名称。</li>
<li>codeSourceString 类型):服务接口所在的完整路径。</li>
<li>methodsList 类型):接口中定义的全部方法描述信息。在 MethodDefinition 中记录了方法的名称、参数类型、返回值类型以及方法参数涉及的所有 TypeDefinition。</li>
<li>typesList 类型接口定义中涉及的全部类型描述信息包括方法的参数和字段如果遇到复杂类型TypeDefinition 会递归获取复杂类型内部的字段。在 dubbo-metadata-api 模块中,提供了多种类型对应的 TypeBuilder 用于创建对应的 TypeDefinition对于没有特定 TypeBuilder 实现的类型,会使用 DefaultTypeBuilder。</li>
</ul>
<p><img src="assets/CgqCHl_lrYOAIzjeAAFEk3cUdpg570.png" alt="png" /></p>
<p>TypeBuilder 接口实现关系图</p>
<p><strong>在服务发布的时候,会将服务的 URL 中的部分数据封装为 FullServiceDefinition 对象,然后作为元数据存储起来</strong>。FullServiceDefinition 继承了 ServiceDefinition并在 ServiceDefinition 基础之上扩展了 params 集合Map&lt;String, String&gt; 类型),用来存储 URL 上的参数。</p>
<h3>MetadataService</h3>
<p>接下来看 MetadataService 接口,在上一讲我们提到<strong>Dubbo 中的每个 ServiceInstance 都会发布 MetadataService 接口供 Consumer 端查询元数据</strong>,下图展示了 MetadataService 接口的继承关系:</p>
<p><img src="assets/CgpVE1_lrZGANC4vAAGdcllZU9o940.png" alt="png" /></p>
<p>MetadataService 接口继承关系图</p>
<p>在 MetadataService 接口中定义了查询当前 ServiceInstance 发布的元数据的相关方法,具体如下所示:</p>
<pre><code>public interface MetadataService {
String serviceName(); // 获取当前ServiceInstance所属服务的名称
default String version() {
return VERSION; // 获取当前MetadataService接口的版本
}
// 获取当前ServiceInstance订阅的全部URL
default SortedSet&lt;String&gt; getSubscribedURLs(){
throw new UnsupportedOperationException(&quot;This operation is not supported for consumer.&quot;);
}
// 获取当前ServiceInstance发布的全部URL
default SortedSet&lt;String&gt; getExportedURLs() {
return getExportedURLs(ALL_SERVICE_INTERFACES);
}
// 根据服务接口查找当前ServiceInstance暴露的全部接口
default SortedSet&lt;String&gt; getExportedURLs(String serviceInterface) {
return getExportedURLs(serviceInterface, null);
}
// 根据服务接口和group两个条件查找当前ServiceInstance暴露的全部接口
default SortedSet&lt;String&gt; getExportedURLs(String serviceInterface, String group) {
return getExportedURLs(serviceInterface, group, null);
}
// 根据服务接口、group和version三个条件查找当前ServiceInstance暴露的全部接口
default SortedSet&lt;String&gt; getExportedURLs(String serviceInterface, String group, String version) {
return getExportedURLs(serviceInterface, group, version, null);
}
// 根据服务接口、group、version和protocol四个条件查找当前ServiceInstance暴露的全部接口
SortedSet&lt;String&gt; getExportedURLs(String serviceInterface, String group, String version, String protocol);
// 根据指定条件查询ServiceDefinition
String getServiceDefinition(String interfaceName, String version, String group);
String getServiceDefinition(String serviceKey);
}
</code></pre>
<p><strong>在 MetadataService 接口中定义的都是查询元数据的方法,在其子接口 WritableMetadataService 中添加了一些发布元数据的写方法</strong>,具体定义如下:</p>
<pre><code>@SPI(DEFAULT_METADATA_STORAGE_TYPE)
public interface WritableMetadataService extends MetadataService {
@Override
default String serviceName() {
// ServiceName默认是从ApplicationModel中获取
// ExtensionLoader、DubboBootstrap以及ApplicationModel是单个Dubbo进程范围内的单例对象
// ExtensionLoader用于Dubbo SPI机制加载扩展实现DubboBootstrap用于启动Dubbo进程
// ApplicationModel用于表示一个Dubbo实例其中维护了多个ProviderModel对象表示当前Dubbo实例发布的服务
// 维护了多个ConsumerModel对象表示当前Dubbo实例引用的服务。
return ApplicationModel.getApplication();
}
boolean exportURL(URL url); // 发布该URL所代表的服务
boolean unexportURL(URL url); // 注销该URL所代表的服务
default boolean refreshMetadata(String exportedRevision, String subscribedRevision) {
return true; // 刷新元数据
}
boolean subscribeURL(URL url); // 订阅该URL所代表的服务
boolean unsubscribeURL(URL url); // 取消订阅该URL所代表的服务
// 发布Provider端的ServiceDefinition
void publishServiceDefinition(URL providerUrl);
// 获取WritableMetadataService的默认扩展实现
static WritableMetadataService getDefaultExtension() {
return getExtensionLoader(WritableMetadataService.class).getDefaultExtension();
}
// 获取WritableMetadataService接口指定的扩展实现无指定扩展名称则返回默认扩展实现
static WritableMetadataService getExtension(String name) {
return getExtensionLoader(WritableMetadataService.class).getOrDefaultExtension(name);
}
}
</code></pre>
<p>WritableMetadataService 接口被 @SPI 注解修饰,是一个扩展接口,在前面的继承关系图中也可以看出,<strong>它有两个比较基础的扩展实现,分别是 InMemoryWritableMetadataService默认扩展实现 和 RemoteWritableMetadataServiceDelegate对应扩展名分别是 local 和 remote</strong></p>
<p>下面我们先来看 InMemoryWritableMetadataService 的实现,其中维护了三个核心集合。</p>
<ul>
<li>exportedServiceURLsConcurrentSkipListMap&lt;String, SortedSet<code>&lt;URL&gt;</code>&gt; 类型):用于记录当前 ServiceInstance 发布的 URL 集合,其中 Key 是 ServiceKey即 interface、group 和 version 三部分构成Value 是对应的 URL 集合。</li>
<li>subscribedServiceURLsConcurrentSkipListMap&lt;String, SortedSet<code>&lt;URL&gt;</code>&gt; 类型):用于记录当前 ServiceInstance 引用的 URL 集合,其中 Key 是 ServiceKey即 interface、group 和 version 三部分构成Value 是对应的 URL 集合。</li>
<li>serviceDefinitionsConcurrentSkipListMap&lt;String, String&gt; 类型):用于记录当前 ServiceInstance 发布的 ServiceDefinition 信息,其中 Key 为 Provider URL 的ServiceKeyValue 为对应的 ServiceDefinition 对象序列化之后的 JSON 字符串。</li>
</ul>
<p>InMemoryWritableMetadataService 对 getExportedURLs()、getSubscribedURLs() 以及 getServiceDefinition() 方法的实现,就是<strong>查询</strong>上述三个集合的数据;对 (un)exportURL()、(un)subscribeURL() 和 publishServiceDefinition() 方法的实现,就是<strong>增删</strong>上述三个集合的数据。</p>
<p>(un)exportURL()、(un)subscribeURL() 等方法都是非常简单的集合操作,我们就不再展示,你若感兴趣的话可以参考<a href="https://github.com/xxxlxy2008/dubbo">源码</a>进行学习。 这里我们重点来看一下 publishServiceDefinition() 方法对 ServiceDefinition 的处理:</p>
<pre><code>public void publishServiceDefinition(URL providerUrl) {
// 获取服务接口
String interfaceName = providerUrl.getParameter(INTERFACE_KEY);
if (StringUtils.isNotEmpty(interfaceName)
&amp;&amp; !ProtocolUtils.isGeneric(providerUrl.getParameter(GENERIC_KEY))) {
Class interfaceClass = Class.forName(interfaceName);
// 创建服务接口对应的ServiceDefinition对象
ServiceDefinition serviceDefinition = ServiceDefinitionBuilder.build(interfaceClass);
Gson gson = new Gson();
// 将ServiceDefinition对象序列化为JSON对象
String data = gson.toJson(serviceDefinition);
// 将ServiceDefinition对象序列化之后的JSON字符串记录到serviceDefinitions集合
serviceDefinitions.put(providerUrl.getServiceKey(), data);
return;
}
}
</code></pre>
<p><strong>在 RemoteWritableMetadataService 实现中封装了一个 InMemoryWritableMetadataService 对象,并对 publishServiceDefinition() 方法进行了覆盖</strong>,具体实现如下:</p>
<pre><code>public void publishServiceDefinition(URL url) {
// 获取URL中的side参数值决定调用publishProvider()还是publishConsumer()方法
String side = url.getParameter(SIDE_KEY);
if (PROVIDER_SIDE.equalsIgnoreCase(side)) {
publishProvider(url);
} else {
publishConsumer(url);
}
}
</code></pre>
<p>在 publishProvider() 方法中,首先会根据 Provider URL 创建对应的 FullServiceDefinition 对象,然后通过 MetadataReport 进行上报,具体实现如下:</p>
<pre><code>private void publishProvider(URL providerUrl) throws RpcException {
// 删除pid、timestamp、bind.ip、bind.port等参数
providerUrl = providerUrl.removeParameters(PID_KEY, TIMESTAMP_KEY, Constants.BIND_IP_KEY,
Constants.BIND_PORT_KEY, TIMESTAMP_KEY);
// 获取服务接口名称
String interfaceName = providerUrl.getParameter(INTERFACE_KEY);
if (StringUtils.isNotEmpty(interfaceName)) {
Class interfaceClass = Class.forName(interfaceName); // 反射
// 创建服务接口对应的FullServiceDefinition对象URL中的参数会记录到FullServiceDefinition的params集合中
FullServiceDefinition fullServiceDefinition = ServiceDefinitionBuilder.buildFullDefinition(interfaceClass,
providerUrl.getParameters());
// 获取MetadataReport并上报FullServiceDefinition
getMetadataReport().storeProviderMetadata(new MetadataIdentifier(providerUrl.getServiceInterface(),
providerUrl.getParameter(VERSION_KEY), providerUrl.getParameter(GROUP_KEY),
PROVIDER_SIDE, providerUrl.getParameter(APPLICATION_KEY)), fullServiceDefinition);
return;
}
}
</code></pre>
<p>publishConsumer() 方法则相对比较简单:首先会清理 Consumer URL 中 pid、timestamp 等参数,然后将 Consumer URL 中的参数集合进行上报。</p>
<p>不过,在 RemoteWritableMetadataService 中的 exportURL()、subscribeURL()、getExportedURLs()、getServiceDefinition() 等一系列方法都是空实现,这是为什么呢?其实我们从 RemoteWritableMetadataServiceDelegate 中就可以找到答案,注意,<strong>RemoteWritableMetadataServiceDelegate 才是 MetadataService 接口的 remote 扩展实现</strong></p>
<p><strong>在 RemoteWritableMetadataServiceDelegate 中同时维护了一个 InMemoryWritableMetadataService 对象和 RemoteWritableMetadataService 对象</strong>exportURL()、subscribeURL() 等发布订阅相关的方法会同时委托给这两个 MetadataService 对象getExportedURLs()、getServiceDefinition() 等查询方法则只会调用 InMemoryWritableMetadataService 对象进行查询。这里我们以 exportURL() 方法为例进行说明:</p>
<pre><code>public boolean exportURL(URL url) {
return doFunction(WritableMetadataService::exportURL, url);
}
private boolean doFunction(BiFunction&lt;WritableMetadataService, URL, Boolean&gt; func, URL url) {
// 同时调用InMemoryWritableMetadataService对象和RemoteWritableMetadataService对象的exportURL()方法
return func.apply(defaultWritableMetadataService, url) &amp;&amp; func.apply(remoteWritableMetadataService, url);
}
</code></pre>
<h3>MetadataReport</h3>
<p>元数据中心是 Dubbo 2.7.0 版本之后新增的一项优化,其主要目的是将 URL 中的一部分内容存储到元数据中心,从而减少注册中心的压力。</p>
<p><strong>元数据中心的数据只是给本端自己使用的,改动不需要告知对端</strong>例如Provider 修改了元数据,不需要实时通知 Consumer。这样在注册中心存储的数据量减少的同时还减少了因为配置修改导致的注册中心频繁通知监听者情况的发生很好地减轻了注册中心的压力。</p>
<p><strong>MetadataReport 接口是 Dubbo 节点与元数据中心交互的桥梁</strong>,其继承关系如下图所示:</p>
<p><img src="assets/Cip5yF_lramAYf82AAFkkbA0N2g785.png" alt="png" /></p>
<p>MetadataReport 继承关系图</p>
<p>我们先来看一下 MetadataReport 接口的核心定义:</p>
<pre><code>public interface MetadataReport {
// 存储Provider元数据
void storeProviderMetadata(MetadataIdentifier providerMetadataIdentifier, ServiceDefinition serviceDefinition);
// 存储Consumer元数据
void storeConsumerMetadata(MetadataIdentifier consumerMetadataIdentifier, Map&lt;String, String&gt; serviceParameterMap);
// 存储、删除Service元数据
void saveServiceMetadata(ServiceMetadataIdentifier metadataIdentifier, URL url);
void removeServiceMetadata(ServiceMetadataIdentifier metadataIdentifier);
// 查询暴露的URL
List&lt;String&gt; getExportedURLs(ServiceMetadataIdentifier metadataIdentifier);
// 查询订阅数据
void saveSubscribedData(SubscriberMetadataIdentifier subscriberMetadataIdentifier, Set&lt;String&gt; urls);
List&lt;String&gt; getSubscribedURLs(SubscriberMetadataIdentifier subscriberMetadataIdentifier);
// 查询ServiceDefinition
String getServiceDefinition(MetadataIdentifier metadataIdentifier);
}
</code></pre>
<p>了解了 MetadataReport 接口定义的核心行为之后,接下来我们就按照其实现的顺序来介绍:先来分析 AbstractMetadataReport 抽象类提供的公共实现,然后以 ZookeeperMetadataReport 这个具体实现为例,介绍 MetadataReport 如何与 ZooKeeper 配合实现元数据上报。</p>
<h4>1. AbstractMetadataReport</h4>
<p><strong>AbstractMetadataReport 中提供了所有 MetadataReport 的公共实现</strong>,其核心字段如下:</p>
<pre><code>private URL reportURL; // 元数据中心的URL其中包含元数据中心的地址
// 本地磁盘缓存,用来缓存上报的元数据
File file;
final Properties properties = new Properties();
// 内存缓存
final Map&lt;MetadataIdentifier, Object&gt; allMetadataReports = new ConcurrentHashMap&lt;&gt;(4);
// 该线程池除了用来同步本地内存缓存与文件缓存,还会用来完成异步上报的功能
private final ExecutorService reportCacheExecutor = Executors.newFixedThreadPool(1, new NamedThreadFactory(&quot;DubboSaveMetadataReport&quot;, true));
// 用来暂存上报失败的元数据,后面会有定时任务进行重试
final Map&lt;MetadataIdentifier, Object&gt; failedReports = new ConcurrentHashMap&lt;&gt;(4);
boolean syncReport; // 是否同步上报元数据
// 记录最近一次元数据上报的版本,单调递增
private final AtomicLong lastCacheChanged = new AtomicLong();
// 用于重试的定时任务
public MetadataReportRetry metadataReportRetry;
// 当前MetadataReport实例是否已经初始化
private AtomicBoolean initialized = new AtomicBoolean(false);
</code></pre>
<p>在 AbstractMetadataReport 的构造方法中,首先会初始化本地的文件缓存,然后创建 MetadataReportRetry 重试任务,并启动一个周期性刷新的定时任务,具体实现如下:</p>
<pre><code>public AbstractMetadataReport(URL reportServerURL) {
setUrl(reportServerURL);
// 默认的本地文件缓存
String defaultFilename = System.getProperty(&quot;user.home&quot;) + &quot;/.dubbo/dubbo-metadata-&quot; + reportServerURL.getParameter(APPLICATION_KEY) + &quot;-&quot; + reportServerURL.getAddress().replaceAll(&quot;:&quot;, &quot;-&quot;) + &quot;.cache&quot;;
String filename = reportServerURL.getParameter(FILE_KEY, defaultFilename);
File file = null;
if (ConfigUtils.isNotEmpty(filename)) {
file = new File(filename);
if (!file.exists() &amp;&amp; file.getParentFile() != null &amp;&amp; !file.getParentFile().exists()) {
if (!file.getParentFile().mkdirs()) {
throw new IllegalArgumentException(&quot;...&quot;);
}
}
if (!initialized.getAndSet(true) &amp;&amp; file.exists()) {
file.delete();
}
}
this.file = file;
// 将file文件中的内容加载到properties字段中
loadProperties();
// 是否同步上报元数据
syncReport = reportServerURL.getParameter(SYNC_REPORT_KEY, false);
// 创建重试任务
metadataReportRetry = new MetadataReportRetry(reportServerURL.getParameter(RETRY_TIMES_KEY, DEFAULT_METADATA_REPORT_RETRY_TIMES),
reportServerURL.getParameter(RETRY_PERIOD_KEY, DEFAULT_METADATA_REPORT_RETRY_PERIOD));
// 是否周期性地上报元数据
if (reportServerURL.getParameter(CYCLE_REPORT_KEY, DEFAULT_METADATA_REPORT_CYCLE_REPORT)) {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory(&quot;DubboMetadataReportTimer&quot;, true));
// 默认每隔1天将本地元数据全部刷新到元数据中心
scheduler.scheduleAtFixedRate(this::publishAll, calculateStartTime(), ONE_DAY_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
}
}
</code></pre>
<p>在 AbstractMetadataReport.storeProviderMetadata() 方法中,首先会根据 syncReport 字段值决定是同步上报还是异步上报:如果是同步上报,则在当前线程执行上报操作;如果是异步上报,则在 reportCacheExecutor 线程池中执行上报操作。具体的上报操作是在storeProviderMetadataTask() 方法中完成的:</p>
<pre><code>private void storeProviderMetadataTask(MetadataIdentifier providerMetadataIdentifier, ServiceDefinition serviceDefinition) {
try {
// 将元数据记录到allMetadataReports集合
allMetadataReports.put(providerMetadataIdentifier, serviceDefinition);
// 如果之前上报失败则在failedReports集合中有记录这里上报成功之后会将其删除
failedReports.remove(providerMetadataIdentifier);
// 将元数据序列化成JSON字符串
Gson gson = new Gson();
String data = gson.toJson(serviceDefinition);
// 上报序列化后的元数据
doStoreProviderMetadata(providerMetadataIdentifier, data);
// 将序列化后的元数据保存到本地文件缓存中
saveProperties(providerMetadataIdentifier, data, true, !syncReport);
} catch (Exception e) {
// 如果上报失败则在failedReports集合中进行记录然后由metadataReportRetry任务中进行重试
failedReports.put(providerMetadataIdentifier, serviceDefinition);
metadataReportRetry.startRetryTask();
}
}
</code></pre>
<p>我们可以看到这里调用了 doStoreProviderMetadata() 方法和 saveProperties() 方法。其中, doStoreProviderMetadata() 方法是一个抽象方法对于不同的元数据中心实现有不同的实现这个方法的具体实现在后面会展开分析。saveProperties() 方法中会更新 properties 字段,递增本地缓存文件的版本号,最后(同步/异步)执行 SaveProperties 任务,更新本地缓存文件的内容,具体实现如下:</p>
<pre><code>private void saveProperties(MetadataIdentifier metadataIdentifier, String value, boolean add, boolean sync) {
if (file == null) {
return;
}
if (add) { // 更新properties中的元数据
properties.setProperty(metadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY), value);
} else {
properties.remove(metadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY));
}
// 递增版本
long version = lastCacheChanged.incrementAndGet();
if (sync) { // 同步更新本地缓存文件
new SaveProperties(version).run();
} else { // 异步更新本地缓存文件
reportCacheExecutor.execute(new SaveProperties(version));
}
}
</code></pre>
<p>下面我们再来看 SaveProperties 任务的核心方法—— doSaveProperties() 方法,该方法中<strong>实现了刷新本地缓存文件的全部操作</strong></p>
<pre><code>private void doSaveProperties(long version) {
if (version &lt; lastCacheChanged.get()) { // 对比当前版本号和此次SaveProperties任务的版本号
return;
}
if (file == null) { // 检测本地缓存文件是否存在
return;
}
try {
// 创建lock文件
File lockfile = new File(file.getAbsolutePath() + &quot;.lock&quot;);
if (!lockfile.exists()) {
lockfile.createNewFile();
}
try (RandomAccessFile raf = new RandomAccessFile(lockfile, &quot;rw&quot;);
FileChannel channel = raf.getChannel()) {
FileLock lock = channel.tryLock(); // 对lock文件加锁
if (lock == null) {
throw new IOException(&quot;Can not lock the metadataReport cache file &quot; + file.getAbsolutePath() + &quot;, ignore and retry later, maybe multi java process use the file, please config: dubbo.metadata.file=xxx.properties&quot;);
}
try {
if (!file.exists()) { // 保证本地缓存文件存在
file.createNewFile();
}
// 将properties中的元数据保存到本地缓存文件中
try (FileOutputStream outputFile = new FileOutputStream(file)) {
properties.store(outputFile, &quot;Dubbo metadataReport Cache&quot;);
}
} finally {
lock.release(); // 释放lock文件上的锁
}
}
} catch (Throwable e) {
if (version &lt; lastCacheChanged.get()) { // 比较版本号
return;
} else { // 如果写文件失败则重新提交SaveProperties任务再次尝试
reportCacheExecutor.execute(new SaveProperties(lastCacheChanged.incrementAndGet()));
}
}
}
</code></pre>
<p>了解了刷新本地缓存文件的核心逻辑之后,我们再来看 AbstractMetadataReport 中<strong>失败重试</strong>的逻辑。MetadataReportRetry 中维护了如下核心字段:</p>
<pre><code>// 执行重试任务的线程池
ScheduledExecutorService retryExecutor = Executors.newScheduledThreadPool(0, new NamedThreadFactory(&quot;DubboMetadataReportRetryTimer&quot;, true));
// 重试任务关联的Future对象
volatile ScheduledFuture retryScheduledFuture;
// 记录重试任务的次数
final AtomicInteger retryCounter = new AtomicInteger(0);
// 重试任务的时间间隔
long retryPeriod;
// 无失败上报的元数据之后重试任务会再执行600次才会销毁
int retryTimesIfNonFail = 600;
// 失败重试的次数上限默认为100次即重试失败100次之后会放弃
int retryLimit;
</code></pre>
<p>在 startRetryTask() 方法中MetadataReportRetry 会创建一个重试任务,并提交到 retryExecutor 线程池中等待执行(如果已存在重试任务,则不会创建新任务)。在重试任务中会调用 AbstractMetadataReport.retry() 方法完成重新上报,当然也会判断 retryLimit 等执行条件,具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考<a href="https://github.com/xxxlxy2008/dubbo">源码</a>进行学习。</p>
<p>AbstractMetadataReport.retry() 方法的具体实现如下:</p>
<pre><code>public boolean retry() {
return doHandleMetadataCollection(failedReports);
}
private boolean doHandleMetadataCollection(Map&lt;MetadataIdentifier, Object&gt; metadataMap) {
if (metadataMap.isEmpty()) { // 没有上报失败的元数据
return true;
}
// 遍历failedReports集合中失败上报的元数据逐个调用storeProviderMetadata()方法或storeConsumerMetadata()方法重新上报
Iterator&lt;Map.Entry&lt;MetadataIdentifier, Object&gt;&gt; iterable = metadataMap.entrySet().iterator();
while (iterable.hasNext()) {
Map.Entry&lt;MetadataIdentifier, Object&gt; item = iterable.next();
if (PROVIDER_SIDE.equals(item.getKey().getSide())) {
this.storeProviderMetadata(item.getKey(), (FullServiceDefinition) item.getValue());
} else if (CONSUMER_SIDE.equals(item.getKey().getSide())) {
this.storeConsumerMetadata(item.getKey(), (Map) item.getValue());
}
}
return false;
}
</code></pre>
<p>在 AbstractMetadataReport 的构造方法中,会根据 reportServerURL也就是后面的 metadataReportURL参数启动一个“天”级别的定时任务该定时任务会执行 publishAll() 方法,其中会通过 doHandleMetadataCollection() 方法将 allMetadataReports 集合中的全部元数据重新进行上报。该定时任务默认是在凌晨 02:00~06:00 启动,每天执行一次。</p>
<p>到此为止AbstractMetadataReport 为子类实现的公共能力就介绍完了,其他方法都是委托给了相应的 do*() 方法,这些 do*() 方法都是在 AbstractMetadataReport 子类中实现的。</p>
<p><img src="assets/CgqCHl_hcauAR9AQAAG7kMJSlc8827.png" alt="png" /></p>
<h4>2. BaseMetadataIdentifier</h4>
<p>在 AbstractMetadataReport 上报元数据的时候,元数据对应的 Key 都是BaseMetadataIdentifier 类型的对象,其继承关系如下图所示:</p>
<p><img src="assets/Ciqc1F_lrb-Ad3a5AAGJ2ySdyBE643.png" alt="png" /></p>
<p>BaseMetadataIdentifier 继承关系图</p>
<ul>
<li>MetadataIdentifier 中包含了服务接口、version、group、side 和 application 五个核心字段。</li>
<li>ServiceMetadataIdentifier 中包含了服务接口、version、group、side、revision 和 protocol 六个核心字段。</li>
<li>SubscriberMetadataIdentifier 中包含了服务接口、version、group、side 和 revision 五个核心字段。</li>
</ul>
<h4>3. MetadataReportFactory &amp; MetadataReportInstance</h4>
<p>MetadataReportFactory 是用来创建 MetadataReport 实例的工厂,具体定义如下:</p>
<pre><code>@SPI(&quot;redis&quot;)
public interface MetadataReportFactory {
@Adaptive({&quot;protocol&quot;})
MetadataReport getMetadataReport(URL url);
}
</code></pre>
<p>MetadataReportFactory 是个扩展接口,从 @SPI 注解的默认值可以看出<strong>Dubbo 默认使用 Redis 实现元数据中心</strong>
Dubbo 提供了针对 ZooKeeper、Redis、Consul 等作为元数据中心的 MetadataReportFactory 实现,如下图所示:</p>
<p><img src="assets/CgqCHl_lrcmAEIYBAAFSOVpEU1Y779.png" alt="png" /></p>
<p>MetadataReportFactory 继承关系图</p>
<p>这些 MetadataReportFactory 实现都继承了 AbstractMetadataReportFactory在 AbstractMetadataReportFactory 提供了缓存 MetadataReport 实现的功能,并定义了一个 createMetadataReport() 抽象方法供子类实现。另外AbstractMetadataReportFactory 实现了 MetadataReportFactory 接口的 getMetadataReport() 方法,下面我们就来简单看一下该方法的实现:</p>
<pre><code>public MetadataReport getMetadataReport(URL url) {
// 清理export、refer参数
url = url.setPath(MetadataReport.class.getName())
.removeParameters(EXPORT_KEY, REFER_KEY);
String key = url.toServiceString();
LOCK.lock();
try {
// 从SERVICE_STORE_MAP集合ConcurrentHashMap&lt;String, MetadataReport&gt;类型中查询是否已经缓存有对应的MetadataReport对象
MetadataReport metadataReport = SERVICE_STORE_MAP.get(key);
if (metadataReport != null) { // 直接返回缓存的MetadataReport对象
return metadataReport;
}
// 创建新的MetadataReport对象createMetadataReport()方法由子类具体实现
metadataReport = createMetadataReport(url);
// 将MetadataReport缓存到SERVICE_STORE_MAP集合中
SERVICE_STORE_MAP.put(key, metadataReport);
return metadataReport;
} finally {
LOCK.unlock();
}
}
</code></pre>
<p>MetadataReportInstance 是一个单例对象,其中会获取 MetadataReportFactory 的适配器,并根据 init() 方法传入的 metadataReportURL 选择对应的 MetadataReportFactory 创建 MetadataReport 实例,这也是当前 Dubbo 进程全局唯一的 MetadataReport 实例。</p>
<p>MetadataReportInstance 的具体实现比较简单,这里就不再展示,你若感兴趣的话可以参考<a href="https://github.com/xxxlxy2008/dubbo">源码</a>进行学习。</p>
<h4>4. ZookeeperMetadataReport</h4>
<p>下面我们来看 dubbo-metadata-report-zookeeper 模块是如何接入 ZooKeeper 作为元数据中心的。</p>
<p>我们首先关注 dubbo-metadata-report-zookeeper 模块的 SPI 文件,可以看到 ZookeeperMetadataReportFactory 的扩展名称是 zookeeper</p>
<pre><code>zookeeper=org.apache.dubbo.metadata.store.zookeeper.ZookeeperMetadataReportFactory
</code></pre>
<p>在 ZookeeperMetadataReportFactory 的 createMetadataReport() 方法中会创建 ZookeeperMetadataReport 这个 MetadataReport 实现类的对象。</p>
<p><strong>在 ZookeeperMetadataReport 中维护了一个 ZookeeperClient 实例用来和 ZooKeeper 进行交互</strong>。ZookeeperMetadataReport 读写元数据的根目录是 metadataReportURL 的 group 参数值,默认值为 dubbo。</p>
<p>下面再来看 ZookeeperMetadataReport 对 AbstractMetadataReport 中各个 do*() 方法的实现,这些方法核心都是通过 ZookeeperClient 创建、查询、删除对应的 ZNode 节点,没有什么复杂的逻辑,<strong>关键是明确一下操作的 ZNode 节点的 path 是什么</strong></p>
<p>doStoreProviderMetadata() 方法和 doStoreConsumerMetadata() 方法会调用 storeMetadata() 创建相应的 ZNode 节点:</p>
<pre><code>private void storeMetadata(MetadataIdentifier metadataIdentifier, String v) {
zkClient.create(getNodePath(metadataIdentifier), v, false);
}
String getNodePath(BaseMetadataIdentifier metadataIdentifier) {
return toRootDir() + metadataIdentifier.getUniqueKey(KeyTypeEnum.PATH);
}
</code></pre>
<p>MetadataIdentifier 对象对应 ZNode 节点的 path 默认格式是 :</p>
<pre><code>/dubbo/metadata/服务接口/version/group/side/application
</code></pre>
<p>对应 ZNode 节点的 Value 是 ServiceDefinition 序列化后的 JSON 字符串。</p>
<p>doSaveMetadata()、doRemoveMetadata() 以及 doGetExportedURLs() 方法参数是 ServiceMetadataIdentifier 对象,对应的 ZNode 节点 path 是:</p>
<pre><code>/dubbo/metadata/服务接口/version/group/side/protocol/revision
</code></pre>
<p>doSaveSubscriberData()、doGetSubscribedURLs() 方法的参数是 SubscriberMetadataIdentifier 对象,对应的 ZNode 节点 path 是:</p>
<pre><code>/dubbo/metadata/服务接口/version/group/side/revision
</code></pre>
<h3>MetadataServiceExporter</h3>
<p>了解了 MetadataService 接口的核心功能和底层实现之后,我们接着再来看 MetadataServiceExporter 接口,这个接口<strong>负责将 MetadataService 接口作为一个 Dubbo 服务发布出去</strong></p>
<p>下面来看 MetadataServiceExporter 接口的具体定义:</p>
<pre><code>public interface MetadataServiceExporter {
// 将MetadataService作为一个Dubbo服务发布出去
MetadataServiceExporter export();
// 注销掉MetadataService服务
MetadataServiceExporter unexport();
// MetadataService可能以多种协议发布这里返回发布MetadataService服务的所有URL
List&lt;URL&gt; getExportedURLs();
// 检测MetadataService服务是否已经发布
boolean isExported();
}
</code></pre>
<p><strong>MetadataServiceExporter 只有 ConfigurableMetadataServiceExporter 这一个实现</strong>,如下图所示:</p>
<p><img src="assets/Cip5yF_hcfmAMtHdAABVR_mzQyg047.png" alt="png" /></p>
<p>MetadataServiceExporter 继承关系图</p>
<p>ConfigurableMetadataServiceExporter 的核心实现是 export() 方法,其中会创建一个 ServiceConfig 对象完成 MetadataService 服务的发布:</p>
<pre><code>public ConfigurableMetadataServiceExporter export() {
if (!isExported()) {
// 创建ServiceConfig对象
ServiceConfig&lt;MetadataService&gt; serviceConfig = new ServiceConfig&lt;&gt;();
serviceConfig.setApplication(getApplicationConfig());
serviceConfig.setRegistries(getRegistries());
serviceConfig.setProtocol(generateMetadataProtocol()); // 设置Protocol默认是Dubbo
serviceConfig.setInterface(MetadataService.class); // 设置服务接口
serviceConfig.setRef(metadataService); // 设置MetadataService对象
serviceConfig.setGroup(getApplicationConfig().getName()); // 设置group
serviceConfig.setVersion(metadataService.version()); // 设置version
// 发布MetadataService服务ServiceConfig发布服务的流程在前面已经详细分析过了这里不再展开
serviceConfig.export();
this.serviceConfig = serviceConfig;
} else {
... // 输出日志
}
return this;
}
</code></pre>
<h3>ServiceNameMapping</h3>
<p><strong>ServiceNameMapping 接口的主要功能是实现 Service ID 到 Service Name 之间的转换,底层会依赖配置中心实现数据存储和查询</strong>。ServiceNameMapping 接口的定义如下:</p>
<pre><code>@SPI(&quot;default&quot;)
public interface ServiceNameMapping {
// 服务接口、group、version、protocol四部分构成了Service ID并与当前Service Name之间形成映射记录到配置中心
void map(String serviceInterface, String group, String version, String protocol);
// 根据服务接口、group、version、protocol四部分构成的Service ID查询对应的Service Name
Set&lt;String&gt; get(String serviceInterface, String group, String version, String protocol);
// 获取默认的ServiceNameMapping接口的扩展实现
static ServiceNameMapping getDefaultExtension() {
return getExtensionLoader(ServiceNameMapping.class).getDefaultExtension();
}
}
</code></pre>
<p><strong>DynamicConfigurationServiceNameMapping 是 ServiceNameMapping 的默认实现</strong>,也是唯一实现,其中会依赖 DynamicConfiguration 读写配置中心,完成 Service ID 和 Service Name 的映射。首先来看 DynamicConfigurationServiceNameMapping 的 map() 方法:</p>
<pre><code>public void map(String serviceInterface, String group, String version, String protocol) {
// 跳过MetadataService接口的处理
if (IGNORED_SERVICE_INTERFACES.contains(serviceInterface)) {
return;
}
// 获取DynamicConfiguration对象
DynamicConfiguration dynamicConfiguration = DynamicConfiguration.getDynamicConfiguration();
// 从ApplicationModel中获取Service Name
String key = getName();
String content = valueOf(System.currentTimeMillis());
execute(() -&gt; {
// 在配置中心创建映射关系这里的buildGroup()方法虽然接收四个参数但是只使用了serviceInterface
// 也就是使用创建了服务接口到Service Name的映射
// 可以暂时将配置中心理解为一个KV存储这里的Key是buildGroup()方法返回值+Service Name构成的value是content即时间戳
dynamicConfiguration.publishConfig(key, buildGroup(serviceInterface, group, version, protocol), content);
});
}
</code></pre>
<p>在 DynamicConfigurationServiceNameMapping.get() 方法中会根据传入的服务接口名称、group、version、protocol 组成 Service ID查找对应的 Service Name如下所示</p>
<pre><code>public Set&lt;String&gt; get(String serviceInterface, String group, String version, String protocol) {
// 获取DynamicConfiguration对象
DynamicConfiguration dynamicConfiguration = DynamicConfiguration.getDynamicConfiguration();
// 根据Service ID从配置查找Service Name
Set&lt;String&gt; serviceNames = new LinkedHashSet&lt;&gt;();
execute(() -&gt; {
Set&lt;String&gt; keys = dynamicConfiguration.getConfigKeys(buildGroup(serviceInterface, group, version, protocol));
serviceNames.addAll(keys);
});
// 返回查找到的全部Service Name
return Collections.unmodifiableSet(serviceNames);
}
</code></pre>
<h3>总结</h3>
<p>本课时我们重点介绍了服务自省架构中元数据相关的实现。</p>
<ul>
<li>首先我们介绍了 ServiceInstance 和 ServiceDefinition 是如何抽象一个服务实例以及服务定义的。</li>
<li>紧接着讲解了元数据服务接口的定义,也就是 MetadataService 接口及核心的扩展实现。</li>
<li>接下来详细分析了 MetadataReport 接口的实现,了解了它是如何与元数据中心配合,实现元数据上报功能的。</li>
<li>然后还说明了 MetadataServiceExporter 的实现,了解了发布元数据服务的核心原理。</li>
<li>最后,我们介绍了 ServiceNameMapping 接口以及其默认实现,它实现了 Service ID 与 Service Name 的映射,也是服务自省架构中不可或缺的一部分。</li>
</ul>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Dubbo源码解读与实战-完/43 服务自省设计方案:新版本新方案.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Dubbo源码解读与实战-完/45 加餐:深入服务自省方案中的服务发布订阅(上).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":"70996f7e1d6e3d60","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>