mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 08:46:40 +08:00
539 lines
39 KiB
HTML
539 lines
39 KiB
HTML
<!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>33 路由机制:请求到底怎么走,它说了算(下).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-RPC,Dubbo 跨语言就是如此简单.md.html">29 加餐:HTTP 协议 + JSON-RPC,Dubbo 跨语言就是如此简单</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 class="current-tab" 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 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>33 路由机制:请求到底怎么走,它说了算(下)</h1>
|
||
<p>在上一课时,我们介绍了 Router 接口的基本功能以及 RouterChain 加载多个 Router 的实现,之后介绍了 ConditionRouter 这个类对条件路由规则的处理逻辑以及 ScriptRouter 这个类对脚本路由规则的处理逻辑。本课时我们继续上一课时的内容,介绍剩余的三个 Router 接口实现类。</p>
|
||
<h3>FileRouterFactory</h3>
|
||
<p><strong>FileRouterFactory 是 ScriptRouterFactory 的装饰器</strong>,其扩展名为 file,FileRouterFactory 在 ScriptRouterFactory 基础上<strong>增加了读取文件的能力</strong>。我们可以将 ScriptRouter 使用的路由规则保存到文件中,然后在 URL 中指定文件路径,FileRouterFactory 从中解析到该脚本文件的路径并进行读取,调用 ScriptRouterFactory 去创建相应的 ScriptRouter 对象。</p>
|
||
<p>下面我们来看 FileRouterFactory 对 getRouter() 方法的具体实现,其中完成了 file 协议的 URL 到 script 协议 URL 的转换,如下是一个转换示例,首先会将 file:// 协议转换成 script:// 协议,然后会添加 type 参数和 rule 参数,其中 type 参数值根据文件后缀名确定,该示例为 js,rule 参数值为文件内容。</p>
|
||
<p><img src="assets/Ciqc1F-zkA2AduheAAGQTzCOwl8784.png" alt="png" /></p>
|
||
<p>我们可以再结合接下来这个示例分析 getRouter() 方法的具体实现:</p>
|
||
<pre><code>public Router getRouter(URL url) {
|
||
// 默认使用script协议
|
||
String protocol = url.getParameter(ROUTER_KEY, ScriptRouterFactory.NAME);
|
||
String type = null;
|
||
String path = url.getPath();
|
||
if (path != null) { // 获取脚本文件的语言类型
|
||
int i = path.lastIndexOf('.');
|
||
if (i > 0) {
|
||
type = path.substring(i + 1);
|
||
}
|
||
}
|
||
// 读取脚本文件中的内容
|
||
String rule = IOUtils.read(new FileReader(new File(url.getAbsolutePath())));
|
||
boolean runtime = url.getParameter(RUNTIME_KEY, false);
|
||
// 创建script协议的URL
|
||
URL script = URLBuilder.from(url)
|
||
.setProtocol(protocol)
|
||
.addParameter(TYPE_KEY, type)
|
||
.addParameter(RUNTIME_KEY, runtime)
|
||
.addParameterAndEncoded(RULE_KEY, rule)
|
||
.build();
|
||
// 获取script对应的Router实现
|
||
return routerFactory.getRouter(script);
|
||
}
|
||
</code></pre>
|
||
<h3>TagRouterFactory & TagRouter</h3>
|
||
<p><strong>TagRouterFactory 作为 RouterFactory 接口的扩展实现</strong>,其扩展名为 tag。但是需要注意的是,TagRouterFactory 与上一课时介绍的 ConditionRouterFactory、ScriptRouterFactory 的不同之处在于,它是<strong>通过继承 CacheableRouterFactory 这个抽象类,间接实现了 RouterFactory 接口</strong>。</p>
|
||
<p>CacheableRouterFactory 抽象类中维护了一个 ConcurrentMap 集合(routerMap 字段)用来缓存 Router,其中的 Key 是 ServiceKey。在 CacheableRouterFactory 的 getRouter() 方法中,会优先根据 URL 的 ServiceKey 查询 routerMap 集合,查询失败之后会调用 createRouter() 抽象方法来创建相应的 Router 对象。在 TagRouterFactory.createRouter() 方法中,创建的自然就是 TagRouter 对象了。</p>
|
||
<h4>基于 Tag 的测试环境隔离方案</h4>
|
||
<p><strong>通过 TagRouter,我们可以将某一个或多个 Provider 划分到同一分组,约束流量只在指定分组中流转,这样就可以轻松达到流量隔离的目的,从而支持灰度发布等场景。</strong></p>
|
||
<p>目前,Dubbo 提供了动态和静态两种方式给 Provider 打标签,其中动态方式就是通过服务治理平台动态下发标签,静态方式就是在 XML 等静态配置中打标签。Consumer 端可以在 RpcContext 的 attachment 中添加 request.tag 附加属性,注意<strong>保存在 attachment 中的值将会在一次完整的远程调用中持续传递</strong>,我们只需要在起始调用时进行设置,就可以达到标签的持续传递。</p>
|
||
<p>了解了 Tag 的基本概念和功能之后,我们再简单介绍一个 Tag 的使用示例。</p>
|
||
<p>在实际的开发测试中,一个完整的请求会涉及非常多的 Provider,分属不同团队进行维护,这些团队每天都会处理不同的需求,并在其负责的 Provider 服务中进行修改,如果所有团队都使用一套测试环境,那么测试环境就会变得很不稳定。如下图所示,4 个 Provider 分属不同的团队管理,Provider 2 和 Provider 4 在测试环境测试,部署了有 Bug 的版本,这样就会导致整个测试环境无法正常处理请求,在这样一个不稳定的测试环境中排查 Bug 是非常困难的,因为可能排查到最后,发现是别人的 Bug。</p>
|
||
<p><img src="assets/CgqCHl-zkBuACzVCAABuM5-1_s4317.png" alt="png" /></p>
|
||
<p>不同状态的 Provider 节点</p>
|
||
<p>为了解决上述问题,我们可以针对每个需求分别独立出一套测试环境,但是这个方案会占用大量机器,前期的搭建成本以及后续的维护成本也都非常高。</p>
|
||
<p>下面是一个通过 Tag 方式实现环境隔离的架构图,其中,需求 1 对 Provider 2 的请求会全部落到有需求 1 标签的 Provider 上,其他 Provider 使用稳定测试环境中的 Provider;需求 2 对 Provider 4 的请求会全部落到有需求 2 标签的 Provider 4 上,其他 Provider 使用稳定测试环境中的 Provider。</p>
|
||
<p><img src="assets/CgqCHl-zkCyANtuuAADgH2I1upA475.png" alt="png" /></p>
|
||
<p>依赖 Tag 实现的测试环境隔离方案</p>
|
||
<p>在一些特殊场景中,会有 Tag 降级的场景,比如找不到对应 Tag 的 Provider,会按照一定的规则进行降级。如果在 Provider 集群中不存在与请求 Tag 对应的 Provider 节点,则默认将降级请求 Tag 为空的 Provider;如果希望在找不到匹配 Tag 的 Provider 节点时抛出异常的话,我们需设置 request.tag.force = true。</p>
|
||
<p>如果请求中的 request.tag 未设置,只会匹配 Tag 为空的 Provider,也就是说即使集群中存在可用的服务,若 Tag 不匹配也就无法调用。一句话总结,<strong>携带 Tag 的请求可以降级访问到无 Tag 的 Provider,但不携带 Tag 的请求永远无法访问到带有 Tag 的 Provider</strong>。</p>
|
||
<h4>TagRouter</h4>
|
||
<p>下面我们再来看 TagRouter 的具体实现。在 TagRouter 中持有一个 TagRouterRule 对象的引用,在 TagRouterRule 中维护了一个 Tag 集合,而在每个 Tag 对象中又都维护了一个 Tag 的名称,以及 Tag 绑定的网络地址集合,如下图所示:</p>
|
||
<p><img src="assets/CgqCHl-zkEGALTHPAADFZZM7Y2A139.png" alt="png" /></p>
|
||
<p>TagRouter、TagRouterRule、Tag 与 address 映射关系图</p>
|
||
<p>另外,在 TagRouterRule 中还维护了 addressToTagnames、tagnameToAddresses 两个集合(都是 Map<String, List<code><String></code>> 类型),分别记录了 Tag 名称到各个 address 的映射以及 address 到 Tag 名称的映射。在 TagRouterRule 的 init() 方法中,会根据 tags 集合初始化这两个集合。</p>
|
||
<p>了解了 TagRouterRule 的基本构造之后,我们继续来看 TagRouter 构造 TagRouterRule 的过程。TagRouter 除了实现了 Router 接口之外,还实现了 ConfigurationListener 接口,如下图所示:</p>
|
||
<p><img src="assets/Ciqc1F-zkEyAMNXQAAF_oit25-o273.png" alt="png" /></p>
|
||
<p>TagRouter 继承关系图</p>
|
||
<p><strong>ConfigurationListener 用于监听配置的变化,其中就包括 TagRouterRule 配置的变更</strong>。当我们通过动态更新 TagRouterRule 配置的时候,就会触发 ConfigurationListener 接口的 process() 方法,TagRouter 对 process() 方法的实现如下:</p>
|
||
<pre><code>public synchronized void process(ConfigChangedEvent event) {
|
||
// DELETED事件会直接清空tagRouterRule
|
||
if (event.getChangeType().equals(ConfigChangeType.DELETED)) {
|
||
this.tagRouterRule = null;
|
||
} else { // 其他事件会解析最新的路由规则,并记录到tagRouterRule字段中
|
||
this.tagRouterRule = TagRuleParser.parse(event.getContent());
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>我们可以看到,如果是删除配置的操作,则直接将 tagRouterRule 设置为 null,如果是修改或新增配置,则通过 TagRuleParser 解析传入的配置,得到对应的 TagRouterRule 对象。TagRuleParser 可以解析 yaml 格式的 TagRouterRule 配置,下面是一个配置示例:</p>
|
||
<pre><code>force: false
|
||
runtime: true
|
||
enabled: false
|
||
priority: 1
|
||
key: demo-provider
|
||
tags:
|
||
- name: tag1
|
||
addresses: null
|
||
- name: tag2
|
||
addresses: ["30.5.120.37:20880"]
|
||
- name: tag3
|
||
addresses: []
|
||
</code></pre>
|
||
<p>经过 TagRuleParser 解析得到的 TagRouterRule 结构,如下所示:</p>
|
||
<p><img src="assets/Ciqc1F-zkF6AHgUEAAE3K8dR6QQ826.png" alt="png" /></p>
|
||
<p>TagRouterRule 结构图</p>
|
||
<p>除了上图展示的几个集合字段,TagRouterRule 还从 AbstractRouterRule 抽象类继承了一些控制字段,后面介绍的 ConditionRouterRule 也继承了 AbstractRouterRule。</p>
|
||
<p><img src="assets/CgqCHl-zkGmAYDBMAAFODGWwRfo125.png" alt="png" /></p>
|
||
<p>AbstractRouterRule继承关系图</p>
|
||
<p>AbstractRouterRule 中核心字段的具体含义大致可总结为如下。</p>
|
||
<ul>
|
||
<li>key(string 类型)、scope(string 类型):key 明确规则体作用在哪个服务或应用。scope 为 service 时,key 由 [{group}:]{service}[:{version}] 构成;scope 为 application 时,key 为 application 的名称。</li>
|
||
<li>rawRule(string 类型):记录了路由规则解析前的原始字符串配置。</li>
|
||
<li>runtime(boolean 类型):表示是否在每次调用时执行该路由规则。如果设置为 false,则会在 Provider 列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。</li>
|
||
<li>force(boolean 类型):当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效。该字段默认值为 false。</li>
|
||
<li>valid(boolean 类型):用于标识解析生成当前 RouterRule 对象的配置是否合法。</li>
|
||
<li>enabled(boolean 类型):标识当前路由规则是否生效。</li>
|
||
<li>priority(int 类型):用于表示当前 RouterRule 的优先级。</li>
|
||
<li>dynamic(boolean 类型):表示该路由规则是否为持久数据,当注册方退出时,路由规则是否依然存在。</li>
|
||
</ul>
|
||
<p>我们可以看到,AbstractRouterRule 中的核心字段与前面的示例配置是一一对应的。</p>
|
||
<p>我们知道,Router 最终目的是要过滤符合条件的 Invoker 对象,下面我们一起来看 TagRouter 是如何使用 TagRouterRule 路由逻辑进行 Invoker 过滤的,大致步骤如下。</p>
|
||
<ol>
|
||
<li>如果 invokers 为空,直接返回空集合。</li>
|
||
<li>检查关联的 tagRouterRule 对象是否可用,如果不可用,则会直接调用 filterUsingStaticTag() 方法进行过滤,并返回过滤结果。在 filterUsingStaticTag() 方法中,会比较请求携带的 tag 值与 Provider URL 中的 tag 参数值。</li>
|
||
<li>获取此次调用的 tag 信息,这里会尝试从 Invocation 以及 URL 的参数中获取。</li>
|
||
<li>如果<strong>此次请求指定了 tag 信息</strong>,则首先会获取 tag 关联的 address 集合。
|
||
<ol>
|
||
<li>如果 address 集合不为空,则根据该 address 集合中的地址,匹配出符合条件的 Invoker 集合。如果存在符合条件的 Invoker,则直接将过滤得到的 Invoker 集合返回;如果不存在,就会根据 force 配置决定是否返回空 Invoker 集合。</li>
|
||
<li>如果 address 集合为空,则会将请求携带的 tag 值与 Provider URL 中的 tag 参数值进行比较,匹配出符合条件的 Invoker 集合。如果存在符合条件的 Invoker,则直接将过滤得到的 Invoker 集合返回;如果不存在,就会根据 force 配置决定是否返回空 Invoker 集合。</li>
|
||
<li>如果 force 配置为 false,且符合条件的 Invoker 集合为空,则返回所有不包含任何 tag 的 Provider 列表。</li>
|
||
</ol>
|
||
</li>
|
||
<li>如果<strong>此次请求未携带 tag 信息</strong>,则会先获取 TagRouterRule 规则中全部 tag 关联的 address 集合。如果 address 集合不为空,则过滤出不在 address 集合中的 Invoker 并添加到结果集合中,最后,将 Provider URL 中的 tag 值与 TagRouterRule 中的 tag 名称进行比较,得到最终的 Invoker 集合。</li>
|
||
</ol>
|
||
<p>上述流程的具体实现是在 TagRouter.route() 方法中,如下所示:</p>
|
||
<pre><code>public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
|
||
... // 如果invokers为空,直接返回空集合(略)
|
||
final TagRouterRule tagRouterRuleCopy = tagRouterRule;
|
||
if (tagRouterRuleCopy == null || !tagRouterRuleCopy.isValid() || !tagRouterRuleCopy.isEnabled()) {
|
||
return filterUsingStaticTag(invokers, url, invocation);
|
||
}
|
||
// 检查关联的tagRouterRule对象是否可用,如果不可用,则会直接调用filterUsingStaticTag() 方法进行过滤
|
||
List<Invoker<T>> result = invokers;
|
||
// 获取此次调用的tag信息,尝试从Invocation以及URL中获取
|
||
String tag = StringUtils.isEmpty(invocation.getAttachment(TAG_KEY)) ? url.getParameter(TAG_KEY) :
|
||
invocation.getAttachment(TAG_KEY);
|
||
if (StringUtils.isNotEmpty(tag)) { // 此次请求一个特殊的tag
|
||
// 获取tag关联的address集合
|
||
List<String> addresses = tagRouterRuleCopy.getTagnameToAddresses().get(tag);
|
||
if (CollectionUtils.isNotEmpty(addresses)) {
|
||
// 根据上面的address集合匹配符合条件的Invoker
|
||
result = filterInvoker(invokers, invoker -> addressMatches(invoker.getUrl(), addresses));
|
||
// 如果存在符合条件的Invoker,则直接将过滤得到的Invoker集合返回
|
||
// 如果不存在符合条件的Invoker,根据force配置决定是否返回空Invoker集合
|
||
if (CollectionUtils.isNotEmpty(result) || tagRouterRuleCopy.isForce()) {
|
||
return result;
|
||
}
|
||
} else {
|
||
// 如果 address 集合为空,则会将请求携带的 tag 与 Provider URL 中的 tag 参数值进行比较,匹配出符合条件的 Invoker 集合。
|
||
result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));
|
||
}
|
||
if (CollectionUtils.isNotEmpty(result) || isForceUseTag(invocation)) {
|
||
return result; // 存在符合条件的Invoker或是force配置为true
|
||
}else { // 如果 force 配置为 false,且符合条件的 Invoker 集合为空,则返回所有不包含任何 tag 的 Provider 列表。
|
||
List<Invoker<T>> tmp = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(),
|
||
tagRouterRuleCopy.getAddresses()));
|
||
return filterInvoker(tmp, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
|
||
}
|
||
} else {
|
||
// 如果此次请求未携带 tag 信息,则会先获取 TagRouterRule 规则中全部 tag 关联的 address 集合。
|
||
List<String> addresses = tagRouterRuleCopy.getAddresses();
|
||
if (CollectionUtils.isNotEmpty(addresses)) {
|
||
// 如果 address 集合不为空,则过滤出不在 address 集合中的 Invoker 并添加到结果集合中。
|
||
result = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(), addresses));
|
||
if (CollectionUtils.isEmpty(result)) {
|
||
return result;
|
||
}
|
||
}
|
||
// 如果不存在符合条件的 Invoker 或是 address 集合为空,则会将请求携带的 tag 与 Provider URL 中的 tag 参数值进行比较,得到最终的 Invoker 集合。
|
||
return filterInvoker(result, invoker -> {
|
||
String localTag = invoker.getUrl().getParameter(TAG_KEY);
|
||
return StringUtils.isEmpty(localTag) || !tagRouterRuleCopy.getTagNames().contains(localTag);
|
||
});
|
||
}
|
||
}
|
||
</code></pre>
|
||
<h3>ServiceRouter & AppRouter</h3>
|
||
<p>除了前文介绍的 TagRouterFactory 继承了 CacheableRouterFactory 之外,<strong>ServiceRouterFactory 也继承 CachabelRouterFactory,具有了缓存的能力</strong>,具体继承关系如下图所示:</p>
|
||
<p><img src="assets/Ciqc1F-zkHqAH3diAAGWl6aQJy8860.png" alt="png" /></p>
|
||
<p>CacheableRouterFactory 继承关系图</p>
|
||
<p>ServiceRouterFactory 创建的 Router 实现是 ServiceRouter,与 ServiceRouter 类似的是 AppRouter,<strong>两者都继承了 ListenableRouter 抽象类</strong>(虽然 ListenableRouter 是个抽象类,但是没有抽象方法留给子类实现),继承关系如下图所示:</p>
|
||
<p><img src="assets/Ciqc1F-zkISAPopjAAH9Njd3pOE049.png" alt="png" /></p>
|
||
<p>ListenableRouter 继承关系图</p>
|
||
<p><strong>ListenableRouter 在 ConditionRouter 基础上添加了动态配置的能力</strong>,ListenableRouter 的 process() 方法与 TagRouter 中的 process() 方法类似,对于 ConfigChangedEvent.DELETE 事件,直接清空 ListenableRouter 中维护的 ConditionRouterRule 和 ConditionRouter 集合的引用;对于 ADDED、UPDATED 事件,则通过 ConditionRuleParser 解析事件内容,得到相应的 ConditionRouterRule 对象和 ConditionRouter 集合。这里的 ConditionRuleParser 同样是以 yaml 文件的格式解析 ConditionRouterRule 的相关配置。ConditionRouterRule 中维护了一个 conditions 集合(List<code><String></code> 类型),记录了多个 Condition 路由规则,对应生成多个 ConditionRouter 对象。</p>
|
||
<p>整个解析 ConditionRouterRule 的过程,与前文介绍的解析 TagRouterRule 的流程类似,这里不再赘述。</p>
|
||
<p>在 ListenableRouter 的 route() 方法中,会遍历全部 ConditionRouter 过滤出符合全部路由条件的 Invoker 集合,具体实现如下:</p>
|
||
<pre><code>public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
|
||
if (CollectionUtils.isEmpty(invokers) || conditionRouters.size() == 0) {
|
||
return invokers; // 检查边界条件,直接返回invokers集合
|
||
}
|
||
for (Router router : conditionRouters) { // 路由规则进行过滤
|
||
invokers = router.route(invokers, url, invocation);
|
||
}
|
||
return invokers;
|
||
}
|
||
</code></pre>
|
||
<p>ServiceRouter 和 AppRouter 都是简单地继承了 ListenableRouter 抽象类,且没有覆盖 ListenableRouter 的任何方法,两者只有以下两点区别。</p>
|
||
<ul>
|
||
<li><strong>一个是 priority 字段值不同</strong>。ServiceRouter 为 140,AppRouter 为 150,也就是说 ServiceRouter 要先于 AppRouter 执行。</li>
|
||
<li><strong>另一个是获取 ConditionRouterRule 配置的 Key 不同</strong>。ServiceRouter 使用的 RuleKey 是由 {interface}:[version]:[group] 三部分构成,获取的是一个服务对应的 ConditionRouterRule。AppRouter 使用的 RuleKey 是 URL 中的 application 参数值,获取的是一个服务实例对应的 ConditionRouterRule。</li>
|
||
</ul>
|
||
<h3>总结</h3>
|
||
<p>本课时我们是紧接上一课时的内容,继续介绍了剩余 Router 接口实现的内容。</p>
|
||
<p>我们首先介绍了基于文件的 FileRouter 实现,其底层会依赖上一课时介绍的 ScriptRouter;接下来又讲解了基于 Tag 的测试环境隔离方案,以及如何基于 TagRouter 实现该方案,同时深入分析了 TagRouter 的核心实现;最后我们还介绍了 ListenableRouter 抽象类以及 ServerRouter 和 AppRouter 两个实现,它们是在条件路由的基础上添加了动态变更路由规则的能力,同时区分了服务级别和服务实例级别的配置。</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/Dubbo源码解读与实战-完/32 路由机制:请求到底怎么走,它说了算(上).md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/Dubbo源码解读与实战-完/34 加餐:初探 Dubbo 动态配置的那些事儿.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":"70996f60bead3d60","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>
|