分布式中间件之Dubbo详解

分布式 专栏收录该内容
9 篇文章 0 订阅

文章内容输出来源:拉勾教育Java高薪训练营

心得体会: 在拉勾教育高薪训练营里经过三个月多的学习,很辛苦但也收获很多,主要体现在:1.以前是工作遇到了哪方面的问题才去学习并解决,很难形成知识体系,在拉勾高薪训练营里课程内容很丰富,基础回顾加进阶讲解,完美的打造了个人的知识体系,为成为一名合格的架构师奠定了基础;2.值得一说的是“闯关式”的学习模式,若上一个模块的作业未完成就无法学习下一个模块,但有班主任和导师对个人的学习进行督促,可以让我们学员必须牢靠的掌握技术知识点;3.在知识点技术落地方面,以前只是听说过或停留在概念层面,在拉勾教育的课程中,不同的老师会从不同的角度对知识点进行讲解,有些比较难理解的知识点(例如Dubbo的SPI机制),老师一般会从基础简单的案例入手,让我们轻松理解并掌握,然后在扩展延伸其重点。特别是源码分析方面,彻底发散了个人思维,理解过后可以写出优雅的代码,比如之前是知道有反射这个概念,也知道有责任链这种设计模式,但不会用也不知道在什么场景下用,经过这次对Dubbo源码的分析理解,成功的使用反射和责任链模式解决了工作上一个不好处理的业务逻辑问题。当然在这里还有一群爱学习的小伙伴,可以一起讨论问题,一起分享技术。是的,如果你现在的能力支撑不了你的梦想,那就让我们站在拉勾教育这个巨人的肩膀上去实现自己的梦想吧!

本文概述: 本文将主要讲解分布式系统中Dubbo框架,主要内容包括Dubbo相关概念、架构设计的演变、注册中心Zookeeper、Dubbo的高级特性、Dubbo源码分析、Dubbo中的设计模式和Dubbo项目实战。
Dubbo知识点思维导图:
在这里插入图片描述

一、Dubbo相关概念

1. 分布式

分布式是指把一个系统拆分为多个子系统,每个子系统负责各自的那部分功能,独立部署,各司其职。简单来说,分布式就是多个系统在一起作不同的事。

2. 集群

集群是指多个实例共同工作,最简单或最常见的集群是把一个应用复制多份部署。简单来说,集群就是多个系统在一起作同样的事 。
常见的集群模式有:
主从集群:一主多从,主从各司其职,主节点负责资源分配和任务调度,从节点负责具体的执行;
主备集群:一主一备,主要是为了解决单点故障问题,便于备份恢复;
注意:分布式的核心体现是拆分;集群的核心体现是复制;所以分布式一定是集群,但集群不一定是分布式。

3. RPC

RPC(remote procedure call)即远程过程调用,比如发送了一个查询用户明细的请求get/userInfo?userId=1234,实际上是调用了服务端的一个方法userInfo(Integer userId)。即在本地电脑上可以调用一个远程服务端的方法,所以叫远程过程调用。这里的"远"也不一定是跨越网络的,也可以是同一台主机的两个进程之间的相互交流。RPC并不是一个具体的技术,它指定了一种通信方式,是一种服务间通信的理念。在java中RPC的落地框架比较多,常见的有Hessian、gRPC、Thrift、HSF (High-Speed Service Framework)、Dubbo等。
【Hessian】
Hessian是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。基于HTTP协议,采用二进制编解码。
【gRPC】
gRPC是Google 公布的开源软件,基于最新的HTTP 2.0协议,并支持常见的众多编程语言。
【Thrift】
Thrift是Facebook的开源 RPC 框架,主要是一个跨语言的服务开发框架。它拥有功能强大的代码生成引擎,无缝地支持C++,C#,Java,Python,PHP和Ruby。Thrift允许你定义一个描述文件,描述数据类型和服务接口。依据该文件,编译器方便地生成RPC客户端和服务器通信代码。
Thrift最初由Facebook开发用做系统内部语言之间的RPC通信,2007年由Facebook贡献到apache组织,现在是apache下的opensource之一 。支持多种语言之间的RPC方式的通信,比如PHP语言client可以构造一个对象,调用相应的服务方法来调用Java语言的服务,跨越语言的C/S RPC调用。底层通讯基于SOCKET。
【HSF】
HSF全称为High-Speed Service Framework,是一个分布式的服务框架,HSF从分布式应用层面以及统一的发布/调用方式层面提供服务支持,从而可以很容易的开发分布式的应用以及提供或使用公用功能模块,而不用考虑分布式领域中的各种细节技术,例如远程通讯、性能损耗、调用的透明化、同步/异步调用方式的实现等问题。
【Dubbo】
Dubbo框架是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的RPC实现服务的输出和输入功能,可以和Spring框架无缝集成。

4. Restful API

RESTful是一种架构风格,其最大的几个特点为:资源、统一接口、URI 和无状态。
资源: 所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,就是一个具体的实体。
统一接口: RESTful架构风格规定,数据的元操作,即 CRUD(Create,Read,Update和Delete,即数据的增查改删)操作,分别对应于HTTP方法:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源,这样就统一了数据操作的接口,仅通过 HTTP方法,就可以完成对数据的所有增删查改工作。
URL: 可以用一个URI(统一资源定位符)指向资源,即每个URI都对应一个特定的资源,要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或识别符。
无状态: 所谓无状态的,即所有的资源,都可以通过URI定位,而且这个定位与其他资源无关,也不会因为其他资源的变化而改变。有状态和无状态的区别,举例如下:
比如查询员工的工资,如果查询工资是需要登录系统,进入查询工资的页面,执行相关操作后,获取工资的多少,则这种情况是有状态的。因为查询工资的每一步操作都依赖于前一步操作,只要前置操作不成功,后续操作就无法执行。如果输入一个URI即可得到指定员工的工资,则这种情况是无状态的,因为获取工资不依赖于其他资源或状态。且这种情况下,员工工资是一个资源,由一个URI与之对应,可以通过HTTP中的GET方法得到资源,这是典型的RESTful风格。
注意: RPC侧重于动作,即RPC可以通过动作去操作资源;RESTful侧重于资源,是面向资源的设计架构;在传输效率方面,RPC效率更高,RPC使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率;但RPC实现复杂,流程繁琐;REST调用及测试都很方便。

5. 长连接和短连接

短连接: HTTP1.0中每一个请求和响应都会触发一次TCP连接的建立和断开,此为短连接。
长连接: 在HTTP1.1中增加了keep-alive机制,允许在一个TCP连接上发送多个请求和响应,此为长连接。通俗点说,就是建立连接过后可以持续发送请求,无须再建立连接。HTTP请求头部中的Connection标识是否使用长连接,当为Connection: keep-alive时,表明使用长连接(通常无需设置,默认);当不希望使用长连接,则设为Connection: close。
在这里插入图片描述
Dubbo服务调用连接是长连接,Dubbo服务调用是小数据量的通信,针对每一次RPC通信,都会生成一个唯一的id来标识,这样就能区分出一次RPC请求对应的RPC响应了。
长连接和短连接的性能比较:
如果二者的限制条件一样(比如都是同步方式或IO复用方式、同等数据量通信、连接个数相同)情况下,长连接性能理论上要优于短连接,因为前者省去了三次握手和四次挥手过程。
如果二者限制条件不同,绝大多数场景的限制条件都是不同的,如果通信较频繁,使用长连接性能较好;如果通信次数较少,使用短连接更合适,毕竟不用维护长连接。

6. Dubbo框架

Dubbo官网:http://dubbo.apache.org/
官网上说,Apache Dubbo是一款高性能Java RPC框架。所以这里必须指出,我们经常所说的Dubbo不是指Dubbo协议,而是指RPC框架即远程服务调用的分布式框架,具体的说Dubbo框架是RPC理念的落地产品。而本文着重介绍的是Dubbo框架。
在分布式应用场景有高并发,高可扩展和高性能的要求。还涉及到,序列化/反序列化,网络,多线程以及设计模式的问题,而Dubbo框架对这些进行了封装。
Dubbo底层是使用Netty这样的NIO框架,是基于TCP协议传输的,配合以Hession序列化完成RPC通信。

7. Dubbo协议

Dubbo协议是阿里巴巴自己实现的一种网络应用层协议,传输层还是TCP,Dubbo协议与HTTP协议、FTP协议,SMTP协议等这些应用层协议是并列的概念。

8. Dubbox框架

Dubbox是一个分布式服务框架,其前身是阿里巴巴开源项目Dubbo ,被国内电商及互联网项目中使用,后期阿里巴巴停止了该项目的维护,当当网便在Dubbo基础上进行优化,并继续维护,为了与原有的Dubbo区分,故将其命名为Dubbox。Dubbox弥补了Dubbo中没有REST框架的不足,即Dubbo服务端服务可以通过Dubbox快速包装为REST风格的服务。

9. Zookeeper

ZooKeeper是一个开源的分布式协调服务,它是服务集群的管理者,监视着集群中各个服务节点的状态,并根据节点提交的反馈进行合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
分布式应用程序基于Zookeeper可以实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理(注册中心)、Master选举、分布式锁和分布式队列等功能。
ZooKeeper将作为注册中心与Dubbo结合使用。

10. Dubbo和Zookeeper的关系

Dubbo为服务间提供了调用,主要用于管理分布式系统,从后面的Dubbo架构中我们可以看到Dubbo的工作需要一个非常重要的组件——注册中心(Register),而Zookeeper是分布式协调的治理工具,它能很好的提供服务注册和发现的功能,这样Dubbo和Zookeeper就完美的相遇了,当然Dubbo官网也推荐注册中心使用Zookeeper。简单来说,打个比方:每个dubbo服务就是动物园的动物,zookeeper是动物园。如果游客想看动物的话那么就去动物园看。比如你要看老虎,那么动物园有老虎你才能看到。换句话说我们把很多不同的dubbo服务(动物)放到zookeeper(动物园中)提供给我们游客进行观赏。这个过程中三个关键:场所、提供者、消费者。
再说一个分布式的项目,提供者与消费者被拆分了开来,部署在不同的tomcat中,在消费者中需要调用提供者的接口,但是两个运行在不同tomcat下的服务无法直接互调接口,那么就可以通过zookeeper和dubbo实现。就好比把动物放到动物园,我们要看了直接去动物园就行。而不能直接去动物生活的地方去看,会有性命安全之忧(比如你去看老虎)。
我们通过Dubbo建立提供者服务,并且到Zookeeper上面注册,即在Zookeeper上填写对应的Dubbo服务的IP及端口号。Zokeeper (注册中心)主要功能是服务的注册与发现,Dubbo(远程服务调用的分布式框架)主要实现应用服务与Zokeeper注册中心的链接调用(类似JDBC工具类)。

二、架构设计的演变

1. 单体架构

单体架构所有模块和功能都集中在一个项目中 ,部署时也是将项目所有功能整体部署到服务器中。

优点:
(1)小项目开发快,成本低
(2)架构简单
(3)易于测试
(4)易于部署
缺点:
(1)大项目模块耦合严重,不易开发维护,沟通成本高
(2)新增业务困难
(3)核心业务与边缘业务混合在一块,出现问题互相影响

2. 垂直架构

根据业务把项目垂直切割成多个项目,因此这种架构称之为垂直架构。

优点:
(1)系统拆分实现了流量分担,解决了并发问题
(2)可以针对不同系统进行优化
(3)方便水平扩展,负载均衡,容错率提高
(4)系统间相互独立,互不影响,新的业务迭代时更加高效
缺点:
(1)服务系统之间接口调用硬编码
(2)搭建集群之后,实现负载均衡比较复杂
(3)服务系统接口调用监控不到位,调用方式不统一
(4)数据库资源浪费,充斥慢查询,主从同步延迟大

3. 分布式架构(SOA )

SOA全称为Service Oriented Architecture,即面向服务的架构 。它是在垂直划分的基础上,将每个项目拆分出多个具备松耦合的服务,一个服务通常以独立的形式存在于操作系统进程中。各个服务之间通过网络调用,这使得构建在各种各样的系统中的服务可以以一种统一和通用的方式进行交互。

优点:
(1)服务以接口为粒度,为开发者屏蔽远程调用底层细节,使用Dubbo面向接口远程方法调用屏蔽了底层调用细节
(2)业务分层以后架构更加清晰,并且每个业务模块职责单一,扩展性更强
(3)数据隔离,权限回收,数据访问都通过接口,让系统更加稳定安全
(4)服务应用本身无状态化,这里的无状态化指的是应用本身不做内存级缓存,而是把数据存入数据库
(5)服务责任易确定,每个服务可以确定责任人,这样更容易保证服务质量和稳定
缺点:
(1)粒度控制复杂,如果没有控制好服务的粒度,服务的模块就会越来越多,就会引发超时、分布式事务等问题
(2)服务接口数量不宜控制,容易引发接口爆炸,所以服务接口建议以业务场景进行单位划分,并对相近的业务做抽象,防止接口爆炸
(3)版本升级兼容困难,尽量不要删除方法、字段,枚举类型的新增字段也可能不兼容等
(4)调用链路长,服务质量不可监控,调用链路变长,下游抖动可能会影响到上游业务,最终形成连锁反应,服务质量不稳定,同时链路的变长使得服务质量的监控变得困难

4. 微服务架构

微服务架构是SOA架构的一种扩展,这种架构模式下它拆分粒度更小、服务更独立。把应用拆分成为一个个微小的服务,不同的服务可以使用不同的开发语言和存储,服务之间往往通过Restful等轻量级通信。微服务架构关键在于微小、独立、轻量级通信。
微服务是在SOA上做的升华 , 粒度更加细致,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”。

优点:
(1)微服务很小,便于特定业务功能的聚焦
(2)微服务很小,每个微服务都可以被一个小团队单独实施(开发、测试、部署上线、运维),团队合作一定程度解耦,便于实施敏捷开发
(3)微服务很小,便于重用和模块之间的组装
(4)微服务很独立,不同的微服务可以使用不同的语言开发,松耦合
(5)微服务架构下,我们更容易引入新技术
(6)微服务架构下,我们可以更好的实现DevOps开发运维一体化

缺点:
(1)微服务架构下,分布式复杂难以管理,当服务数量增加,管理将越加复杂
(2)微服务架构下,分布式链路追踪难等

三、Dubbo架构概述

1. Dubbo基本架构

在这里插入图片描述

节点角色名称
Provider暴露服务的服务提供方
Consumer调用远程服务的服务消费方
Registry服务注册与发现的注册中心
Monitor统计服务的调用次数和调用时间的监控中心
Container服务运行容器,负责启动、加载、运行服务提供者

调用关系说明:
虚线:代表异步调用
实线:代表同步访问
蓝色虚线:代表启动时完成的功能
蓝绿色虚线:代表程序运行中执行的功能
调用流程:
0:服务运行容器运行容器初始化,启动、加载、运行服务提供者
1:服务提供者在服务容器启动时,向注册中心注册自己提供的服务
2:服务消费者在启动时,向注册中心订阅自己所需的服务
3:注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心会基于长连接推送变更数据给消费者
4:服务消费者从提供者地址列表中基于软负载均衡算法选一台提供者进行调用,如果调用失败,则重新选择一台
5:服务提供者和消费者在内存中的调用次数和调用时间会定时每分钟发送给监控中心

2. Dubbo的服务治理

在这里插入图片描述
在大规模服务化之前,应用可能只是通过RMI或Hessian等工具,简单的暴露和引用远程服务,通过配置服务的URL地址进行调用,通过F5等硬件进行负载均衡。

2.1 服务注册与发现

问题: 当服务越来越多时,服务URL配置管理变得非常困难,F5硬件负载均衡器的单点压力也越来越大。
解决: 此时需要一个服务注册中心,动态的注册和发现服务,使服务的位置透明。并通过在消费方获取服务提供方地址列表,实现软负载均衡和Failover,降低对F5硬件负载均衡器的依赖,也能减少部分成本。

2.2 服务依赖关系

问题: 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。
解决: 这时,需要自动画出应用间的依赖关系图,以帮助架构师理清理关系。

2.3 服务监控与统计

问题: 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器?
解决: 为了解决这些问题,首先,要将服务现在每天的调用量,响应时间,都统计出来,作为容量规划的参考指标。其次,要可以动态调整权重,在线上,将某台机器的权重一直加大,并在加大的过程中记录响应时间的变化,直到响应时间到达阀值,记录此时的访问量,再以此访问量乘以机器数反推总容量。

2.4 服务分层架构

问题: 规模继续扩大,应用之间不再是扁平的对应关系,开始分层,比如核心数据层,业务集成层等,就算没有出现循环依赖,也不允许从低层向高层依赖,以免后续被逼循环依赖。
解决: 这时,需要在注册中心定义架构体系,列明有哪些层的定义,每个服务暴露或引用时,都必须声明自己应用属于哪一层,这样注册中心能更快的发现架构的腐化现象。

2.5 服务负责人和服务文档

问题: 服务多了,沟通成本也开始上升,调某个服务失败该找谁?服务的参数都有什么约定?
解决: 这时就需要登记每个服务都是谁负责的,并建立一个服务的文档库,方便检索。

2.6 服务权限控制

问题: 慢慢一些敏感数据也都服务化了,安全问题开始变得重要,谁能调该服务?如何授权?
解决: 这样的服务可能需要一个密码,访问时需带着此密码,但如果用密码,要改密码时,就会很不方便,所有的消费方都要改,所以动态生成令牌(Token)可能会更好,提供方将令牌告之注册中心,由注册中心决定是否告之消费方,这样就能在注册中心页面上做复杂的授权模型。

2.7 服务等级协定

问题: 就算是不敏感的服务,也不是能任意调用,比如某服务突然多了一个消费者,这个消费者的请求量直接把服务给拖跨了,其它消费者跟着一起故障。
解决: 首先服务提供方需要流控,当流程超标时,能拒绝部分请求,进行自我保护。其次,消费者上线前和提供者约定《服务质量等级协定(SLA)》,SLA包括消费者承诺每天调用量,请求数据量,提供方承诺响应时间,出错率等,将SLA记录在监控中心,定时与监控数据对比,超标则报警。

2.8 服务路由和服务负载均衡

问题: 虽然有SLA约定,如果不能控制,就只是君子协定,如何确保服务质量?
解决: 比如:一个应用很重要,一个不那么重要,它们调用同一个服务,这个服务就应该向重要应用倾斜,而不是一视同仁,当支撑不住时,应限制不重要应用的访问,保障重要应用的可用,如何做到这一点呢。这时,就需要服务路由,控制不同应用访问不同机器,比如:
应用分离:
consumer.application = foo ==> provider.host = 1,2,3
consumer.application != foo ==> provider.host = 5,6
读写分离:
method.name = find*,get* ==> provider.host = 1,2,3
method.name != find*,get* ==> provider.host = 5,6

2.9 服务自动测试

问题: 服务上线后,需要验证服务是否可用,但因防火墙的限制,线下是不能访问线上服务的,不得不先写好一个测试Main,然后放到线上去执行,非常麻烦,并且容易忘记验证。
解决: 所以线上需要有一个自动运行的验证程序,用户只需在界面上填上要验证的服务方法,以及参数值和期望的返回值,当有一个服务提供者上线时,将自动运行该用例,并将运行结果发邮件通知负责人。

2.10 服务模板工程

问题: 服务应用和Web应用是有区别的,它是一个后台Daemon程序,不需要Tomcat之类的Web容器。但因公司之前以Web应用为主,规范都是按Web应用的,所以不得不把服务跑在一个根本用不上的Web容器里,而搭一个这样的Web工程也非常费事。
解决: 所以需要实现一个非Web的容器,只需简单的Main加载spring配置即可,并提供Maven模板工程,只需mvn dubbo:generate即可创建一个五脏俱全的服务应用。

2.11 服务开发IDE

问题: 开发服务的人越来越多,更注重开发效率,IDE的集成支持必不可少。
解决: 通过插件,可以在IDEA中直接运行服务,提供方可以直接填入测试数据测试服务,消费方可以直接Mock服务不依赖提供方开发。

2.12 服务上线审批和下线通知

问题: 因为暴露服务很简单,服务的上线越来越随意,有时候负责服务化的架构师都不知道有人上线了某个服务,使得线上服务鱼龙混杂,甚至出现重复的服务,而服务下线比上线还困难。
解决: 需要一个新服务上线审批流程,必须经过服务化的架构师审批过了,才可以上线。而服务下线时,应先标识为过时,然后通知调用方尽快修改调用,直到没有人调此服务,才能下线。

2.13 服务兼容性检测

问题: 因服务接口设计的经验一直在慢慢的积累过程中,很多接口并不能一促而蹴,在修改的过程中,如何保证兼容性,怎么判断是否兼容?另外,更深层次的,业务行为兼容吗?
解决: 可以根据使用的协议类型,分析接口及领域模型的变更是否兼容,比如:对比加减字段,方法签名等。而业务上,可能需要基于自动回归测试用例,形成Technology Compatibility Kit (TCK),确保兼容升级。

2.14 服务容错和降级

问题: 随着服务的不停升级,总有些意想不到的事发生,比如cache写错了导致内存溢出,故障不可避免,每次核心服务一挂,影响一大片,人心慌慌,如何控制故障的影响面?服务是否可以功能降级?或者资源劣化?
解决: 应用间声明依赖强度,哪些功能强依赖,哪些弱依赖,然后基于依赖强度,计算出影响面,并定期测试复查,加强关键路径上的服务的优化和容错,清理不该在关键路径上的服务。提供容错Mock数据,Mock数据也应可以在注册中心在运行时动态下发,当某服务不可用时,用Mock数据代替,可以减少故障的发生,比如某验权服务,当验权服务全部挂掉后,直接返回false表示没有权限,并打印Error日志报警。另外,前端的页面也应采用Portal进行降级,当该Portal获取不到数据时,直接隐藏,或替换为其它模块展示,并提供功能开关,可人工干预是否展示,或限制多少流量可以展示。

2.15 服务编排

问题: 当已有很多小服务,可能就需要组合多个小服务的大服务,为此,不得不增加一个中间层,暴露一个新服务,里面分别调其它小服务,这样的新服务业务逻辑少,却带来很多开发工作量。
解决: 此时,需要一个服务编排引擎,内置简单的流程引擎,只需用XML或DSL声明如何聚合服务,注册中心可以直接下发给消费者执行聚合逻辑,或者部署通用的编排服务器,所有请求有编排服务器转发。

2.16 服务使用情况报告

问题: 并不是所有服务的访问量都大,很多的服务都只有一丁点访问量,却需要部署两台提供服务的机器,进行HA互备,如何减少浪费的机器。
解决: 此时可能需要让服务容器支持在一台机器上部署多个应用,可以用多JVM隔离,也可以用ClassLoader隔离。

2.17 服务自动部署

问题: 多个应用如果不是一个团队开发的,部署在一台机器上,很有可以误操作,停掉了别人的服务。
解决: 需要实现自动部署,所有的部署都无需人工干扰,最好是一键式部署。

2.18 服务权重动态调整

问题: 机器总是的闲时和忙时,或者冗余机器防灾,如何提高机器的利用率?
解决: 即然已经可以自动部署了,那根据监控数据,就可以实现资源调度,根据应用的压力情况,自动添加机器并部署。如果你的应用是国际化的,有中文站,美国站之类,因为时差,美国站的机器晚上闲的时候,可能正是中文站的白天忙时,可以通过资源调度,分时段自动调配和部署双方应用。

四、Dubbo的注册中心Zookeeper

官方推荐Dubbo的注册中心使用Zookeeper,本部分将对Zookeeper的相关知识进行讲解,包括Zookeeper基本概念、基本使用、工作原理、集群相关和主要应用场景。

1. Zookeeper基本概念

1.1 会话 Session

Session指客户端会话,一个客户端连接是指客户端和服务端之间的一个TCP长连接,Zookeeper对外的服务端口默认为2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来自服务器的Watch事件通知。

1.2 数据节点 Znode

Zookeeper提供一个多层级的节点命名空间即节点称为Znode,在Zookeeper中,节点可以分为两类,第一类指构成集群的机器,称为机器节点;第二类指数据模型中的数据单元即数据节点。Zookeeper中数据节点分为以下四种:
(1)PERSISTENT-持久节点
除非手动删除,否则节点一直存在于 Zookeeper 上。
(2)EPHEMERAL-临时节点
临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与Zookeeper连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除。
(3)PERSISTENT_SEQUENTIAL-持久顺序节点
基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
(4)EPHEMERAL_SEQUENTIAL-临时顺序节点
基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

1.3 版本 Version

由上可知,Zookeeper的每个Znode上都会存储数据,对于每个Znode,Zookeeper都会为其维护一个叫作Stat的数据结构,Stat记录了这个Znode的三个数据版本,分别是version(当前Znode的版本)、cversion(当前Znode子节点的版本)、aversion(当前Znode的ACL版本)。

1.4 事件监听器 Watcher

事件监听器(Watcher)是Zookeeper中一个很重要的特性,Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端,该机制是Zookeeper实现分布式协调服务的重要特性。

1.5 权限控制策略 ACL

Zookeeper采用ACL(Access Control Lists)策略来进行权限控制,其定义了如下五种权限:
(1)CREATE:创建子节点的权限;
(2)READ:获取节点数据和子节点列表的权限;
(3)WRITE:更新节点数据的权限;
(4)DELETE:删除子节点的权限;
(5)ADMIN:设置节点ACL的权限。
其中需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。

2. Zookeeper基本使用

2.1 Zookeeper的部署模式

部署模式:单机模式、伪集群模式、集群模式。

2.2 Zookeeper基本使用方式

在客户端如何对Zookeeper的节点数据进行操作,Zookeeper为我们提供了以下四种方式:
(1)命令行操作
(2)API操作
(3)开源客户端ZkClient操作
(4)开源客户端Curator操作

3. Zookeeper工作原理

3.1 Zookeeper Watcher 机制(数据变更通知)

Zookeeper允许客户端向服务端的某个Znode注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher监听,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据Watcher通知状态和事件类型做出业务上的改变。
Watcher特性:
(1)一次性
无论是服务端还是客户端,一旦一个Watcher被 触 发 ,Zookeeper都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
(2)客户端串行执行
客户端Watcher回调的过程是一个串行同步的过程。
(3)轻量
Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容,客户端向服务端注册Watcher的时候,并不会把客户端真实的Watcher对象实体传递到服务端,仅仅是在客户端请求中使用Boolean类型属性进行了标记。
(4)Watcher event异步发送Watcher的通知事件从server发送到client是异步的,这就存在一个问题,不同的客户端和服务器之间通过socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于Zookeeper本身提供了ordering guarantee,即客户端监听事件后,才会感知它所监视的Znode发生了变化。所以我们使用Zookeeper不能期望能够监控到节点每次的变化。Zookeeper只能保证最终的一致性,而无法保证强一致性。
(5)注册Watcher getData、exists、getChildren
(6)触发Watcher create、delete、setData
(7)当一个客户端连接到一个新的服务器上时,Watcher将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到Watcher的。而当client重新连接时,如果需要的话,所有先前注册过的Watcher,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,Watcher可能会丢失:对于一个未创建的Znode的exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个Watcher事件可能会被丢失。

工作机制:
(1)客户端注册Watcher
(2)服务端处理Watcher
(3)客户端回调Watcher
客户端注册Watcher实现说明:
(1)调用getData()/getChildren()/exist()三个API,传入Watcher对象
(2)标记请求request,封装Watcher到WatchRegistration
(3)封装成Packet对象,服务端发送request
(4)收到服务端响应后,将Watcher注册到ZKWatcherManager中进行管理
(5)请求返回,完成注册。
服务端处理Watcher实现:
(1)服务端接收Watcher并存储
接收到客户端请求,处理请求判断是否需要注册Watcher,需要的话将数据节点的节点路径和ServerCnxn(ServerCnxn 代表一个客户端和服务端的连接,实现了Watcher的process接口,此时可以看成一个Watcher对象)存储在WatcherManager的WatchTable和watch2Paths 中去。
(2)Watcher触发
以服务端接收到setData() 事务请求触发NodeDataChanged事件为例:
先封装WatchedEvent,将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个WatchedEvent对象;后查询Watcher,即从WatchTable中根据节点路径查找 Watcher,若没找到说明没有客户端在该数据节点上注册过Watcher,若找到则提取并从WatchTable 和Watch2Paths中删除对应Watcher(从这里可以看出Watcher在服务端是一次性的,触发一次就失效了);最后调用process方法来触发Watcher,即通过ServerCnxn对应的TCP连接发送Watcher事件通知。
客户端回调 Watcher:
客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调 Watcher。客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效了。

3.2 ZAB协议(保证主从节点的状态同步)

ZAB协议是为分布式协调服务(保证各个服务之间的数据状态同步)Zookeeper专门设计的一种支持崩溃恢复的原子广播协议。
ZAB 协议包括两种基本的模式:崩溃恢复和消息广播。
当整个Zookeeper集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与Leader服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的Leader 服务器,然后集群中Follower服务器开始与新的Leader服务器进行数据同步,当集群中超过半数机器与该Leader服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
恢复模式:
当服务启动或者在领导者崩溃后,Zookeeper集群服务就进入了恢复模式,当领导者被选举出来,且大多数服务Server完成了和Leader的状态同步以后,恢复模式就结束了。状态同步保证了Leader和 Server具有相同的系统数据状态。
广播模式:
一旦Leader已经和多数的Follower进行了状态同步后,它就可以开始广播消息了,即进入广播模式。这时候当一个Server加入到ZooKeeper集群服务中,它会在恢复模式下启动,发现Leader,并和 Leader进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper服务一直维持在Broadcast状态,直到Leader崩溃了或者Leader失去了大部分的Followers支持。

4. Zookeeper集群相关

4.1 Zookeeper集群规则

如果保证Zookeeper集群可用,必须满足过半原则;
集群规则为 2N+1 台,N>0,即Zookeeper集群数量为奇数,最少为3台。
官方推荐集群数必须为奇数的原因:
如果Zookeeper集群总数为5的话,能够保证Zookeeper可用性,最多只能宕机几台?
因为可用必须满足5-2>5/2,所以最多只能宕机2台;
如果Zookeeper集群总数为6的话,能够保证Zookeeper可用性,最多只能宕机几台?
因为可用必须满足6-2>6/2,所以最多只能宕机2台;
也就是说,如果有2个Zookeeper节点,那么只要有1个Zookeeper节点死了,那么Zookeeper服务就不能用了,因为1没有过半,所以2个Zookeeper的死亡容忍度为0;同理,要是有3个Zookeeper,一个死了,还剩下2个正常的,过半了,所以3个Zookeeper的容忍度为1;同理也可以多列举几个:2->0; 3->1; 4->1; 5->2; 6->2 就会发现一个规律,2n和2n-1的容忍度是一样的,都是n-1,所以为了更加高效,不必增加那一个不必要的Zookeeper节点,所以从资源节省的角度来考虑,Zookeeper集群的节点最好要部署成奇数个。

4.2 集群角色

通常在分布式系统中,构成集群的每一台机器(服务)都有自己的角色,Zookeeper中服务角色分为Leader、Follower、Observer,主要功能如下:
Leader:
(1)事务请求的唯一调度和处理者,保证集群事务处理的顺序性
(2)集群内部各服务的调度者
Follower:
(1)处理客户端的非事务请求,转发事务请求给Leader服务器
(2)参与事务请求Proposal的投票
(3)参与Leader选举投票
Observer:
(1)3.0 版本以后引入的一个服务器角色,在不影响集群事务处理能力的基础上提升集群的非事务处理能力
(2)处理客户端的非事务请求,转发事务请求给Leader服务器
(3)不参与任何形式的投票

4.3 集群服务的工作状态

集群中服务器具有四种状态,分别是 LOOKING、FOLLOWING、LEADING、OBSERVING。
(1)LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态。
(2)FOLLOWING:跟随者状态。表明当前服务器角色是Follower。
(3)LEADING:领导者状态。表明当前服务器角色是Leader。
(4)OBSERVING:观察者状态。表明当前服务器角色是Observer。

4.4 Zookeeper集群的脑裂问题

(1)什么是脑裂
脑裂(split-brain)就是“大脑分裂”,也就是本来一个“大脑”被拆分了两个或多个“大脑”,对于人来说,如果一个人有多个大脑,并且相互独立的话,那么会导致人体“手舞足蹈”,“不听使唤”。
在"双机热备"高可用(HA)系统中,当联系两个节点的"心跳线"断开时(即两个节点断开联系时),本来为一个整体、动作协调的HA系统,就分裂成为两个独立的节点(即两个独立的个体)。由于相互失去了联系,都以为是对方出了故障,两个节点上的HA软件像"裂脑人"一样,“本能"地争抢"共享资源”、争起"应用服务"。
对于Zookeeper集群来说,在Zookeeper集群中有Leader节点,Zookeeper集群的脑裂就是存在多个Leader节点。
(2)Zookeeper集群的脑裂问题
对于一个集群,想要提高这个集群的可用性,通常会采用多机房部署,比如现在有一个由6台zkServer所组成的一个集群,部署在了两个机房,每个机房有3台zkServer。正常情况下,此集群只会有一个Leader,那么如果机房之间的网络断了之后,两个机房内的zkServer还是可以相互通信的,如果不考虑过半机制,那么就会出现每个机房内部都将选出一个Leader。这就相当于原本一个集群,被分成了两个集群,出现了两个“大脑”,这就是脑裂。对于这种情况,原本应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络突然联通了,那么此时就会出现问题了,两个集群刚刚都对外提供服务了,数据该怎么合并,数据冲突怎么解决等等问题。
(3)Zookeeper集群脑裂问题的解决
Zookeeper集群的脑裂问题在Zookeeper中已经通过过半选举机制解决了。所谓过半选举机制,即在领导者选举的过程中,如果某台zkServer获得了超过半数的选票,则此zkServer就可以成为Leader了。
接下来,先看下过半选举机制的源码:

public class QuorumMaj implements QuorumVerifier {
    private static final Logger LOG = LoggerFactory.getLogger(QuorumMaj.class);
    
    int half;
    
    // n表示集群中zkServer的个数(准确的说是参与者的个数,参与者不包括观察者节点)
    public QuorumMaj(int n){
        this.half = n/2;
    }

    // 验证是否符合过半机制
    public boolean containsQuorum(Set<Long> set){
        // half是在构造方法里赋值的
        // set.size()表示某台zkServer获得的票数
        return (set.size() > half);
    }
    
}

可以看出代码中过半选举出Leader的条件是,获得投票数必须大于服务个数的一半,注意不是大于等于!
解决过程分析:
情况1:如果现在集群中有5台zkServer,机房1有3台,机房2有2台,如下:
在这里插入图片描述
那么half=5/2=2,此时过半机制的条件是set.size() > 2,也就是说,领导者选举的过程中至少要有三台zkServer投了同一个zkServer,才会符合过半机制,才能选出来一个Leader。此时机房间的网络断开了,对于机房1来说是没有影响的,Leader依然还是Leader,对于机房2来说是选不出来Leader的,此时整个集群中只有一个Leader。

情况2:如果现在集群中有6台zkServer,机房1有3台,机房2有3台,如下:
在这里插入图片描述
当机房间的网络断掉之后,机房1内的三台服务器会进行领导者选举,但是此时过半机制的条件是set.size() > 3,也就是说至少要4台zkServer才能选出来一个Leader,所以对于机房1来说它不能选出一个Leader,同样机房2也不能选出一个Leader,这种情况下整个集群当机房间的网络断掉后,整个集群将没有Leader。
而如果过半机制的条件是set.size() >= 3,那么机房1和机房2都会选出一个Leader,这样就出现了脑裂。所以我们就知道了,为什么过半机制中是大于,而不是大于等于。就是为了防止脑裂。

综上所述,可以得出结论,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要没只有1个Leader,这样就避免了脑裂问题。

5. Zookeeper主要应用场景

5.1 数据发布/订阅(配置中心)

原理解说:
数据发布/订阅,即配置中心管理,就是发布者发布数据供订阅者进行数据订阅,实现动态获取配置数据。常见的设计模式有Push模式和Pull模式。
Zookeeper作为配置中心适合以下数据(配置信息)特性:
(1)数据量通常比较小
(2)数据内容在运行时会发生动态更新
(3)集群中各机器共享,配置一致
比如机器列表信息、运行时开关配置、数据库配置信息等
案例:基于Zookeeper实现动态获取数据源配置信息,即切换数据源时,不需要重启应用服务即可获得最新的数据源信息。
实现图解:
在这里插入图片描述
代码地址:https://gitee.com/tudedong/zookeeper/tree/master/zk-config-management

5.2 命名服务

命名服务简单理解为提供名字的服务,Zookeeper提供的命名服务功能能够帮助应用系统通过一个资源引用的方式来实现对资源的定位和使用。
Zookeeper的命名服务,主要有两个应用方向:
(1)提供类似JNDI的功能,即利用Zookeeper中的树形分层结构,可以把系统中的各种服务的名称,地址以及目录信息存放在Zookeeper中,需要的时候去Zookeeper中去读取;
(2)生成分布式全局唯一ID,即利用Zookeeper中的顺序节点的特性,来生成分布式全局唯一ID,我们知道,在数据库中插入数据时,通常是要有一个ID号,在单机环境下,可以利用数据库的主键自动生成ID号,但在分布式环境下就无法使用了,可以使用UUID,但是UUID有一个缺点,就是没有什么规律很难理解。使用Zookeeper的命名服务可以生成有顺序的序列号,支持分布式的编号。
案例:基于Zookeeper实现分布式的ID生成器
代码实现图解:
在这里插入图片描述
代码地址:https://gitee.com/tudedong/zookeeper/tree/master/zk-name-service

5.3 分布式锁

在分布式环境中,经常需要对某些操作进行加锁,这时就需要分布式锁来完成。
实现分布式锁的方案:
(1)基于数据库实现分布式锁
(2)基于Redis实现分布式锁
(3)基于Zookeeper实现分布式锁
(4)基于Redission框架实现分布式锁
基于Zookeeper实现分布式锁实现思路:
这里是基于模板模式来实现分布式锁,主要步骤如下:
(1)定义模板,即锁的实现骨架,包括:定义锁、等待锁、释放锁;
(2)实现模块,即Zookeeper定义锁实现模板,包括创建临时节点来定义锁,通过事件监听来等待锁,关闭客户端连接(Session会话)来释放锁;
(3)测试:通过多个线程来创建订单序号模拟在分布式环境下独占锁使用情况。

代码地址:https://gitee.com/tudedong/zookeeper/tree/master/zk-lock

5.4 集群管理(注册中心)

Zookeeper在集群管理方面作为注册中心的应用非常广泛,即Zookeeper利用其数据节点模型和Watcher机制提供了服务的注册和发现功能,这里主要通过Zookeeper自身实现服务的注册为例来熟悉Zookeeper作为注册中心的工作逻辑。特别Zookeeper作为Dubbo框架的注册中心将在后面学习到。(具体可以阅读下面的Dubbo中的注册中心)
基于Zookeeper实现简单的服务注册中心思路:
主要步骤如下:
(1)启动服务端时,将服务端注册到Zookeeper中,以节点形式进行数据存储,节点为/zookeeper/server;
(2)客户端启动时,从Zookeeper中获取节点信息,并进行连接;
(3)下线一个服务端,利用Zookeeper的监听机制,客户端与它断开连接;
(4)某服务端上线后,利用Zookeeper的监听机制,客户端感知与它重新建立连接;
(5)测试
先分别启动两个服务端,然后启动客户端,查看连接情况;再停掉一个服务端,查看连接情况;最后再启动停掉的服务端,查看连接情况;

代码地址:https://gitee.com/tudedong/zk-register.git

五、Dubbo的高级特性

这部分根据官网的介绍,知道Dubbo提供的三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。除此之外,还有高度可扩展能力(Dubbo的SPI机制),运行期流量调度(路由规则),Dubbo服务的监控和管理(Dubbo管理控制台 dubbo-admin)等,这些实现基于它的一些高级特性。

1. Dubbo的SPI机制

1.1 SPI介绍

SPI(Service Provider Interface),即服务提供者接口的意思。简单说SPI就是一种扩展机制,我们在相应配置文件中定义好某个接口的实现类,然后再根据这个接口去这个配置文件中加载这个实现类并实例化。即用户调用接口时按照SPI的机制可以调用到想要的实现类。
常见的SPI机制有:
(1)Java的SPI机制:例如JDBC驱动加载案例,即利用Java的SPI机制,可以根据不同的数据库厂商来引入不同的JDBC驱动包;
(2)SpringBoot的SPI机制:例如自动装载配置类案例,我们可以在spring.factories中加上我们自定义的自动配置类,事件监听器或初始化器等;
(3)Dubbo的SPI机制:Dubbo基本上自身的每个功能点都提供了扩展点,从官网上可以看到,比如提供了集群扩展,路由扩展和负载均衡扩展等差不多接近30个扩展点。如果Dubbo的某个内置实现不符合我们的需求,那么我们只要利用其SPI机制将我们的实现类替换掉Dubbo的实现类即可,即通过实现同一接口的前提下,可以进行定制自己的实现类。

1.2 Dubbo中SPI机制的使用

下面使用三个项目来演示Dubbo中SPI机制的使用,一个主项目main,一个服务接口项目api,一个
服务实现项目impl。
api项目创建:
(1)导入Dubbo依赖

<dependency>
	<groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</artifactId>
    <version>2.7.5</version>
</dependency>

(2)创建接口
在接口上使用@SPI,也可以使用@SPI(“xxx”),添加默认的实现

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;

@SPI("human")
public interface HelloService {
    String  sayHello();
    @Adaptive
    String  sayHello(URL  url);
}

impl项目创建:
(1)导入api项目的依赖

<dependency>
	<groupId>com.lagou</groupId>
	<artifactId>dubbo_spi_demo_api</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>

(2)建立实现类,为了表达支持多个实现的目的,这里分别创建两个实现。分别为HumanHelloService和DogHelloService

import com.lagou.service.HelloService;
import org.apache.dubbo.common.URL;

public class HumanHelloService implements HelloService{
    @Override
    public String sayHello() {
        return "hello 你好";
    }

    @Override
    public String sayHello(URL url) {
        return  "hello url";
    }
}
public class DogHelloService implements HelloService{
    @Override
    public String sayHello() {
        return "wang wang";
    }

    @Override
    public String sayHello(URL url) {
        return "wang url";
    }
}

(3)SPI进行声明操作,在resources目录下创建目录META-INF/dubbo目录,在目录下创建名称为
com.lagou.dubbo.study.spi.demo.api.HelloService的文件,文件内部配置两个实现类名称和对应的全限定名:

human=com.lagou.service.impl.HumanHelloService
dog=com.lagou.service.impl.DogHelloService

main项目创建:
(1)导入接口项目依赖和实现类项目依赖

<dependency>
	<groupId>com.lagou</groupId>
	<artifactId>dubbo_spi_demo_api</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
	<groupId>com.lagou</groupId>
	<artifactId>dubbo_spi_demo_impl</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>

(2)创建DubboSpiMain
测试时,Dubbo有对其进行自我重新实现,借助ExtensionLoader查询出所有的已知实现,并且调用。

import com.lagou.service.HelloService;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;

public class DubboAdaptiveMain {
    public static void main(String[] args) {
        URL   url  = URL.valueOf("test://localhost/hello?hello.service=dog");
        HelloService  adaptiveExtension = ExtensionLoader.getExtensionLoader(HelloService.class).getAdaptiveExtension();
        String  msg = adaptiveExtension.sayHello(url);
        System.out.println(msg);
    }
}

1.3 Dubbo中SPI机制的Activate和Adaptive

Dubbo中的Activate功能,主要表示了一个扩展类被获取到的的条件,符合条件就被获取,不符合条件就不获取 ,@Activate注解在类型和方法上,根据 @Activate中的 group 、value属性来过滤 。
Dubbo中的Adaptive功能,主要解决的问题是如何动态的选择具体的扩展点。通过
getAdaptiveExtension统一对指定接口对应的所有扩展点进行封装,通过URL的方式对扩展点来进行
动态选择。 (Dubbo中所有的注册信息都是通过URL的形式进行处理的)这里同样采用相同的方式进行
实现。具体实现见上面代码。

2. Dubbo的注册中心

2.1 Dubbo的注册中心介绍

Dubbo通过注册中心实现了分布式环境中服务的注册和发现。
Dubbo 有五种注册中心的实现,分别是Multicast,Zookeeper,Redis,Simple和Nacos 。
Dubbo的注册中心实现了如下的功能:
(1)动态注册服务。服务提供者通过注册中心,把自己暴露给消费者,无须消费者逐个更新配置文件。
(2)动态发现服务。消费者动态感知新的配置,路由规则和新的服务提供者。
(3)参数动态调整。支持参数的动态调整,新参数自动更新到所有服务节点。
(4)服务统一配置。统一连接到注册中心的服务配置。

2.2 Dubbo注册中心的工作流程

Dubbo注册中心的工作流程如下:
(1)提供者(Provider)启动时,会向注册中心写入自己的元数据信息(调用方式)。
(2)消费者(Consumer)启动时,也会在注册中心写入自己的元数据信息,并且订阅服务提供者,路由和配置元数据的信息。
(3)服务治理中心(Duubo-admin)启动时,会同时订阅所有消费者,提供者,路由和配置元数据的信息。
(4)当提供者离开或者新提供者加入时,注册中心发现变化会通知消费者和服务治理中心。

2.3 Dubbo注册中心Zookeeper

在这里插入图片描述
注册中心Zookeeper工作流程:
(1)服务提供者启动时,向/dubbo/com.foo.BarService/providers目录下写入自己的URL地址;
(2)服务消费者启动时,订阅/dubbo/com.foo.BarService/providers目录下的提供者URL地址。并向/dubbo/com.foo.BarService/consumers目录下写入自己的URL地址;
(3)监控中心启动时,订阅/dubbo/com.foo.BarService目录下的所有提供者和消费者URL地址;
注册中心Zookeeper工作原理:
Zookeeper通过树形文件存储的数据节点ZNode在/dubbo/Service目录下面建立了四个目录,分别是:
(1)Providers目录下面,存放服务提供者URL和元数据。
(2)Consumers目录下面,存放消费者的URL和元数据。
(3)Routers目录下面,存放消费者的路由策略。
(4)Configurators目录下面,存放多个用于服务提供者动态配置URL元数据信息。
客户端第一次连接注册中心的时候,会获取全量的服务元数据,包括服务提供者和服务消费者以及路由和配置的信息。根据ZooKeeper客户端的特性,会在对应ZNode的目录上注册一个Watcher,同时让客户端和注册中心保持TCP长连接。如果服务的元数据信息发生变化,客户端会接受到变更通知,然后去注册中心更新元数据信息。变更时根据ZNode节点中版本变化进行。

3. Dubbo的容错策略

Dubbo的消费者在提供者数据的时候,它timeout=0代表永不超时,这样就很容易阻塞过多,为了防止这种服务雪崩的情况,Dubbo提供了一些容错处理策略。
Dubbo 主要提供了这样几种容错方式:
Failover,失败自动切换,失败时会重试其它服务器,可以设置重试次数。
Failfast, 快速失败,请求失败后快速返回异常结果,不进行重试。
Failsafe,失败安全,出现异常,直接忽略,会对请求做负载均衡。
Failback, 失败自动恢复, 请求失败后,会自动记录请求到失败队列中,通过定时线程扫描该队列,并定时重试。
Forking, 并行调用多个服务提供者,其中有一个返回,则立即返回结果。
Broadcast,广播调用所有可以连接的服务,任意一个服务返回错误,就任务调用失败。
Mock,响应失败时返回伪造的响应结果。
Available,通过遍历的方式查找所有服务列表,找到第一个可以返回结果的节点,并且返回结果。
Mergable,将多个节点请求合并进行返回。

策略名称优点缺点主要应用场景
Failover对调用者屏蔽调用失败的信息增加RT,额外资源开销,资源浪费对调用RT不敏感的场景
Failfast业务快速感知失败状态进行自主决策产生较多报错的信息调用非幂等性接口,需要快速感知失败的场景
Failsafe即使失败了也不会影响核心流程对于失败的信息不敏感,需要额外的监控旁路系统,失败不影响核心流程正确性的场景
Failback失败自动异步重试重试任务可能堆积对于实时性要求不高,且不需要返回值的一些异步操作
Forking并行发起多个调用,降低失败概率消耗额外的机器资源,需要确保操作幂等性资源充足,且对于失败的容忍度较低,实时性要求高的场景
Broadcast支持对所有的服务提供者进行操作资源消耗很大通知所有提供者更新缓存或日志等本地资源信息

4. Dubbo的服务降级

4.1 什么是服务降级

服务降级,当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务有策略的降低服务级别,以释放服务器资源,保证核心任务的正常运行。

4.2 为什么要服务降级

防止分布式服务发生雪崩效应,即当一个请求发生超时,一直等待着服务响应,那么在高并发情况下,很多请求都是因为这样一直等着响应,直到服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机,这样下去将导致整个分布式服务都瘫痪,这就是雪崩。

4.3 Dubbo服务降级实现方式

(1)在Dubbo管理控制台配置服务降级(即屏蔽和容错)
mock=force:return+null 表示消费方对该服务的方法调用都直接返回null值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回null值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。在这里插入图片描述
(2)指定返回简单值或者null

<dubbo:reference id="xxService" check="false" interface="com.xx.XxService"
timeout="3000" mock="return null" />
<dubbo:reference id="xxService2" check="false" interface="com.xx.XxService2" 
timeout="3000" mock="return 1234" />

如果是标注,则使用@Reference(mock=“return null”) @Reference(mock=“return 简单值”),也支持 @Reference(mock=“force:return null”)

(3)使用java代码动态写入配置中心

RegistryFactory registryFactory =
ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://IP:PORT"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?
category=configurators&dynamic=false&application=foo&mock=force:return+null"));

(4)整合hystrix实现服务降级,见第八部分项目实战。
Dubbo服务降级的真实含义:并不是对provider进行操作,而是告诉consumer,调用服务时要做哪些动作。

5. Dubbo的负载均衡

Dubbo中为了消费者能很好的调用到对应的服务提供者,也为了保证服务调用的高可用提供了多种负载均衡策略。
Dubbo的负载均衡策略有四种(缺省为Random随机调用):
Random, 随机策略,按照权重设置随机概率做负载均衡。
RoundRobin, 轮询策略,按照公约后的权重设置轮询比例。
LeastActive, 按照活跃数调用策略,活跃度差的被调用的次数多。活跃度相同的Invoker进行随机调用。
ConsistentHash, 一致性Hash策略,相同参数的请求总是发到同一个提供者。

6. Dubbo的过滤器

6.1 Dubbo的Filter介绍

在服务的调用过程中,在服务消费者调用服务提供者的前后,都会调用Filter(过滤器)。可以针对消费者和提供者配置对应的过滤器,由于过滤器在RPC执行过程中都会被调用,所以为了提高性能需要根据具体情况配置。Dubbo框架中有自带的系统过滤器,服务提供者有11个,服务消费者有5个。

6.2 Dubbo的Filter使用

Dubbo中过滤器的使用可以通过@Activate的注释,或者配置文件实现。
举例:配置文件实现过滤器

<!-- 消费者过滤器配置 -->
<dubbo:reference filter="filter01,filter02"/>
<!-- 消费者默认过滤器配置,拦截reference过滤器 -->
<dubbo:consumer filter="filter03,filter04"/>
<!-- 提供者过滤器配置 -->
<dubbo:service filter="filter05"/>
<!-- 提供者默认过滤器配置,拦截service过滤器 -->
<dubbo:provider filter="filter06,filter07"/>

过滤器的使用遵循以下几个规则:
(1)过滤器顺序,过滤器执行是有顺序的。例如,用户定义的过滤器的过滤顺序默认会在系统过滤器之后。又例如,上图中 filter=“filter01, filter02”,filter01 过滤器执行就在filter02之前。
(2)过滤器失效,如果针对某些服务或者方法不希望使用某些过滤器,可以通过“-”(减号)的方式使该过滤器失效。例如,filter=“-filter01”。
(3)过滤器叠加,如果服务提供者和服务消费者都配置了过滤器,那么两个过滤器会被叠加生效。
由于,每个服务都支持多个过滤器,而且过滤器之间有先后顺序。因此在设计上Dubbo采用了装饰器模式,将Invoker进行层层包装,每包装一层就加入一层过滤条件。在执行过滤器的时候就好像拆开一个一个包装一样。

7. Dubbo的协议

7.1 Dubbo 支持的通信协议

(1)dubbo协议
Dubbo默认的通信协议就是dubbo协议,单一长连接,进行的是NIO异步通信,基于hessian作为序列化协议。适用场景:传输数据量小(每次请求在100kb以内),但是并发量很高。

(2)rmi协议
支持java二进制序列化,多个短连接。适用场景:消费者和提供者数量差不多的情况,适用于文件的传输,一般较少用。

(3)hessian协议
支持hessian序列化协议,多个短连接。适用场景:提供者数量比消费者数量多的情况,适用于文件的传输,跨语言传输,一般较少用。

(4)http协议
支持json序列化协议,多个短连接,采用同步传输。适用场景:提供者数量比消费者数量多的情况,数据包混合。

(5)webservice协议
支持SOAP文本序列化协议,多个短连接,采用同步传输。适用场景:系统集成和跨语言调用。

7.2 Dubbo支持的序列化协议

Dubbo 支持hession、java二进制序列化、json序列化、SOAP文本序列化多种序列化协议。但是 hessian是其默认的序列化协议。

7.3 Dubbo协议详解

Dubbo的缺省协议,使用基于mina 1.1.7和hessian 3.2.1的tbremoting交互。

  • 连接个数:单连接
  • 连接方式:长连接
  • 传输协议:TCP协议
  • 传输方式:NIO异步传输
  • 序列化协议:Hessian二进制序列化协议
  • 适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo协议传输大文件或超大字符串。
  • 适用场景:常规远程服务方法调用
    Dubbo协议采用固定长度的消息头(16字节)和不定长度的消息体来进行数据传输,消息头定义了底层框架(netty)在IO线程处理时需要的信息,协议的报文格式如下:
    在这里插入图片描述
    协议体包含了传输的主要内容,它是由16字节长的报文组成,每个字节包括8个二进制位。协议详情如下:
Magic - Magic High & Magic Low (16 bits)
标识协议版本号,Dubbo 协议:0xdabb
Serialization ID (5 bit)
标识序列化类型:比如 fastjson 的值为6。
Event (1 bit)
标识是否是事件消息,例如,心跳事件。如果这是一个事件,则设置为1。
Two Way (1 bit)
仅在 Req/Res 为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器
的返回值,则设置为1。
Req/Res (1 bit)
标识是请求或响应。请求: 1; 响应: 0。
Status (8 bits)
仅在 Req/Res 为0(响应)时有用,用于标识响应的状态。
	20 - OK
	30 - CLIENT_TIMEOUT
	31 - SERVER_TIMEOUT
	40 - BAD_REQUEST
	50 - BAD_RESPONSE
	60 - SERVICE_NOT_FOUND
	70 - SERVICE_ERROR
	80 - SERVER_ERROR
	90 - CLIENT_ERROR
	100 - SERVER_THREADPOOL_EXHAUSTED_ERROR
Request ID (64 bits)
标识唯一请求。类型为long。
Data Length (32 bits)
序列化后的内容长度(可变部分),按字节计数。int类型。
Variable Part
被特定的序列化类型(由序列化 ID 标识)序列化后,每个部分都是一个 byte [] 或者 byte
	如果是请求包 ( Req/Res = 1),则每个部分依次为:
		Dubbo version
		Service name
		Service version
		Method name
		Method parameter types
		Method arguments
		Attachments
	如果是响应包(Req/Res = 0),则每个部分依次为:
		返回值类型(byte),标识从服务器端返回的值类型:
		返回空值:RESPONSE_NULL_VALUE 2
		正常响应值: RESPONSE_VALUE 1
		异常:RESPONSE_WITH_EXCEPTION 0
		返回值:从服务端返回的响应bytes

注意:对于(Variable Part)变长部分,当前版本的Dubbo框架使用json序列化时,在每部分内容间额外增加了换行符作为分隔,请在Variable Part的每个part后额外增加换行符, 如:

Dubbo version bytes (换行符)
Service name bytes (换行符)
...

优点:
协议设计上很紧凑,能用1个bit表示的,不会用一个byte来表示,比如boolean类型的标识。请求、响应的header 一致,通过序列化器对content组装特定的内容,代码实现起来简单。
可以改进的点:
(1)类似于http请求,通过header就可以确定要访问的资源,而Dubbo需要涉及到用特定序列化协议才可以将服务名、方法、方法签名解析出来,并且这些资源定位符是string类型或者string数组,很容易转成bytes,因此可以组装到header中。类似于http2的header压缩,对于RPC调用的资源也可以协商出来一个int来标识,从而提升性能,如果在header上组装资源定位符的话,该功能则更易实现。
(2)通过req/res是否是请求后,可以精细定制协议,去掉一些不需要的标识和添加一些特定的标识。比如status , twoWay标识可以严格定制,去掉冗余标识。还有超时时间是作为Dubbo的attachment进行传输的,理论上应该放到请求协议的header中,因为超时是网络请求中必不可少的。提到 attachment ,通过实现可以看到attachment中有一些是跟协议content中已有的字段是重复的,比如 path和version 等字段,这些会增大协尺寸。
(3)Dubbo会将服务名com.alibaba.middleware.hsf.guide.api.param.ModifyOrderPriceParam ,转换为com/alibaba/middleware/hsf/guide/api/param/ModifyOrderPriceParam; ,理论上是不必要的,最后追加一个 ; 即可。
(4)Dubbo协议没有预留扩展字段,没法新增标识,扩展性不太好,比如新增响应上下文的功能,只有改协议版本号的方式,但是这样要求客户端和服务端的版本都进行升级,对于分布式场景很不友好。

8. Dubbo的线程池

8.1 Dubbo线程池的使用

DUBBO提供多种线程池策略,选择线程池策略并在配置文件指定threadpool属性,如下:

<dubbo:protocol name="dubbo" threadpool="fixed" threads="100" />
<dubbo:protocol name="dubbo" threadpool="cached" threads="100" />
<dubbo:protocol name="dubbo" threadpool="limited" threads="100" />
<dubbo:protocol name="dubbo" threadpool="eager" threads="100" />

8.2 Dubbo提供的4种线程池策略

这里的线程池ThreadPool也是一个扩展接口SPI,Dubbo提供了该扩展接口的一些实现,具体实现如下:
CachedThreadPool:创建一个自适应线程池,当线程处于空闲1分钟时候,线程会被回收,当有新请求到来时候会创建新线程

public class CachedThreadPool implements ThreadPool {

    @Override
    public Executor getExecutor(URL url) {
        //线程名,默认为Dubbo
        String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
        //核心线程数,默认为0
        int cores = url.getParameter(CORE_THREADS_KEY, DEFAULT_CORE_THREADS);
        //可使用的最大线程数,默认为Integer.MAX_VALUE
        int threads = url.getParameter(THREADS_KEY, Integer.MAX_VALUE);
        //队列数,默认为0
        int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
        //线程存活时间,默认60s
        int alive = url.getParameter(ALIVE_KEY, DEFAULT_ALIVE);
        //根据queue决定是SynchronousQueue还是LinkedBlockingQueue,默认queue=0,所以是SynchronousQueue
        return new ThreadPoolExecutor(cores, threads, alive, TimeUnit.MILLISECONDS,
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)),
                //NamedInternalThreadFactory主要用于修改线程名,方便我们排查问题。
                // AbortPolicyWithReport对拒绝的任务打印日志,也是方便排查问题。
                new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }
}

LimitedThreadPool:创建一个线程池,这个线程池中线程个数随着需要量动态增加,但是数量不超过配置的阈值的个数,另外空闲线程不会被回收,会一直存在

public class LimitedThreadPool implements ThreadPool {

    @Override
    public Executor getExecutor(URL url) {
        //线程名,默认为Dubbo
        String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
        //核心线程数,默认为0
        int cores = url.getParameter(CORE_THREADS_KEY, DEFAULT_CORE_THREADS);
        //可使用的最大线程数,默认为200
        int threads = url.getParameter(THREADS_KEY, DEFAULT_THREADS);
        //队列数,默认为0
        int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
        //线程存活时间,默认Long.MAX_VALUE,说明线程数只会增加不会减少
        return new ThreadPoolExecutor(cores, threads, Long.MAX_VALUE, TimeUnit.MILLISECONDS,
                //根据queue决定是SynchronousQueue还是LinkedBlockingQueue,默认queue=0,所以是SynchronousQueue
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)),
                //NamedInternalThreadFactory主要用于修改线程名,方便我们排查问题。
                // AbortPolicyWithReport对拒绝的任务打印日志,也是方便排查问题。
                new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }

}

FixedThreadPool:创建一个复用固定个数线程的线程池

public class FixedThreadPool implements ThreadPool {

    @Override
    public Executor getExecutor(URL url) {
        //线程名,默认为Dubbo
        String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
        //可使用的最大线程数,默认为200
        int threads = url.getParameter(THREADS_KEY, DEFAULT_THREADS);
        //队列数,默认为0
        int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
        //线程存活时间,默认0s
        return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS,
                //根据queue决定是SynchronousQueue还是LinkedBlockingQueue,默认queue=0,所以是SynchronousQueue
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)),
                //NamedInternalThreadFactory主要用于修改线程名,方便我们排查问题。
                // AbortPolicyWithReport对拒绝的任务打印日志,也是方便排查问题。
                new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }

}

EagerThreadPool:创建一个线程池,这个线程池当所有核心线程都处于忙碌状态时候,创建新的线程来执行新任务,而不是把任务放入线程池阻塞队列

public class EagerThreadPool implements ThreadPool {

    @Override
    public Executor getExecutor(URL url) {
        //线程名,默认为Dubbo
        String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
        //核心线程数,默认为0
        int cores = url.getParameter(CORE_THREADS_KEY, DEFAULT_CORE_THREADS);
        //可使用的最大线程数,默认为Integer.MAX_VALUE
        int threads = url.getParameter(THREADS_KEY, Integer.MAX_VALUE);
        //队列数,默认为0
        int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
        //线程存活时间,默认60s
        int alive = url.getParameter(ALIVE_KEY, DEFAULT_ALIVE);
        // init queue and executor
        //自定义实现TaskQueue,默认长度为1,使用时要自己配置下;它会在提交任务时判断是否
        // currentPoolSize<submittedtaskcount<maxpoolsize,
        // 然后通过它的offer方法返回false导致增加工作线程
        TaskQueue<Runnable> taskQueue = new TaskQueue<Runnable>(queues <= 0 ? 1 : queues);
        //EagerThreadPoolExecutor继承ThreadPoolExecutor,对当前线程池提交的任务数submittedTaskCount进行记录
        EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(cores,
                threads,
                alive,
                TimeUnit.MILLISECONDS,
                taskQueue,
                new NamedInternalThreadFactory(name, true),
                new AbortPolicyWithReport(name, url));
        taskQueue.setExecutor(executor);
        return executor;
    }
}

8.3 Dubbo 线程池打满异常分析及解决

问题介绍:
Dubbo默认的线程模型:
Dubbo服务端每次接收到一个Dubbo请求,便交给一个线程池处理,该线程池默认有200个线程,如果200个线程都不处于空闲状态,则客户端会报出如下异常:

Caused by: java.util.concurrent.ExecutionException: org.apache.dubbo.remoting.RemotingException: Server side(192.168.1.101,20880) threadpool is exhausted ...

服务端会打印 WARN 级别的日志:

[DUBBO] Thread pool is EXHAUSTED!

问题原因分析:
(1)客户端/服务端超时时间设置不合理,导致请求无限等待,耗尽了线程数
(2)客户端请求量过大,服务端无法及时处理,耗尽了线程数
(3)服务端由于fullgc等原因导致处理请求较慢,耗尽了线程数
(4)服务端由于数据库、Redis、网络IO阻塞问题,耗尽了线程数
原因可能很多,但究其根本,都是因为业务上出了问题,导致Dubbo线程池资源耗尽了。
问题定位分析:
可以利用阿里巴巴开源的Java 诊断工具Arthas(阿尔萨斯)中的dashboard命令和thread命令进行分析。
(1)使用dashboard命令查看线程全局信息

$ dashboard

【$ dashboard】命令可以查看到线程 ID、线程名、线程组名、线程优先级、线程的状态、线程消耗的 CPU 占比、线程运行总时间、线程当前的中断位状态、是否是 daemon 线程
利用下面命令可以根据线程池名筛选出 Dubbo 服务端线程:

dashboard | grep "DubboServerHandler"

(2)使用thread 命令查看线程具体情况
查看当前最忙的前 n 个线程:

$ thread -n 3

显示所有线程信息:

$ thread

显示当前阻塞其他线程的线程:

$ thread -b

显示指定状态的线程:

$ thread --state TIMED_WAITING

线程状态一共有RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, NEW, TERMINATED 6种。
查看指定线程的运行堆栈:

$ thread 46

分析线程池打满异常的常见问题:
(1)阻塞类问题。例如数据库连接不上导致卡死,运行中的线程基本都应该处于BLOCKED或者 TIMED_WAITING状态,我们可以借助thread --state定位到;
(2)繁忙类问题。例如CPU密集型运算,运行中的线程基本都处于RUNNABLE状态,可以借助于thread -n来定位出最繁忙的线程;
(3)GC类问题。很多外部因素会导致该异常,例如GC就是其中一个因素,这里就不能仅仅借助于thread命令来排查了;
(4)定点爆破。还记得在前面我们通过grep筛选出了一批Dubbo线程,可以通过thread ${thread_id}定向的查看堆栈,如果统计到大量的堆栈都是一个服务时,基本可以断定是该服务出了问题,至于说是该服务请求量突然激增,还是该服务依赖的某个下游服务突然出了问题,还是该服务访问的数据库断了,那就得根据堆栈去判断了。
Dubbo 线程池打满异常问题排查及Dubbo优化:
(1)排查业务异常
(2)合理设置客户端/服务端超时时间
(3)服务端扩容,调整Provider端的 dubbo.provider.threads参数大小,默认200,可以适当提高
(4)客户端限流,调整Consumer端的 dubbo.consumer.actives参数,控制消费者调用的速率

9. Dubbo的路由规则

路由是决定一次请求中需要发往目标机器的重要判断,通过对其控制可以决定请求的目标机器。我们可以通过创建这样的规则来决定一个请求会交给哪些服务器去处理。

9.1 路由规则快速入门

(1)提供两个提供者(一台本机作为提供者,一台为其他的服务器),每个提供者会在调用时可以返回不同的信息以区分提供者。
(2)针对于消费者,我们这里通过一个死循环,每次等待用户输入,再进行调用,来模拟真实的请求
情况。通过调用的返回值,确认具体的提供者。
(3)我们通过ipconfig来查询到我们的IP地址,并且单独启动一个客户端,来进行如下配置(这里假设
我们希望隔离掉本机的请求,都发送到另外一台机器上)。
(4)通过这个程序执行后,我们就通过消费端不停的发起请求,看到真实的请求都发到了除去本机以
外的另外一台机器上。

9.2 路由规则详解

路由规则本质上是通过在Zookeeper中保存一个节点数据,来记录路由规则。消费者会通过监听这个服务的路径,来感知整个服务的路由规则配置,然后进行适配。
主要路由配置的参数如下:
route:// 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填。
0.0.0.0 表示对所有IP地址生效,如果只想对某个IP的生效,请填入具体IP,必填。
com.lagou.service.HelloService 表示只对指定服务生效,必填。
category=routers 表示该数据为动态配置类型,必填。
dynamic : 是否为持久数据,当指定服务重启时是否继续生效。必填。
runtime : 是否在设置规则时自动缓存规则,如果设置为true则会影响部分性能。
rule : 是整个路由最关键的配置,用于配置路由规则。
… => … 在这里 => 前面的就是表示消费者方的匹配规则,可以不填(代表全部)。 => 后方则必须填写,表示当请求过来时,如果选择提供者的配置。官方这块儿也给出了详细的示例,可以按照那里来讲。其中使用最多的便是host参数。 必填。

10. Dubbo的管理控制台 dubbo-admin

10.1 dubbo-admin的作用

主要包含:服务管理 、 路由规则、动态配置、服务降级、访问控制、权重调整、负载均衡等管理功能
如我们在开发时,需要知道Zookeeper注册中心都注册了哪些服务,有哪些消费者来消费这些服务。我们可以通过部署一个管理中心来实现。其实管理中心就是一个web应用,原来是war(2.6版本以前)包需要部署到tomcat即可。现在是jar包可以直接通过java命令运行。

10.2 控制台安装步骤

(1)从git 上下载项目 https://github.com/apache/dubbo-admin
(2)修改项目下的dubbo.properties文件
注意:dubbo.registry.address对应的值需要对应当前使用的Zookeeper的ip地址和端口号

dubbo.registry.address=zookeeper://zk所在机器ip:zk端口
dubbo.admin.root.password=root
dubbo.admin.guest.password=guest

(3)切换到项目所在的路径,使用mvn打包
mvn clean package -Dmaven.test.skip=true
(4)java命令运行
java -jar 对应的jar包

10.3 使用控制台

(1)访问http://IP:端口;
(2)输入用户名root,密码root;
(3)点击菜单查看服务提供者和服务消费者信息。

六、Dubbo源码分析

本次的源码分析将基于Dubbo 2.7.8的版本。

1. Dubbo源码环境构建

源码下载、编译和导入步骤如下:
(1)Dubbo的项目在github中的地址为: https://github.com/apache/dubbo
(2)进入需要进行下载的地址,执行 git clone https://github.com/apache/dubbo.git
(3)为了防止master中代码不稳定,进入Dubbo项目cd dubbo 可以切入到最近的release分支git
checkout 2.7.8-release
(4)进行本地编译,进入Dubbo项目cd dubbo , 进行编译操作mvn clean install -DskipTests
(5)使用IDEA引入项目。

2. Dubbo分层设计

Dubbo整体结构设计大致上可分为三层,分别是:
(1)业务层:即处理业务逻辑层
(2)RPC 层:即远程过程调用层
(3)Remoting 层:即远程数据传输层
在这里插入图片描述

组件名称描述
Service业务层,包括业务代码的实现,比如接口、实现类,直接面向开发者
Config配置层,对外提供配置,以ServiceConfig和ReferenceConfig为核心,可以直接初始化配置类,也可以解析配置文件生成
Proxy服务代理层,无论是生产者还是消费者,框架都会产生一个代理类,它用来调用远程接口,整个过程对上层透明,就是业务层对远程调用无感
Registry注册中心层,封装服务地址的注册与发现,以服务的URL为中心
Cluster路由层 (集群容错层),负责远程调用的容错策略,负载均衡策略以及路由策略,并且它桥接注册中心,以Invoker为核心
Monitor监控层,负责监控RPC调用相关的信息,比如调用次数、成功失败的情况、调用时间等
Protocol远程调用层,封装RPC调用,无论是服务的暴露还是服务的引用都是在Protocol中作为主功能入口,负责Invoker的整个生命周期,Dubbo中所有的模型都向Invoker靠拢
Exchange信息交换层,建立Request-Response模型,封装请求和响应模式, 比如把请求由同步 转换成异步
Transport网络传输层,统一网络传输的接口,比如netty和mina统一为一个网络传输接口
Serialize数据序列化层,负责管理整个框架中的数据传输的序列化和反序列化

3. Dubbo远程调用工作流程

Dubbo 框架是用来处理分布式系统中,服务发现与注册以及调用问题的,并且管理调用过程。
在这里插入图片描述
Dubbo远程调用流程:
0.服务提供者在启动的时候,会通过读取一些配置将服务实例化;
1.Proxy封装服务调用接口,方便调用者调用。客户端获取Proxy时,可以像调用本地服务一样,调用远程服务。
2.Proxy在封装时,需要调用Protocol定义协议格式,例如:Dubbo Protocol;
3.将Proxy封装成Invoker,它是真实服务调用的实例;
4.将Invoker转化成Exporter,Exporter只是把Invoker包装了一层,是为了在注册中心中暴露自己,方便消费者使用;
5.将包装好的Exporter注册到注册中心;
6.服务消费者建立好实例,会到服务注册中心订阅服务提供者的元数据。元数据包括服务IP和端口以及调用方式(Proxy);
7.消费者会通过获取的Proxy进行调用。通过服务提供方包装过程可以知道,Proxy实际包装了Invoker 实体,因此需要使用Invoker进行调用;
8.(9)在Invoker调用之前,通过Directory获取服务提供者的Invoker列表。在分布式的服务中有可能出现同一个服务,分布在不同的节点上;
9.(10)通过路由规则了解,服务需要从哪些节点获取;
10.(8)Invoker调用过程中,通过Cluster进行容错,如果遇到失败策略进行重试;
11.调用中,由于多个服务可能会分布到不同的节点,就要通过LoadBalance来实现负载均衡;
12.Invoker调用之前还需要经过Filter,它是一个过滤链,用来处理上下文,限流和计数的工作;
13.生成过滤以后的Invoker;
14.用Client进行数据传输;
15.Codec会根据Protocol定义的协议,进行协议的构造;
16.构造完成的数据,通过序列化Serialization传输给服务提供者;
17.Request已经到达了服务提供者,它会被分配到线程池(ThreadPool)中进行处理;
18.Request被线程中的server执行;
19.Server拿到请求以后查找对应的Exporter(包含有 Invoker);
20.由于Exporter也会被Filter层层包裹;
21.通过Filter以后获得Invoker;
22.最后,对服务提供者实体进行调用。

4. Dubbo源码框架设计

在这里插入图片描述
【dubbo-parent】
dubbo-parent是dubbo的父工程,dubbo的maven模块都会引入该pom文件。
【dubbo-all】
dubbo-all模块定义了dubbo的打包脚本,使用dubbo库的时候,需要引入该pom文件。
【dubbo-bom】
dubbo-bom模块,利用Maven BOM统一定义了dubbo的版本号,方便被其他模块引用,例如dubbo-demo中的pom文件就引用了dubbo-bom/pom.xml。
【dubbo-dependencies-bom】
dubbo-dependencies-bom模块,利用Maven BOM统一定义了dubbo依赖的第三方库的版本号,dubbo-parent会引入该bom。
【dubbo-common】
dubbo-common模块,公共逻辑模块,主要包括通用模型、配置类和Util 类等。
【dubbo-remoting】
dubbo-remoting模块,远程通讯模块,相当于dubbo协议的实现,提供了各种客户端和服务端的通信功能,例如grizzly,netty,tomcat等,RPC除了rmi的协议都要用到此模块。主要包括如下:
dubbo-remoting-api:定义了客户端和服务端的连接;
dubbo-remoting-grizzly:基于grizzly实现的Client和Server;
dubbo-remoting-http:基于Jetty或Tomcat实现的Client和Server;
dubbo-remoting-mina:基于Mina实现的Client和Server;
dubbo-remoting-netty4:基于Netty4实现的Client和Server;
dubbo-remoting-p2p: P2P服务器,封装了注册中心multicast的Client和Server通信,会用到这个服务器;
dubbo-remoting-zookeeper:封装了注册中心Zookeeper的Client和Server通信;
【dubbo-rpc】
dubbo-rpc模块,远程调用模块,dubbo提供了非常多的协议实现,这个模块抽象各种协议,以及动态代理,只包含一对一的调用,不关心集群的管理,但官方推荐的还是自己的dubbo协议。这个模块依赖与dubbo-remoting模块。其中:dubbo-rpc-api 抽象了动态代理的各种协议,实现了一对一的调用包都是各种协议的实现。
【dubbo-cluster】
dubbo-cluster模块,集群容错模块,这个模块包括负载均衡、容错、路由等,集群的地址列表可以是静态配置,也可以由注册中心提供。如下图,cluster把多个Invoker伪装成一个Invoker,并且在伪装的过程中加入了容错策略,失败重试机制,负载均衡策略等。
在这里插入图片描述
主要包如下:
configurator包:配置包,dubbo的基本设计原理都是采用url作为配置信息的统一格式,所有拓展点都通过传递url携带配置信息,这个包就是更具统一配置规则生成配置信息;
directory包:Directory代表了多个Invoker,并且它的值会随着注册中心的服务变更推送而变化。Invoker是Provider的一个调用Service的抽象,Invoker封装了Provider地址以及Service接口信息;
governance包:SPI扩展服务治理包;
interceptor包:选择一个特定的地址调用程序之前在最外层工作;
loadbalance包:封装了负载均衡的实现,负责利用负载均衡算法,从多个Invoker中选出具体的一个Invoker用于此次的调用,如果调用失败,则需要重新选择;
merger包:封装了合并返回结果,分组聚合到方法,支持多种数据结构类型;
router包:封装了路由规则的实现,路由规则决定了一次dubbo服务调用的目标服务器,路由规则分两种:条件路由规则和脚本路由规则,并且支持可拓展;
support包:封装了各类Invoker和cluster,包括集群容错模式和分组聚合的cluster以及相关的Invoker。
【dubbo-registry】
dubbo-registry模块,注册中心模块,dubbo注册中心的实现是由Multicast,Zookeeper,Redis,Simple 和 Nacos这五种,这个模块封装了dubbo所支持的注册中心的实现。说明:
dubbo-registry-api: 抽象了注册中心的注册发现,实现了一些公用的方法,让子类只关注重要的方法;
其他模块:封装了注册中心的实现方法,其中dubbo-registry-default就是官方文档里面的Simple注册中心。
【dubbo-monitor】
dubbo-monitor模块,监控模块,对服务的监控,比如统计服务调用次数,调用时间等,是调用链跟踪的服务dubbo-monitor-api:定义了monitor相关的接口,实现了监控所需要的过滤器;
dubbo-monitor-default:实现了dubbo相关的监控功能。
【dubbo-config】
dubbo-config模块,配置模块,dubbo提供了四种配置方式,包括XML配置,属性配置,API配置,注解配置,配置模块就是实现了这四种配置。
dubbo-config-api :实现了API配置和属性配置的功能;
dubbo-config-spring: 实现了XML配置和注解配置
【dubbo-container】
dubbo-container模块,容器模块,一个 Standlone 的容器,以简单的 Main 加载 Spring 容器启动,因为服务通常不需要 Tomcat/JBoss 等 Web 容器的特性,没必要用 Web 容器去加载服务
dubbo-container-api:定义了Container接口,实现了服务加载的Main方法;
其他三个分别提供了对应的容器,供Main方法加载
【dubbo-filter】
dubbo-filter模块,过滤器模块,这个模块提供了内置的一些过滤器
dubbo-filter-cache :提供缓存过滤器;
dubbo-filter-validation:提供参数校验过滤器
【dubbo-plugs】
dubbo-plugs模块, 插件模块,提供内置插件
dubbo-qos:提供了在线运维的命令;
dubbo-auth:提供了权限控制
【dubbo-serialization】
dubbo-serialization模块,序列化模块,该组件提供了支持各种序列化框架的实现
dubbo-serialization-api:定义了Serialization的接口以及数据输入输出的接口;
其他的包都是实现了对应的序列化框架的方法,dubbo内置的就是这几类的序列化框架,序列化也支持扩展。
【dubbo-metadata】
dubbo-metadata模块,元数据管理模块,元数据(Metadata)指的是服务分组、服务版本、服务名、方法列表、方法参数列表、超时时间等,这些信息将会存储在元数据中心之中。
现在支持 consul、etcd、nacos、redis、zookeeper 这五种组件的元数据配置。
【dubbo-demo】
dubbo-demo模块,示例模块,这个模块是快速启动示例,包含了服务提供和服务调用,注册中心是multicast,调用方式是基于XML配置。

5. Dubbo SPI机制源码剖析

基于前面SPI机制的说明,可以看到SPI机制运行流程图如下:
在这里插入图片描述
SPI机制运行过程主要包括两部分:
(1)getExtensionLoader: 获取扩展点加载器,并加载所对应的所有的扩展点实现
(2)getExtension: 根据name获取扩展的指定实现
源码分析过程如下:
(1)实例化 ExtensionLoader

private static <T> boolean withExtensionAnnotation(Class<T> type) {
	// 包含`@SPI`注解在接口上
	return type.isAnnotationPresent(SPI.class);
	}
	public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
		@SuppressWarnings("unchecked")
		// 必须传入类型
		if (type == null) {
			throw new IllegalArgumentException("Extension type null");
		}
		// 必须是接口类型
		if (!type.isInterface()) {
			throw new IllegalArgumentException("Extension type (" + type + ") is not
			an interface!");
		}
		// 必须包含SPI的注解
		if (!withExtensionAnnotation(type)) {
			throw new IllegalArgumentException("Extension type (" + type +
			") is not an extension, because it is NOT annotated with @" +
			SPI.class.getSimpleName() + "!");
		}
		// 尝试从已经加载过的数据中去读取(缓存功能)
		ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
		if (loader == null) {
			// 如果没有的话,才会进行初始化,并且放入到缓存汇总
			EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
			loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
		}
		return loader;
}

(2)获取 ExtensionFactory 对象

@SPI
public interface ExtensionFactory {
	/**
	* Get extension.
	*
	* @param type object type.
	* @param name object name.
	* @return object instance.
	*/
	<T> T getExtension(Class<T> type, String name);
}

(3)获取 ExtensionFactory工厂对象的具体实现,

private ExtensionLoader(Class<?> type) {
	this.type = type;
	// 这里需要对对象的工厂做额外的创建,可以看到扩展的工厂也是一个扩展点
	objectFactory = (type == ExtensionFactory.class ? null :
	ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

可以在 dubbo-common/src/main/resources/META-
INF/dubbo/internal/org.apache.dubbo.common.extension.ExtensionFactory 中看到,他默认有两个实现的提供(之前版本有三类,包括SpringExtensionFactory):

adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory

(4)利用ExtensionFactory工厂对象的具体实现获取所有支持扩展信息的实现,即ExtensionLoader.getSupportedExtensions

public Set<String> getSupportedExtensions() {
	// 获取所有的扩展类信息
	Map<String, Class<?>> clazzes = getExtensionClasses();
	// 返回所有的扩展点名称
	return Collections.unmodifiableSet(new TreeSet<>(clazzes.keySet()));
}

(5)加载对应的扩展实现类,即getExtensionClasses
在通过名称获取扩展类之前,首先需要根据配置文件解析出扩展类名称到扩展类的映射关系表classes中,之后再根据扩展项名称从映射关系表中获取取对应的扩展类即可。

private Map<String, Class<?>> getExtensionClasses() {
	// 从缓存中获取已加载的扩展类
	Map<String, Class<?>> classes = cachedClasses.get();
	// 双重检查
	if (classes == null) {
		// 为空的话,则锁住,标识只会被执行一次
		synchronized (cachedClasses) {
			classes = cachedClasses.get();
			if (classes == null) {
				// 进行加载信息 加载扩展类
				classes = loadExtensionClasses();
				cachedClasses.set(classes);
			}
		}
	}
	return classes;
}

(6)接下来,加载当前SPI的默认实现以及加载这个类的所有扩展点实现,并且按照name和Class对象的形式存储,即loadExtensionClasses中cacheDefaultExtensionName 和 loadDirectory

private Map<String, Class<?>> loadExtensionClasses() {
	// 加载默认扩展的实现名称
	cacheDefaultExtensionName();
	// 获取其中每一种实现的名称和对应的classes
	// 具体的目录请参照下面的所有目录
	Map<String, Class<?>> extensionClasses = new HashMap<>();
	// internal extension load from ExtensionLoader's ClassLoader first
	loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(),true);
	loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY,
	type.getName().replace("org.apache", "com.alibaba"), true);
	loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
	loadDirectory(extensionClasses, DUBBO_DIRECTORY,
	type.getName().replace("org.apache", "com.alibaba"));
	loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
	loadDirectory(extensionClasses, SERVICES_DIRECTORY,
	type.getName().replace("org.apache", "com.alibaba"));
	return extensionClasses;
}
private void cacheDefaultExtensionName() {
	// 获取当前类是否包含SPI注解,一般走到这里都是拥有这个注解的
	final SPI defaultAnnotation = type.getAnnotation(SPI.class);
	if (defaultAnnotation == null) {
		return;
	}
	// 来获取其的value值,这个值主要的作用是设置这个SPI中的默认扩展名
	// 比如LoadBalance的默认实现就是random。就是通过这里进行的设置
	String value = defaultAnnotation.value();
	if ((value = value.trim()).length() > 0) {
		String[] names = NAME_SEPARATOR.split(value);
		if (names.length > 1) {
			throw new IllegalStateException("More than 1 default extension name"
			"on extension" + type.getName()+ ": " + Arrays.toString(names));
		}
		if (names.length == 1) {
			cachedDefaultName = names[0];
		}
	}
}

loadDirectory方法主要功能是从这个文件夹中寻找真正的文件列表,并且对其中的文件内容利用loadResource解析并且放入到extensionClasses Map中。

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir,
String type, boolean extensionLoaderClassLoaderFirst) {
	// 文件名称规则: 路径/包名.接口名
	String fileName = dir + type;
	try {
		// 寻找classloader和url列表
		Enumeration<java.net.URL> urls = null;
		ClassLoader classLoader = findClassLoader();
		// try to load from ExtensionLoader's ClassLoader first
		// 如果需要的话, 需要先从当前类的ClassLoader中寻找
		if (extensionLoaderClassLoaderFirst) {
			ClassLoader extensionLoaderClassLoader =									             ExtensionLoader.class.getClassLoader();
			if (ClassLoader.getSystemClassLoader() !=
				extensionLoaderClassLoader) {
				urls = extensionLoaderClassLoader.getResources(fileName);
			}
		}
		// 如果找不到任何的URL列表,则继续尝试去其当前线程的ClassLoader中寻找
		if(urls == null || !urls.hasMoreElements()) {
			if (classLoader != null) {
				urls = classLoader.getResources(fileName);
			} else {
				urls = ClassLoader.getSystemResources(fileName);
			}
		}
		// 如果存在文件的话
		if (urls != null) {
			while (urls.hasMoreElements()) {
			// 遍历每一个资源文件,并且进行加载资源信息到extensionClasses, 
            //主要功能是读取文件内容
				java.net.URL resourceURL = urls.nextElement();
				loadResource(extensionClasses, classLoader, resourceURL);
			}
		}
	} catch (Throwable t) {
		logger.error("Exception occurred when loading extension class
		"(interface: " + type + ", description file: " + fileName + ").", t);
	}
}

(7)读取文件操作,并利用loadClass来加载类信息,即loadResource

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader
classLoader, java.net.URL resourceURL) {
	try {
		// 读取文件
		try (BufferedReader reader = new BufferedReader(new
		InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
			String line;
			while ((line = reader.readLine()) != null) {
				// 截取文件#前面的内容
				final int ci = line.indexOf('#');
				if (ci >= 0) {
					line = line.substring(0, ci);
				}
				line = line.trim();
				// 如果有内容的话
				if (line.length() > 0) {
					try {
						// 则进行加载key=value的形式数据
						String name = null;
						int i = line.indexOf('=');
						if (i > 0) {
							name = line.substring(0, i).trim();
							line = line.substring(i + 1).trim();
						}
						if (line.length() > 0) {
							// 对类信息进行加载操作
							loadClass(extensionClasses, resourceURL,
							Class.forName(line, true, classLoader), name);
						}
					} catch (Throwable t) {
						IllegalStateException e = new
						IllegalStateException("Failed to load extension class (interface: " + type + ",class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                    	exceptions.put(line, e);
					}
				}
			}
		}
	} catch (Throwable t) {
		logger.error("Exception occurred when loading extension class"
	"(interface: " +type + ", class file: " + resourceURL + ") in " + resourceURL,t);
	}
}

(8)最终加载实现类信息,即loadClass

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL
resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
	// 当前扩展点的实现,必须是当前扩展接口的实现才可以
	if (!type.isAssignableFrom(clazz)) {
		throw new IllegalStateException("Error occurred when loading extension"
		"class (interface: " +type + ", class line: " + clazz.getName() + "), class "
		+ clazz.getName() + " is not subtype of interface.");
	}
	// 如果是包含了Adaptive注解,则认为是需要对扩展点包装的方法,这里只做了存储操作,存储至
	//cachedAdaptiveClass中
	if (clazz.isAnnotationPresent(Adaptive.class)) {
		cacheAdaptiveClass(clazz);
	} else if (isWrapperClass(clazz)) {
		// 判断是否是wapper类型, 是否构造函数中有该接口类型的传入
		// wrapper类型的意思是,对当前的扩展点实现封装功能处理
		cacheWrapperClass(clazz);
	} else {
		clazz.getConstructor();
		// 寻找他是否已经定义过了名称, 这里就不继续往里面细看了,主要是获取当前类的
		//org.apache.dubbo.common.Extension注解,如果有的话就使用这个名称,否则的话就是用当前类的
		//简单名称
		if (StringUtils.isEmpty(name)) {
			name = findAnnotationName(clazz);
			if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the"
				"class " + clazz.getName() + " in the config " + resourceURL);
			}
		}
		// 否则的话,就对这个名称和class做映射关系
		String[] names = NAME_SEPARATOR.split(name);
		if (ArrayUtils.isNotEmpty(names)) {
			// 如果当前类拥有Activate注解,则将其进行添加到cachedActivates对象中,意味
			//着需要执行
			cacheActivateClass(clazz, names[0]);
			// 进行名称映射保存
			for (String n : names) {
				cacheName(clazz, n);
				saveInExtensionClass(extensionClasses, clazz, n);
			}
		}
	}
}

(9)上面已经加载好所有的扩展点实现,接下来根据name对扩展点进行处理和进行加锁来创建
真实的引用,其中都是有使用缓存来处理。

public T getExtension(String name) {
	if (StringUtils.isEmpty(name)) {
		throw new IllegalArgumentException("Extension name null");
	}
	// 获取当前SPi的默认扩展实现类
	if ("true".equals(name)) {
		return getDefaultExtension();
	}
	// 获取当前类的holder,实现原理和cachedClasses的方式相同,都是建立同一个引用后再进行加锁
	final Holder<Object> holder = getOrCreateHolder(name);
	Object instance = holder.get();
	if (instance == null) {
		synchronized (holder) {
			instance = holder.get();
			if (instance == null) {
				// 真正进行创建实例
				instance = createExtension(name);
				holder.set(instance);
			}
		}
	}
	return (T) instance;
}
private Holder<Object> getOrCreateHolder(String name) {
	// 获取当前名称的和对象Holder的映射关系
	Holder<Object> holder = cachedInstances.get(name);
	if (holder == null) {
		// 如果不存在的话,则使用putIfAbsent的原子操作来设置值,这个值可以保证多线程的额情
		//况下有值的时候不处理,没有值进行保存
		cachedInstances.putIfAbsent(name, new Holder<>());
		// 获取真实的holder处理器
		holder = cachedInstances.get(name);
	}
	return holder;
}
private T createExtension(String name) {
	// 从配置文件中加载所有的扩展类 可以得到配置项名称 到配置类的映射关系
	Class<?> clazz = getExtensionClasses().get(name);
	if (clazz == null) {
		throw findException(name);
	}
	try {
		// 获取是否已经有实例了
		T instance = (T) EXTENSION_INSTANCES.get(clazz);
		if (instance == null) {
			// 没有的话,同样适用putIfAbsent的方式来保证只会创建一个对象并且保存
			EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
			instance = (T) EXTENSION_INSTANCES.get(clazz);
		}
		// 注入其他扩展点的实体,用于扩展点和其他的扩展点相互打通
		injectExtension(instance);
		// 进行遍历所有的包装类信息,分别对包装的类进行包装实例化,并且返回自身引用
		Set<Class<?>> wrapperClasses = cachedWrapperClasses;
		if (CollectionUtils.isNotEmpty(wrapperClasses)) {
			for (Class<?> wrapperClass : wrapperClasses) {
				// 同样进行注册其他扩展点的功能
				instance = injectExtension((T)
				wrapperClass.getConstructor(type).newInstance(instance));
			}
		}
		// 对扩展点进行初始化操作
		initExtension(instance);
		return instance;
	} catch (Throwable t) {
	throw new IllegalStateException("Extension instance (name: " + name + ",
	"class: " +type + ") couldn't be instantiated: " + t.getMessage(), t);
	}
}

(10)最终拿到指定名称的扩展点的实现类后的方法调用

private T injectExtension(T instance) {
	if (objectFactory null) {
		return instance;
	}
	try {
		// 遍历其中的所有方法
		for (Method method : instance.getClass().getMethods()) {
			// 是否是set方法
			// 1. 以"set"开头
			// 2. 参数长度为1
			// 3. 是公开的方法
			if (!isSetter(method)) {
				continue;
			}
			/**
			* Check {@link DisableInject} to see if we need auto injection for
				this property
			*/
			// 如果设置了取消注册,则不进行处理
			if (method.getAnnotation(DisableInject.class) != null) {
				continue;
			}
			// 获取参数类型,并且非基础类型(String, Integer等类型)
			Class<?> pt = method.getParameterTypes()[0];
			if (ReflectUtils.isPrimitives(pt)) {
				continue;
			}
			try {
				// 获取需要set的扩展点名称
				String property = getSetterProperty(method);
				// 从ExtensionLoader中加载指定的扩展点
				// 比如有一个方法为setRandom(LoadBalance loadBalance),那么则以为着需
				//要加载负载均衡中名为random的扩展点
				Object object = objectFactory.getExtension(pt, property);
				if (object != null) {
					method.invoke(instance, object);
				}
			} catch (Exception e) {
				logger.error("Failed to inject via method " + method.getName()
				+ " of interface " + type.getName() + ": " +e.getMessage(), e);
			}
		}
	} catch (Exception e) {
		logger.error(e.getMessage(), e);
	}
	return instance;
}

总结:
第一阶段:ExtensionLoader执行getExtensionLoader,实际是先获取ExtensionFactory对应类型的具体实现,主要有两类,AdaptiveExtensionFactory和SpiExtensionFactory,那到对应类型的扩展点工厂后就去获取支持对应类型的所有扩展点实现,并将这个扩展点的所有实现按照name和Class对象的形式进行存储,进而执行loadDirectory的方法按照路径利用loadResource加载文件并交由loadClass 来加载实现类信息。
第二阶段:利用getExtension根据name对扩展点进行处理和进行加锁来创建真实的引用,进而调用其对应的方法,完成功能的实现。

6. 服务暴露(注册)源码剖析

服务暴露(注册)流程:
开始服务提供者会进行初始化,将暴露给其他服务调用,如下图:
在这里插入图片描述
服务消费者也需要初始化,并且在注册中心注册自己,如下图:
在这里插入图片描述
服务提供者暴露的主要过程:
(1)将服务转化成 Invoker: 在服务提供者初始化的时候,会通过Config组件中的ServiceConfig 读取服务的配置信息,在读取配置文件生成服务实体以后,会通过ProxyFactory将Proxy转换成 Invoker。
(2)将 Invoker转化成Exporter: Invoker会被定义Protocol,然后会被包装成Exporter,最后Exporter会发送到注册中心,作为服务的注册信息。
现以dubbo-demo工程中的dubbo-demo-xml项目进行源码分析。
服务暴露源码剖析过程:
服务提供端执行org.apache.dubbo.demo.provider.Application启动后,服务提供者进行初始化,Dubbo是基于Spring的Schema进行扩展和加载的,如下:
在这里插入图片描述
这里应用了Spring的自定义标签功能,定义了dubbo标签,然后声明xsd的位置,我们进入xsd文件如下:
在这里插入图片描述
dubbo的xsd文件在dubbo-config-spring项目的META-INF下面,注意还有个spring.handlers,这个是用来解析文件标签的,进入到这个文件发现里面只有一个DubboNamespaceHandler,如下:
在这里插入图片描述
进入到这个方法如下:
在这里插入图片描述
首先看到DubboNamespaceHandler继承了类NamespaceHandlerSupport,对NamespaceHandlerSupport可以注册任意个BeanDefinitionParser,而解析XML的工作委托给各个BeanDefinitionParser负责。spring在扫描并加载BeanDefinition的时候会执行到这里,根据dubbo配置文件生成的BeanDefinition此刻交由spring管理。因为现在看的是服务端,所以这里我们主要看service对应的ServiceBean。
在这里插入图片描述
可以看到ServiceBean继承了ServiceConfig,如下:
在这里插入图片描述
在spring容器加载完成后触发contextrefreshedevent事件,这个事件会被实现了ApplicationListener接口的类监听到,执行对应的onApplicationEvent函数。这个方法里面核心只有这个export方法。接下来在ServiceConfig.java中的491行,可以看到ProxyFactory通过getInvoker方法获得了invoker实例,然后对invoker实例进行了包装,接下来将包装好的invoker实例转化成exporter实例。整个过程如下:

=>org.apache.dubbo.config.spring.context.DubboBootstrapApplicationListener#onContextRefreshedEvent
==> org.apache.dubbo.config.bootstrap.DubboBootstrap#start
==> org.apache.dubbo.config.bootstrap.DubboBootstrap#exportServices
==> org.apache.dubbo.config.ServiceConfig#export
==> org.apache.dubbo.config.ServiceConfig#doExport
==> org.apache.dubbo.config.ServiceConfig#doExportUrls
==> org.apache.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol
==> java.lang.String#equalsIgnoreCase(local)
==> org.apache.dubbo.rpc.ProxyFactory#getInvoker

==> org.apache.dubbo.rpc.Protocol#export
==> org.apache.dubbo.registry.integration.RegistryProtocol#export
==> org.apache.dubbo.registry.integration.RegistryProtocol#register
==> org.apache.dubbo.registry.RegistryFactory#getRegistry
==> org.apache.dubbo.registry.RegistryService#register
==> org.apache.dubbo.registry.ListenerRegistryWrapper#register
==> org.apache.dubbo.registry.support.FailbackRegistry#register
==> org.apache.dubbo.registry.support.FailbackRegistry#doRegister
==> org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doRegister
==> org.apache.dubbo.remoting.zookeeper.ZookeeperClient#create
==> java.util.List#add(E)
服务暴露(注册)源码级流程图:
在这里插入图片描述
总结:
第一阶段:service转化为Invoker,服务容器启动初始化过程中,会将service服务对应的ServiceBean信息初始化加载,Dubbo的启动器监听会启动服务容器,其中一项工作就是暴露服务,ServiceConfig组件会执行export方法暴露服务,这里会判断是本地暴露还是远程暴露,然后ProxyFactory接口的实现类会调用getInvoker方法使用 ref(对外提供服务的实际类) 生成一个AbstractProxyInvoker实例,并对其进行包装得到wrapperInvoker;
第二阶段:Invoker转化为Exporter,注册中心协议管理类执行export方法对包装的Invoker进行暴露,若是远程暴露,先通过注册中心工厂类RegistryFactory获得对应协议的注册类Register,进而调用其register方法进行注册,若使用Zookeeper注册中心,注册过程将由ZookeeperRegistry执行doRegister方法完成,即创建Zookeeper数据节点保存注册服务的信息,最终将需要暴露的Exporter加入到列表中,整个暴露过程完成。

7. 服务引用(消费)源码剖析

服务消费流程:服务消费者首先持有远程服务实例生成的 Invoker,然后把 Invoker 转换成用户接口的动态代理引用。
在这里插入图片描述
服务消费者消费初始化过程:
(1)把远端服务转化为Invoker: ReferenceConfig 类的init方法调用createProxy() ,期间使用 Protocol调用refer方法生成Invoker 实例;
(2)把Invoker转化为客户端需要的接口: 即使用ProxyFactory把Invoker转换为客户端需要的接口(如HelloService)
服务消费源码剖析过程:
服务消费端执行org.apache.dubbo.demo.consumer.Application启动后,服务消费者进行初始化,Dubbo是基于Spring的Schema进行扩展和加载的,如下:
在这里插入图片描述
这里应用了Spring的自定义标签功能,定义了dubbo标签,然后声明xsd的位置,我们进入xsd文件如下:
在这里插入图片描述
dubbo的xsd文件在dubbo-config-spring项目的META-INF下面,注意还有个spring.handlers,这个是用来解析文件标签的,进入到这个文件发现里面只有一个DubboNamespaceHandler,如下:
在这里插入图片描述
进入到这个方法如下:
在这里插入图片描述
首先看到DubboNamespaceHandler继承了类NamespaceHandlerSupport,对NamespaceHandlerSupport可以注册任意个BeanDefinitionParser,而解析XML的工作委托给各个BeanDefinitionParser负责。spring在扫描并加载BeanDefinition的时候会执行到这里,根据dubbo配置文件生成的BeanDefinition此刻交由spring管理。因为现在看的是服务端,所以这里我们主要看reference对应的ReferenceBean。
在这里插入图片描述
可以看到ReferenceBean继承了ReferenceConfig,如下:
在这里插入图片描述
在spring容器加载完成后触发contextrefreshedevent事件,这个事件会被实现了ApplicationListener接口的类监听到,执行对应的onApplicationEvent函数。接下来执行ReferenceConfig.java中225行的init方法,整个过程如下:
==> org.apache.dubbo.config.ReferenceConfig#init
==> org.apache.dubbo.config.ReferenceConfig#createProxy
==> org.apache.dubbo.config.ReferenceConfig#shouldJvmRefer(本地引用)
==> org.apache.dubbo.rpc.Protocol#refer(远程引用)
==> org.apache.dubbo.registry.integration.RegistryProtocol#refer
==> org.apache.dubbo.registry.integration.RegistryProtocol#doRefer
==> org.apache.dubbo.rpc.cluster.Cluster#getCluster(java.lang.String, boolean)

==> org.apache.dubbo.rpc.ProxyFactory#getProxy(org.apache.dubbo.rpc.Invoker, boolean)
服务消费源码级流程图:
在这里插入图片描述
总结:
第一阶段:reference转化为Invoker,服务消费容器启动初始化过程中,会将reference服务对应的ReferenceBean信息初始化加载,进而调用ReferenceConfig 类的init方法中的createProxy() 方法,这里会判断是本地引用还是远程引用,对于远程引用,如果url只存在一个,那么直接用Protocol的refer()进行转换,如果存在多个url,会先通过urls获取所有invoker,然后根据urls中是否存在registry协议的url,做不同的集群调用,最终获得消费者需要的Invoker实例;
第二阶段:Invoker转化为interface,上面已经得到Invoker实例,ProxyFactory会通过getProxy方法将Invoker进行转化,实际是交由其实现类AbstractProxyFactory的getProxy方法进行处理,然后由其子类JdkProxyFactory的getProxy方法执行,到此就创建出接口的动态代理对象,然后用InvokerInvocationHandler调用invoke方法执行,最后用recreate方法用来将result转换为接口实际需要的类型返回后将代理放到spring容器中,这样用起来就像本地调用一样,依赖注入的时候主动进行初始化,整个服务消费的初始化完成。

8. 集群容错、负载均衡、路由服务源码剖析

智能容错、负载均衡及路由服务执行流程,如下图:
在这里插入图片描述
集群容错的所有组件,包含集群容错组件Cluster、集群容错调用者组件Cluster Invoker、信息缓存组件Directory、路由服务组件Router 和 负载均衡组件LoadBalance 等
智能容错、负载均衡及路由服务执行流程核心过程:
(1)缓存服务信息: Cluster组件调用信息缓存接口Directory缓存可调用的服务列表;
(2)用路由规则过滤服务信息: 信息缓存接口Directory调用路由组件Router根据路由规则进行过滤;
(3)服务调用进行负载均衡: 通过负载均衡组件LoadBalance对过滤后的服务进行选择负载调用。
源码剖析过程:
FailoverClusterInvoker的doInvoke重试机制和负载均衡源码如下:==>org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker#doInvoke
在FailoverClusterInvoker的doInvoke方法中,首先checkInvokers进行检查服务是否可用情况,然后获取重试次数,如果重试次数配置不合理就给个默认值1,然后RpcException le为成功调用服务后的最后一次调用失败的异常对象,如果没有调用失败的情况,则该对象一直为null,然后for循环遍历len次进行重试调用,在调用的过程中,重试时,进行重新选择,避免重试时invoker列表已发生变化,如果列表发生了变化,那么invoked判断会失效,因为invoker实例已经改变。
然后select执行的是负载均衡的逻辑,选出一个invoker,调用目标 Invoker 的 invoke 方法,如果调用成功,直接返回result,反之,如果捕获到异常,如果是业务逻辑层异常,捕获到即退出重试,向上抛出返回给客户端,如果不是业务逻辑层异常,就赋值给len。最终invoker放进providers中,开始下一次重试。如果最终len次重试后均调用失败,则直接抛出异常信息。

public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> {

    private static final Logger logger = LoggerFactory.getLogger(FailoverClusterInvoker.class);

    public FailoverClusterInvoker(Directory<T> directory) {
        super(directory);
    }

    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        List<Invoker<T>> copyInvokers = invokers;
        checkInvokers(copyInvokers, invocation);
        String methodName = RpcUtils.getMethodName(invocation);
        //获取重试次数,默认重试次数DEFAULT_RETRIES为2
        int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
        if (len <= 0) {
            len = 1;
        }

        //循环调用,失败重试

        //成功调用之后的最后一次失败调用的异常
        RpcException le = null; // last exception.
        //被调用的调用者
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.
        Set<String> providers = new HashSet<String>(len);
        for (int i = 0; i < len; i++) {
            //重试时,进行重新选择,避免重试时invoker列表已发生变化.
            //注意:如果列表发生了变化,那么invoked判断会失效,因为invoker实例已经改变
            if (i > 0) {
                checkWhetherDestroyed();
                //在进行重试前重新列举 Invoker,这样做的好处是,如果某个服务挂了,
                //通过调用 list 可得到最新可用的 Invoker 列表
                copyInvokers = list(invocation);
                checkInvokers(copyInvokers, invocation);
            }
            //开始执行负载均衡逻辑
            Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
            //添加到 invoker 到 invoked 列表中
            invoked.add(invoker);
            //设置 invoked 到 RPC 上下文中
            RpcContext.getContext().setInvokers((List) invoked);
            try {
                //调用目标 Invoker 的 invoke 方法
                Result result = invoker.invoke(invocation);
                if (le != null && logger.isWarnEnabled()) {
                    logger.warn("Although retry the method " + methodName
                            + " in the service " + getInterface().getName()
                            + " was successful by the provider " + invoker.getUrl().getAddress()
                            + ", but there have been failed providers " + providers
                            + " (" + providers.size() + "/" + copyInvokers.size()
                            + ") from the registry " + directory.getUrl().getAddress()
                            + " on the consumer " + NetUtils.getLocalHost()
                            + " using the dubbo version " + Version.getVersion() + ". Last error is: "
                            + le.getMessage(), le);
                }
                return result;
            } catch (RpcException e) {
                //如果是业务逻辑层异常,捕获到即退出重试
                if (e.isBiz()) { // biz exception.
                    throw e;
                }
                le = e;
            } catch (Throwable e) {
                le = new RpcException(e.getMessage(), e);
            } finally {
                providers.add(invoker.getUrl().getAddress());
            }
        }
        //若重试失败,则抛出异常
        throw new RpcException(le.getCode(), "Failed to invoke the method "
                + methodName + " in the service " + getInterface().getName()
                + ". Tried " + len + " times of the providers " + providers
                + " (" + providers.size() + "/" + copyInvokers.size()
                + ") from the registry " + directory.getUrl().getAddress()
                + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
                + Version.getVersion() + ". Last error is: "
                + le.getMessage(), le.getCause() != null ? le.getCause() : le);
    }

}

负载均衡是如何进行select的源码如下:
==>org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker#select
主要包括粘滞连接后的检测和LB进行doselect,获取sticky配置,sticky表示粘滞连接(所谓粘滞连接是指让服务消费者尽可能的调用同一个服务提供者,除非该提供者挂了再进行切换)。检测 invokers列表是否包含stickyInvoker,如果不包含,说明 stickyInvoker 代表的服务提供者挂了,此时需要将其置空。 在sticky为true,且stickyInvoker != null的情况下。如果selected包含stickyInvoker,表明stickyInvoker对应的服务提供者可能因网络原因未能成功提供服务。但是该提供者并没挂,此时 invokers列表中仍存在该服务提供者对应的 Invoker。如果此时能够通过服务检查的话说明就可以继续提供服务。

protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation,
                            List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {

    if (CollectionUtils.isEmpty(invokers)) {
        return null;
    }
    //获取调用方法名
    String methodName = invocation == null ? StringUtils.EMPTY_STRING : invocation.getMethodName();
    //=========================== 粘滞连接特性 ===========================
    //获取 sticky 配置,sticky 表示粘滞连接。
    // 所谓粘滞连接是指让服务消费者尽可能的调用同一个服务提供者,除非该提供者挂了再进行切换
    boolean sticky = invokers.get(0).getUrl()
            .getMethodParameter(methodName, CLUSTER_STICKY_KEY, DEFAULT_CLUSTER_STICKY);

    //检测 invokers 列表是否包含 stickyInvoker,如果不包含,
    //说明 stickyInvoker 代表的服务提供者挂了,此时需要将其置空
    if (stickyInvoker != null && !invokers.contains(stickyInvoker)) {
        stickyInvoker = null;
    }
    //在 sticky 为 true,且 stickyInvoker != null 的情况下。如果 selected 包含
    //stickyInvoker,表明 stickyInvoker 对应的服务提供者可能因网络原因未能成功提供服务。
    //但是该提供者并没挂,此时 invokers 列表中仍存在该服务提供者对应的 Invoker。
    if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))) {
        if (availablecheck && stickyInvoker.isAvailable()) {
            return stickyInvoker;
        }
    }
    //=========================== LB选择 Invoker ===========================
    //如果线程走到当前代码处,说明前面的 stickyInvoker 为空,或者不可用。
    //此时继续调用 doSelect 选择 Invoker
    Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);
    //如果 sticky 为 true,则将负载均衡组件选出的 Invoker 赋值给 stickyInvoker
    if (sticky) {
        stickyInvoker = invoker;
    }
    return invoker;
}

负载均衡是如何进行doSelect的源码如下:
==>org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker#doSelect
doselect方法首先判断如果invokers只有一个invoker,那么直接返回。然后通过负载均衡选择 Invoker,如果selected 包含负载均衡选择出的Invoker,或者该Invoker无法经过可用性检查,此时进行重选,获得rinvoker,如果rinvoker不为空,则将其赋值给rinvoker,否则,rinvoker为空,定位 invoker 在invokers中的位置,获取 index + 1位置处的Invoker,最终返回invoker。

private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation,
                            List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {

    if (CollectionUtils.isEmpty(invokers)) {
        return null;
    }
    if (invokers.size() == 1) {
        return invokers.get(0);
    }
    //通过负载均衡组件选择 Invoker
    Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);

    //如果 selected 包含负载均衡选择出的 Invoker,或者该 Invoker 无法经过可用性检查,此时进行重选
    if ((selected != null && selected.contains(invoker))
            || (!invoker.isAvailable() && getUrl() != null && availablecheck)) {
        try {
            //通过负载均衡重新选择 Invoker
            Invoker<T> rInvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck);
            //如果 rinvoker 不为空,则将其赋值给 invoker
            if (rInvoker != null) {
                invoker = rInvoker;
            } else {
                //rinvoker 为空,定位 invoker 在 invokers 中的位置
                int index = invokers.indexOf(invoker);
                try {
                    // 避免碰撞
                    //获取 index + 1 位置处的 Invoker,以下代码等价于:
                    // invoker = invokers.get((index + 1) % invokers.size());
                    invoker = invokers.get((index + 1) % invokers.size());
                } catch (Exception e) {
                    logger.warn(e.getMessage() + " may because invokers list dynamic change, ignore.", e);
                }
            }
        } catch (Throwable t) {
            logger.error("cluster reselect fail reason is :" + t.getMessage() + " if can not solve, you can set cluster.availablecheck=false in url", t);
        }
    }
    return invoker;
}

通过负载均衡重新选择 Invoker的reselect方法源码如下:==>org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker#reselect
reselect方法中,首先进行available校验,查找可用的 Invoker,并将其添加到 reselectInvokers 集合中,然后根据reselectInvokers进行LB的select。然后不进行available校验,查找可用的 Invoker,并将其添加到 reselectInvokers 集合中,然后根据reselectInvokers进行LB的select。最后如果reselectInvokers为空,这个时候就从可用的select中选择invoker。

private Invoker<T> reselect(LoadBalance loadbalance, Invocation invocation,
                            List<Invoker<T>> invokers, List<Invoker<T>> selected, boolean availablecheck) throws RpcException {

    //如果有可能预先分配一个,肯定会使用此list
    List<Invoker<T>> reselectInvokers = new ArrayList<>(
            invokers.size() > 1 ? (invokers.size() - 1) : invokers.size());

    // First, try picking a invoker not in `selected`.
    //遍历 invokers 列表
    for (Invoker<T> invoker : invokers) {
        //根据 availablecheck 进行不同的处理并检测可用性
        if (availablecheck && !invoker.isAvailable()) {
            continue;
        }
        // 如果 selected 列表不包含当前 invoker,则将其添加到 reselectInvokers 中
        if (selected == null || !selected.contains(invoker)) {
            reselectInvokers.add(invoker);
        }
    }
    //reselectInvokers 不为空,此时通过负载均衡组件进行选择
    if (!reselectInvokers.isEmpty()) {
        return loadbalance.select(reselectInvokers, getUrl(), invocation);
    }

    // 只需使用负载均衡策略选择一个可用的调用程序
    // 若线程走到此处,说明 reselectInvokers 集合为空,此时不会调用负载均衡组件进行筛选。
    // 这里从 selected 列表中查找可用的 Invoker,并将其添加到 reselectInvokers 集合中
    if (selected != null) {
        for (Invoker<T> invoker : selected) {
            if ((invoker.isAvailable()) // available first
                    && !reselectInvokers.contains(invoker)) {
                reselectInvokers.add(invoker);
            }
        }
    }
    if (!reselectInvokers.isEmpty()) {
        return loadbalance.select(reselectInvokers, getUrl(), invocation);
    }

    return null;
}

总结:
第一个阶段:服务消费者初始化
即集群Cluster实现类为服务消费者创建ClusterInvoker实例对象,然后通过ClusertInvoker中的Invoker方法启动调用流程;
第二个阶段:服务消费者进行远程调用
以FailoverClusterInvoker为例,该类型ClusterInvoker 首先会调用Directory的list方法获取可调用的服务列表,Directory的用途是保存Invoker列表,可简单类比为List。其实现类RegistryDirectory是一个动态服务目录,可感知注册中心配置的变化,它所持有的Invoker列表会随着注册中心内容的变化而变化。每次变化后,RegistryDirectory会动态增删Invoker,并调用Router的route方法进行路由,过滤掉不符合路由规则的Invoker。当FailoverClusterInvoker拿到Directory返回的Invoker列表后,它会通过LoadBalance从Invoker列表中选择一个Invoker。最后FailoverClusterInvoker会将参数传给LoadBalance选择出的 Invoker实例的invoke方法,进行真正的远程调用。

9. 远程方法调用源码剖析

服务消费者(客户端)对服务提供者(服务端)的方法调用流程,如下图:
在这里插入图片描述
由Dubbo远程调用流程可知,服务消费者经过容错,Invoker 列表,路由和负载均衡以后,会对 Invoker 进行过滤,之后通过 Client 编码,序列化发给服务提供者。
远程方法调用的核心过程:
(1)过滤 Invoker
(2)Invoker发送请求
(3)Client 编码: Dubbo的编码器将打包好的协议对象进行编码,其过程是将信息传化成字节流;
(4)Serialize序列化发送给服务提供者: 当服务提供者收到请求协议包以后,先将其放到 ThreadPool 中,然后依次处理。
接下来,我主要分析Dubbo协议编码请求对象的过程:
Dubbo协议编码请求对象分为使用 ExchangeCodec 中的两个方法,encodeRequest 负责编码协议头和 encodeRequestData 编码协议体。同样通过 encodeResponse 编码协议头,encodeResponseData 编码协议体。
(1)数据协议类ExchangeCodec中常量定义

public class ExchangeCodec extends TelnetCodec {

    // 消息头的长度
    protected static final int HEADER_LENGTH = 16;
    // 标示为0-15位
    protected static final short MAGIC = (short) 0xdabb;
    protected static final byte MAGIC_HIGH = Bytes.short2bytes(MAGIC)[0];
    protected static final byte MAGIC_LOW = Bytes.short2bytes(MAGIC)[1];
    // 消息头中的内容
    protected static final byte FLAG_REQUEST = (byte) 0x80;
    protected static final byte FLAG_TWOWAY = (byte) 0x40;
    protected static final byte FLAG_EVENT = (byte) 0x20;
    protected static final int SERIALIZATION_MASK = 0x1f;
    private static final Logger logger = LoggerFactory.getLogger(ExchangeCodec.class);
    。。。
}

(2)这个类中 encode 和 decode 分别用于将数据发送到 ByteBuffer 中,将其反向的转换为对象

public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws
IOException {
	// 处理请求对象
	if (msg instanceof Request) {
		encodeRequest(channel, buffer, (Request) msg);
	} else if (msg instanceof Response) {
		// 处理响应
		encodeResponse(channel, buffer, (Response) msg);
	} else {
		// 其他的交给上级处理,用于telnet模式
		super.encode(channel, buffer, msg);
	}
}

进入encodeRequest方法:

protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req)
throws IOException {
	// 请求的序列化类型
	Serialization serialization = getSerialization(channel);
	// 写入header信息
	byte[] header = new byte[HEADER_LENGTH];
	// 模数0-15位
	Bytes.short2bytes(MAGIC, header);
	// 标记为请求
	header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
    // 是否是单向还是双向的(异步)
	if (req.isTwoWay()) {
		header[2] |= FLAG_TWOWAY;
	}
	// 是否为事件(心跳)
	if (req.isEvent()) {
		header[2] |= FLAG_EVENT;
	}
	// 写入当前的请求ID
	Bytes.long2bytes(req.getId(), header, 4);
	// 保存当前写入的位置,将其写入的位置往后面偏移,保留出写入内容大小的位置,先进行写入body内容
	int savedWriteIndex = buffer.writerIndex();
	buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
	ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
	ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
	// 按照数据内容的不同,来写入不同的内容
	if (req.isEvent()) {
		encodeEventData(channel, out, req.getData());
	} else {
		encodeRequestData(channel, out, req.getData(), req.getVersion());
	}
	out.flushBuffer();
	if (out instanceof Cleanable) {
		((Cleanable) out).cleanup();
	}
	bos.flush();
	bos.close();
	// 记录body中写入的长度
	int len = bos.writtenBytes();
	checkPayload(channel, len);
	// 将其写入到header中的位置中
	Bytes.int2bytes(len, header, 12);
	// 发送到buffer中
	buffer.writerIndex(savedWriteIndex);
	buffer.writeBytes(header); // write header.
	buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}

进入encodeRequestData方法:

protected void encodeRequestData(Channel channel, ObjectOutput out, Object data,
String version) throws IOException {
	RpcInvocation inv = (RpcInvocation) data;
	// 写入版本
	out.writeUTF(version);
	// 接口全名称
	out.writeUTF(inv.getAttachment(PATH_KEY));
	// 接口版本号
	out.writeUTF(inv.getAttachment(VERSION_KEY));
	// 写入方法名称
    out.writeUTF(inv.getMethodName());
	// 调用参数描述信息
	out.writeUTF(inv.getParameterTypesDesc());
	// 所有的请求参数写入
	Object[] args = inv.getArguments();
	if (args != null) {
		for (int i = 0; i < args.length; i++) {
			out.writeObject(encodeInvocationArgument(channel, inv, i));
		}
	}
	// 写入所有的附加信息
	out.writeAttachments(inv.getAttachments());
}

进入encodeResponse方法:

protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response
res) throws IOException {
	int savedWriteIndex = buffer.writerIndex();
	try {
		Serialization serialization = getSerialization(channel);
		// 和之前的参数一致
		byte[] header = new byte[HEADER_LENGTH];
		Bytes.short2bytes(MAGIC, header);
		header[2] = serialization.getContentTypeId();
		if (res.isHeartbeat()) {
			header[2] |= FLAG_EVENT;
		}
		// 写入状态码
		byte status = res.getStatus();
		header[3] = status;
		// 写入内容
		Bytes.long2bytes(res.getId(), header, 4);
		// 和Request一样的内容写入方式,先写入内容,再写入长度
		buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
		ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
		ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
		// encode response data or error message.
		if (status Response.OK) {
			if (res.isHeartbeat()) {
				encodeEventData(channel, out, res.getResult());
			} else {
				encodeResponseData(channel, out, res.getResult(),
				res.getVersion());
			}
		} else {
			// 这里不太一样的地方在于,如果错误的时候,则直接将错误信息写入,不需要再交由序列化
			out.writeUTF(res.getErrorMessage());
		}
		out.flushBuffer();
		if (out instanceof Cleanable) {
			((Cleanable) out).cleanup();
         }
		bos.flush();
		bos.close();
		// 一样的写入模式
		int len = bos.writtenBytes();
		checkPayload(channel, len);
		Bytes.int2bytes(len, header, 12);
		buffer.writerIndex(savedWriteIndex);
		buffer.writeBytes(header); // write header.
		buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
	} catch (Throwable t) {
		// 写入出现异常
		buffer.writerIndex(savedWriteIndex);
		// send error message to Consumer, otherwise, Consumer will wait till timeout.
		if (!res.isEvent() && res.getStatus() != Response.BAD_RESPONSE) {
			Response r = new Response(res.getId(), res.getVersion());
			r.setStatus(Response.BAD_RESPONSE);
			// 如果是超过内容长度则重新设置内容大小并写入
			if (t instanceof ExceedPayloadLimitException) {
				logger.warn(t.getMessage(), t);
				try {
					r.setErrorMessage(t.getMessage());
					channel.send(r);
					return;
				} catch (RemotingException e) {
					logger.warn("Failed to send bad_response info back: " +
					t.getMessage() + ", cause: " + e.getMessage(), e);
				}
			}
		}
		// 其他的则抛出异常,省去代码
	}
}

进入encodeResponseData方法:

protected void encodeResponseData(Channel channel, ObjectOutput out, Object
data, String version) throws IOException {
	Result result = (Result) data;
	// 是否支持返回attachment参数
	boolean attach = Version.isSupportResponseAttachment(version);
	Throwable th = result.getException();
	if (th == null) {
		// 如果没有异常信息,则直接写入内容
		Object ret = result.getValue();
		if (ret == null) {
			out.writeByte(attach ? RESPONSE_NULL_VALUE_WITH_ATTACHMENTS :
			RESPONSE_NULL_VALUE);
		} else {
			out.writeByte(attach ? RESPONSE_VALUE_WITH_ATTACHMENTS :RESPONSE_VALUE);
			out.writeObject(ret);
		}
    } else {
		// 否则的话则将异常信息序列化
		out.writeByte(attach ? RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS :
		RESPONSE_WITH_EXCEPTION);
		out.writeThrowable(th);
	}
	// 支持写入attachment,则写入
	if (attach) {
		// returns current version of Response to consumer side.
		result.getAttachments().put(DUBBO_VERSION_KEY,
		Version.getProtocolVersion());
		out.writeAttachments(result.getAttachments());
	}
}

(3)解码器decode的源码如下:

@Override
public Object decode(Channel channel, ChannelBuffer buffer) throws
IOException {
	// 可读字节数
	int readable = buffer.readableBytes();
	// 选取可读字节数 和 HEADER_LENGTH 中小的
	byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
	buffer.readBytes(header);
	return decode(channel, buffer, readable, header);
}
@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable,
byte[] header) throws IOException {
	// 检查魔数
	if (readable > 0 && header[0] != MAGIC_HIGH
	|| readable > 1 && header[1] != MAGIC_LOW) {
		int length = header.length;
		if (header.length < readable) {
			header = Bytes.copyOf(header, readable);
			buffer.readBytes(header, length, readable - length);
		}
		for (int i = 1; i < header.length - 1; i++) {
			if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
				buffer.readerIndex(buffer.readerIndex() - header.length +i);
				header = Bytes.copyOf(header, i);
				break;
			}
		}
		return super.decode(channel, buffer, readable, header);
	}
	// check length. 不完整的包 需要继续读取
	if (readable < HEADER_LENGTH) {
		return DecodeResult.NEED_MORE_INPUT;
	}
	// 获取数据长度
	int len = Bytes.bytes2int(header, 12);
    checkPayload(channel, len);
	int tt = len + HEADER_LENGTH;
	// 需要继续读取
	if (readable < tt) {
		return DecodeResult.NEED_MORE_INPUT;
	}
	// limit input stream.
	ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
	try {
		// 解码数据
		return decodeBody(channel, is, header);
	} finally {
		if (is.available() > 0) {
			try {
				if (logger.isWarnEnabled()) {
					logger.warn("Skip input stream " + is.available());
				}
				StreamUtils.skipUnusedStream(is);
			} catch (IOException e) {
				logger.warn(e.getMessage(), e);
			}
		}
	}
}

进入decodeBody方法,解析响应中的信息处理:

protected Object decodeBody(Channel channel, InputStream is, byte[] header)
throws IOException {
	byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
	// 获取请求ID
	long id = Bytes.bytes2long(header, 4);
	// 判断是请求还是响应
	if ((flag & FLAG_REQUEST) == 0) {
		// 说明是响应
		Response res = new Response(id);
		// 是否是event事件
		if ((flag & FLAG_EVENT) != 0) {
			res.setEvent(true);
		}
		// 获取请求的状态码
		byte status = header[3];
		res.setStatus(status);
		try {
			// 进行数据内容解析
			ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is,proto);
			if (status == Response.OK) {
				Object data;
				// 根据不同的类型来进行解析
				if (res.isHeartbeat()) {
					data = decodeHeartbeatData(channel, in);
				} else if (res.isEvent()) {
					data = decodeEventData(channel, in);
    			} else {
					data = decodeResponseData(channel, in, getRequestData(id));
				}
				res.setResult(data);
			} else {
				res.setErrorMessage(in.readUTF());
			}
		} catch (Throwable t) {
			res.setStatus(Response.CLIENT_ERROR);
			res.setErrorMessage(StringUtils.toString(t));
		}
		return res;
	} else {
		// 解析为请求
		Request req = new Request(id);
		req.setVersion(Version.getProtocolVersion());
		req.setTwoWay((flag & FLAG_TWOWAY) != 0);
		if ((flag & FLAG_EVENT) != 0) {
			req.setEvent(true);
		}
		try {
			// 与响应相同,进行内容解析
			ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is,proto);
			Object data;
			if (req.isHeartbeat()) {
				data = decodeHeartbeatData(channel, in);
			} else if (req.isEvent()) {
				data = decodeEventData(channel, in);
			} else {
				data = decodeRequestData(channel, in);
			}
			req.setData(data);
		} catch (Throwable t) {
			// bad request
			req.setBroken(true);
			req.setData(t);
		}
		return req;
	}
}

10. Dubbo服务调用过程超时实现原理源码剖析

Dubbo中可在两个地方配置超时时间:分别是provider(服务端,服务提供方)和consumer(消费端,服务调用方)。服务端的超时配置是消费端的缺省配置,也就是说只要服务端设置了超时时间,则所有消费端都无需设置,可通过注册中心传递给消费端。
Dubbo支持非常细粒度的超时设置,包括:方法级别、接口级别和全局。如果各个级别同时配置了,优先级为:消费端方法级 > 服务端方法级 > 消费端接口级 > 服务端接口级 > 消费端全局 > 服务端全局。
具体遵循:
(1)精确优先(方法级>接口级>全局配置(消费者))
(2)消费者设置优先(在生产者和消费者中同时设置超时时间(防止由于网络等原因造成的堵塞),消费者设置的超时时间生效)
(1)服务端的超时处理逻辑源码

public class TimeoutFilter implements Filter {
  
     public TimeoutFilter() {
	 }

    public Result invoke(...) throws RpcException {
        // 执行真正的逻辑调用,并统计耗时
        long start = System.currentTimeMillis();
        Result result = invoker.invoke(invocation);
        long elapsed = System.currentTimeMillis() - start;

        // 判断是否超时
        if (invoker.getUrl() != null && elapsed > timeout) {
            // 打印warn日志
            logger.warn("invoke time out...");
        }

        return result;
    }
}

可以看到,服务端即使超时,也只是打印了一个warn日志。因此,服务端的超时设置并不会影响实际的调用过程,就算超时也会执行完整个处理逻辑。
(2)消费端的超时处理逻辑

public class FailoverClusterInvoker {

    public Result doInvoke(...)  {
        ...
        // 循环调用设定的重试次数
        for (int i = 0; i < retryTimes; ++i) {
            ...
            try {
                Result result = invoker.invoke(invocation);
                return result;
            } catch (RpcException e) {
                // 如果是业务异常,终止重试
                if (e.isBiz()) {
                    throw e;
                }

                le = e;
            } catch (Throwable e) {
                le = new RpcException(...);
            } finally {
                ...
            }
        }

        throw new RpcException("...");
    }
}

FailoverCluster是集群容错的缺省模式,当调用失败后会切换成调用其他服务器。再看下doInvoke方法,当调用失败时,会先判断是否是业务异常,如果是则终止重试,否则会一直重试直到达到重试次数。
继续跟踪invoker的invoke方法,可以看到在请求发出后通过Future的get方法获取结果,源码如下:

public Object get(int timeout) {
        if (timeout <= 0) {
            timeout = 1000;
        }

        if (!isDone()) {
            long start = System.currentTimeMillis();
            this.lock.lock();

            try {
                // 循环判断
                while(!isDone()) {
                    // 放弃锁,进入等待状态
                    done.await((long)timeout, TimeUnit.MILLISECONDS);

                    // 判断是否已经返回结果或者已经超时
                    long elapsed = System.currentTimeMillis() - start;
                    if (isDone() || elapsed > (long)timeout) {
                        break;
                    }
                }
            } catch (InterruptedException var8) {
                throw new RuntimeException(var8);
            } finally {
                this.lock.unlock();
            }

            if (!isDone()) {
                // 如果未返回结果,则抛出超时异常
                throw new TimeoutException(...);
            }
        }

        return returnFromResponse();
  }

进入方法后开始计时,如果在设定的超时时间内没有获得返回结果,则抛出TimeoutException。因此,消费端的超时逻辑同时受到超时时间超时次数两个参数的控制,像网络异常、响应超时等都会一直重试,直到达到重试次数。
总结:
从上可以看出Dubbo框架的超时实现原理受到超时时间超时次数两个参数的控制,也引入了超时重试机制。超时机制的实现确保了服务链路的稳定性,提供了一种框架级的容错能力。比如整个调用链路经过了A、B、C、D多个服务,只要D的性能变差,就会自下而上影响到A、B、C,最终造成整个链路超时甚至瘫痪,因此设置超时时间是非常有必要的;又比如因为某个瞬间的网络抖动或者机器高负载引起的超时,如果超时后直接放弃,某些场景会造成业务损失,因此,对于这种临时性的服务抖动,如果在超时后重试一下是可以挽救的,因此设置超时次数进行重试也是非常必要的。

11. Dubbo服务调用过程异步调用源码剖析

Dubbo的四种调用方式:
(1)oneway: 单向调用,指的是客户端发送消息后,不需要接受响应,所以客户端不需要阻塞等待。对于那些不关心服务端响应的请求,比较适合使用oneway通信。但注意,返回值定义为void的并不是oneway的调用方式,void表示的程序上不需要关心返回值,但是对Dubbo框架而言,还是需要构建返回数据的。
(2)sync: 同步调用,指客服端发起请求到服务端,然后在设置的超时时间内,一直等待服务器的响应,这个时候,客户端处于阻塞的状态,当服务器返回响应后,客户端才会继续运行。即服务消费端发起一个远程调用后,调用线程要被阻塞挂起,直到服务提供方返回。sync是最常用的通信方式,也是Dubbo默认的通信方式。
(3)future和callback: 异步调用,即服务消费方发起一个远程调用后,不等服务提供方返回结果,调用方法就返回了,也就是当前线程不会被阻塞,这就允许调用方同时调用多个远程方法。在接收响应时,future.get() 会导致线程的阻塞,future从RpcContext中获取;callback 通常会设置一个回调线程,当接收到服务端响应时,自动执行,不会对当前线程造成阻塞。
Dubbo的异步调用是基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
异步调用过程如下:
(1)在消费端发送请求的过程中,会执行DubboInvoker中的doInvoke方法,在此方法中异步调用逻辑有变动,与2.7.x之前的版本逻辑区别在于,之前直接在此方法中根据配置区分单向调用、同步、异步。现在只做了单向调用和需要返回结果的区分,统一先使用AsyncRpcResult来表示结果,即都认为是异步调用,然后调用回到AsyncToSyncInvoker(异步转同步类)的invoke方法中,才对同步异步调用做区分,这里新增了InvokeMode,从源码中可以看出InvokeMode现在有三种模式:FUTURE、ASYNC、SYNC,FUTURE模式表示调用的返回类型是Future类型,代表调用的方法的返回类型是CompletableFuture类型,这种模式专门用来支持服务端异步调用的。
接下来,看下Dubbo相关调用的源码:
==>org.apache.dubbo.rpc.protocol.dubbo.DubboInvoker#doInvoke

protected Result doInvoke(final Invocation invocation) throws Throwable {
    RpcInvocation inv = (RpcInvocation) invocation;
    final String methodName = RpcUtils.getMethodName(invocation);
    inv.setAttachment(PATH_KEY, getUrl().getPath());
    inv.setAttachment(VERSION_KEY, version);

    ExchangeClient currentClient;
    if (clients.length == 1) {
        currentClient = clients[0];
    } else {
        currentClient = clients[index.getAndIncrement() % clients.length];
    }
    try {
        boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
        int timeout = calculateTimeout(invocation, methodName);
        //判断是否是无响应式调用
        if (isOneway) {
            boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
            currentClient.send(inv, isSent);
            return AsyncRpcResult.newDefaultAsyncResult(invocation);
        } else {
            //剩下的三种调用:sync调用,future调用,callback调用在这里?
            ExecutorService executor = getCallbackExecutor(getUrl(), inv);
            //CompletableFuture支持同步获得结果,也支持异步获得结果
            CompletableFuture<AppResponse> appResponseFuture =
                    currentClient.request(inv, timeout, executor).thenApply(obj -> (AppResponse) obj);
            // save for 2.6.x compatibility, for example, TraceFilter in Zipkin uses com.alibaba.xxx.FutureAdapter
            FutureContext.getContext().setCompatibleFuture(appResponseFuture);
            //异步调用结果
            AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);
            result.setExecutor(executor);
            return result;
        }
    } catch (TimeoutException e) {
        throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
    } catch (RemotingException e) {
        throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}
public static InvokeMode getInvokeMode(URL url, Invocation inv) {
    if (isReturnTypeFuture(inv)) {
        //FUTURE模式
        return InvokeMode.FUTURE;
    } else if (isAsync(url, inv)) {
        //ASYNC模式
        return InvokeMode.ASYNC;
    } else {
        //SYNC模式
        return InvokeMode.SYNC;
    }
}

(2)CompletableFuture实现了Future和CompletionStage接口,所以它可以向以前一样通过阻塞或者轮询的方式获得结果,这一点就保证了阻塞式获得结果,也就是同步调用;CompletableFuture在回调方面也提供了良好的回调方法来异步获得结果。

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
	...
}

源码剖析后的总结:
源码剖析不仅涉及到很多技术层面的问题(比如接口幂等、服务降级和熔断、性能评估和优化),同时还需要从业务角度评估必要性。当然Dubbo源码里面还有很多可以学习的地方,上面的源码剖析只是粗略从整体流程上进行了略读,通过源码剖析希望能知其然知其所以然,也希望这些知识能让你在开发RPC接口时,有更全局的视野。

七、Dubbo中的设计模式

三类23种设计模式:
(1)创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。5种创建型模式:单例、原型、工厂方法、抽象工厂、建造者。
(2)结构型模式:用于描述如何将类或对象按某种布局组成更大的结构。7种结构型模式:代理、适配器、桥接、装饰、外观、享元、组合。
(3)行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。11种行为型模式:模板方法、策略、命令、责任链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器。

1. 工厂方法模式

1.1 定义

工厂方法模式又叫简单工厂模式,属于创建型模式,在工厂方法模式中,可以根据参数的不同返回不同类的实例。即定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

1.2 工厂方法模式的实现

(1)创建抽象产品类

public abstract class Computer {
    /**
     * 产品的抽象方法,由具体的产品类去实现
     */
    public abstract void start();
}

(2)创建具体产品类

public class LenovoComputer extends Computer {
    @Override
    public void start() {
        System.out.println("开始生产联想电脑,,,");
    }
}
public class HpComputer extends Computer{
    @Override
    public void start() {
        System.out.println("开始生产惠普电脑,,,");
    }
}

(3)创建工厂类

public class ComputerFactory {
    public static Computer createComputer(String type){
        Computer mComputer=null;
        switch (type) {
            case "lenovo":
                mComputer=new LenovoComputer();
                break;
            case "hp":
                mComputer=new HpComputer();
                break;
        }
        return mComputer;
    }
}

(4)测试

public class CreatComputerTest {

    public static void main(String[] args) {
        ComputerFactory.createComputer("hp").start();
    }
}

输出结果:

开始生产惠普电脑,,,

1.3 Dubbo中的工厂方法模式

CacheFactory的实现采用的是工厂方法模式。CacheFactory接口定义getCache方法,然后定义一个AbstractCacheFactory抽象类实现CacheFactory,并将实际创建cache的createCache方法分离出来,并设置为抽象方法。这样具体cache的创建工作就留给具体的子类去完成。

2. 抽象工厂模式

2.1 定义

抽象工厂模式(Abstract Factory Pattern)指提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。它是一种对象创建型模式。在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的产品。

2.2 抽象工厂模式的实现

(1)定义抽象产品

public abstract class Video {
    public abstract void produce();
}
public abstract class Article {
    public abstract void produce();
}

(2)定义具体的产品

public class JavaVideo extends Video{
    @Override
    public void produce() {
        System.out.println("java 课程视频");
    }
}
public class JavaArticle extends Article{
    @Override
    public void produce() {
        System.out.println("java 课程笔记");
    }
}
public class PythonVideo extends Video{
    @Override
    public void produce() {
        System.out.println("python 课程视频");
    }
}
public class PythonArticle extends Article{
    @Override
    public void produce() {
        System.out.println("python 课程笔记");
    }
}

(3)创建抽象工厂

public interface CourseFactory {
    Video getVideo();
    Article getArticle();
}

(4)创建具体工厂

public class JavaCourseFactory implements CourseFactory {
    @Override
    public Video getVideo() {
        return new JavaVideo();
    }

    @Override
    public Article getArticle() {
        return new JavaArticle();
    }
}
public class PythonCourseFactory implements CourseFactory{
    @Override
    public Video getVideo() {
        return new PythonVideo();
    }

    @Override
    public Article getArticle() {
        return new PythonArticle();
    }
}

(5)创建测试类

public class AbstractFactoryTest {
    public static void main(String[] args) {
        CourseFactory courseFactory = new JavaCourseFactory();
        Video video = courseFactory.getVideo();
        Article article = courseFactory.getArticle();
        video.produce();
        article.produce();
    }
}

输出结果:

java 课程视频
java 课程笔记

2.3 Dubbo中的抽象工厂模式

Dubbo中的ProxyFactory及其子类使用的是抽象工厂模式。ProxyFactory提供两个方法,分别用来生产Proxy和Invoker(这两个方法签名看起来有些矛盾,因为getProxy方法需要传入一个Invoker对象,而getInvoker方法需要传入一个Proxy对象,看起来会形成循环依赖,但其实两个方式使用的场景不一样)。AbstractProxyFactory实现了ProxyFactory接口,作为具体实现类的抽象父类。然后定义了JdkProxyFactory和JavassistProxyFactory两个具体类,分别用来生产基于jdk代理机制和基于javassist代理机制的Proxy和Invoker。

3. 代理模式

3.1 定义

代理模式(Proxy Pattern)指给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式是一种对象结构型模式,其分为静态代理和动态代理。

3.2 代理模式的实现

(1)创建一个抽象类

public interface Person {
    void doSomething();
}

(2)创建具体实现类

public class Bob implements Person{
    @Override
    public void doSomething() {
        System.out.println("Bob do something ,,,");
    }
}

(3)创建jdk动态代理类,实现InvocationHandler接口

public class JDKDynamicProxy implements InvocationHandler {

    // 被代理的对象
    Person target;

    // JDKDynamicProxy构造函数
    public JDKDynamicProxy(Person person) {
        this.target = person;
    }
    // 获取代理对象
    public Person getTarget() {
        return (Person) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                        target.getClass().getInterfaces(), this);
    }
    // 动态代理invoke方法
    @Override
    public Person invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 被代理的方法前执行
        System.out.println("JDKDynamicProxy do something before!");
        // 执行被代理的方法
        Person result = (Person) method.invoke(target, args);
        // 被代理的方法后执行
        System.out.println("JDKDynamicProxy do something after!");
        return result;
    }
}

(4)创建测试类

public class JDKDynamicTest {
    public static void main(String[] args) {
        // 获取代理类
        Person proxyPerson = new JDKDynamicProxy(new Bob()).getTarget();
        // 调用doSomething方法
        proxyPerson.doSomething();
    }
}

输出结果:

JDKDynamicProxy do something before!
Bob do something ,,,
JDKDynamicProxy do something after!

3.3 Dubbo中的代理模式

Dubbo实现SPI机制的类ExtensionLoader的 Adaptive实现是典型的动态代理实现。 Dubbo需要灵活地控制实现类,即在调用阶段动态地根据参数决定调用哪个实现类,所以采用先生成代理类的方法,能够做到灵活的调用。生成代理类的代码是 ExtensionLoader 的createAdaptiveExtensionClassCode 方法。代理类的主要逻辑是,获取 URL 参数中指定参数的值作为获取实现类的 key。此外,Dubbo consumer使用Proxy类创建远程服务的本地代理,这也是一种代理模式,本地代理实现和远程服务一样的接口,并且屏蔽了网络通信的细节,使得用户在使用本地代理的时候,感觉和使用本地服务一样。

4. 适配器模式

4.1 定义

适配器模式(Adapter Pattern)作为两个不兼容接口之间的桥梁,通过适配类结合了两个独立的接口功能,它属于结构型模式。

4.2 适配器模式的实现

音频播放器只能播放mp3文件,另一个更高级的音频播放器可以播放mp4和avi文件。实现音频播放器可以播放mp3、mp4和avi文件。
(1)创建普通播放器接口和高级播放器接口

public interface GeneralMediaPlayer {
    void play(String audioType, String fileName);
}
public interface AdvancedMediaPlayer {
    void playMp4(String fileName);
    void playAvi(String fileName);
}

(2)创建高级播放器接口的实现类

public class Mp4Player implements AdvancedMediaPlayer{
    @Override
    public void playMp4(String fileName) {
        System.out.println("play mp4 file: "+ fileName);
    }

    @Override
    public void playAvi(String fileName) {

    }
}
public class AviPlayer implements AdvancedMediaPlayer {
    @Override
    public void playMp4(String fileName) {

    }

    @Override
    public void playAvi(String fileName) {
        System.out.println("paly avi file: "+fileName);
    }
}

(3)创建实现GeneralMediaPlayer接口的适配器类

public class MediaAdapter implements GeneralMediaPlayer {

    AdvancedMediaPlayer advancedMediaPlayer;

    public MediaAdapter(String audioType) {
        if(audioType.equalsIgnoreCase("mp4")){
            advancedMediaPlayer= new Mp4Player();
        }else if (audioType.equalsIgnoreCase("avi")){
            advancedMediaPlayer= new AviPlayer();
        }
    }

    @Override
    public void play(String audioType, String fileName) {
        if(audioType.equalsIgnoreCase("mp4")){
            advancedMediaPlayer.playMp4(fileName);
        }else if (audioType.equalsIgnoreCase("avi")){
            advancedMediaPlayer.playAvi(fileName);
        }
    }
}

(4)创建普通播放器接口的实现类

public class AudioPlayer implements GeneralMediaPlayer {

    GeneralMediaPlayer generalMediaPlayer;

    @Override
    public void play(String audioType, String fileName) {

        if(audioType.equalsIgnoreCase("mp3")){
            System.out.println("play mp3 file " + fileName);
        }else if (audioType.equalsIgnoreCase("mp4") ||audioType.equalsIgnoreCase("avi")) {
            generalMediaPlayer = new MediaAdapter(audioType);
            generalMediaPlayer.play(audioType,fileName);
        }else {
            System.out.println("invalid media: " + audioType + " format not supported !");
        }
    }
}

(5)创建测试类

public class AdapterTest {
    public static void main(String[] args) {
        AudioPlayer audioPlayer = new AudioPlayer();
        audioPlayer.play("mp3","百花香.mp3");
        audioPlayer.play("mp4","光辉岁月.mp4");
        audioPlayer.play("avi","盗梦空间.avi");
        audioPlayer.play("vlc","because of you.vlc");
    }
}

输出结果:

play mp3 file 百花香.mp3
play mp4 file: 光辉岁月.mp4
paly avi file: 盗梦空间.avi
invalid media: vlc format not supported !

4.3 Dubbo中的适配器模式

Dubbo中为了让用户根据自己的需求选择日志组件,Dubbo自定义了自己的Logger接口,并为常见的日志组件(包括jcl, jdk, log4j, slf4j)提供相应的适配器。并且利用简单工厂模式提供一个LoggerFactory,客户可以创建抽象的Dubbo自定义Logger,而无需关心实际使用的日志组件类型。在LoggerFactory初始化时,客户通过设置系统变量的方式选择自己所用的日志组件,这样提供了很大的灵活性。

5. 装饰器模式

5.1 定义

装饰器模式(Decorator Pattern)指允许向一个现有的对象添加新的功能,同时又不改变其结构。即对原有的类进行包装,它属于结构型模式。

5.2 修饰器模式的实现

(1)定义一个抽象类

public abstract class Goods {
    public abstract String getName();
}

(2)定义一个具体类

public class Phone extends Goods{
    @Override
    public String getName() {
        System.out.println("origin name");
        return "origin name";
    }
}

(3)定义一个抽象的包装类

public abstract class Wrapper extends Goods{

    private Goods goods;

    public Wrapper(Goods goods){
        this.goods = goods;
    }

    @Override
    public String getName() {
        return goods.getName();
    }
}

(4)定义一个具体包装类

public class WrapperFunction extends Wrapper{

    public WrapperFunction(Goods goods) {
        super(goods);
    }

    public String addFunction(String origin){
        String newStr = "在" + origin + "的基础上添加功能,,,";
        System.out.println(newStr);
        return newStr;
    }

    @Override
    public String getName() {
        String origin = super.getName();
        return addFunction(origin);
    }
}

(5)创建测试类

public class DecoratorTest {
    public static void main(String[] args) {
        Goods goods = new Phone();
        goods = new WrapperFunction(goods);
        goods.getName();
    }
}

输出结果:

origin name
在origin name的基础上添加功能,,,

5.3 Dubbo中的修饰器模式

Dubbo中使用的装饰器模式有ProtocolFilterWrapper类是对Protocol类的装饰。在export和refer方法中,配合责任链模式,把Filter组装成责任链,实现对Protocol功能的装饰。其他还有ProtocolListenerWrapper、 ListenerInvokerWrapper、InvokerWrapper等。

6. 责任链模式

6.1 定义

责任链模式是将请求发送和接收解耦,让多个接收对象都有机会处理这个请求。这些接收对象串成一条链路并沿着这条链路传递这个请求,直到链路上某个接收对象能够处理它。

6.2 责任链模式的实现

(1)定义AbstractHandler抽象处理者,使子类形成一条链

public abstract class AbstractHandler {

    private AbstractHandler abstractHandler;

    /**
     * 定义请求处理方法
     * @param condition
     */
    public abstract void handleRequest(String condition);

    public AbstractHandler getAbstractHandler() {
        return abstractHandler;
    }

    public void setAbstractHandler(AbstractHandler abstractHandler) {
        this.abstractHandler = abstractHandler;
    }
}

(2)创建若干个ConcreteHandler具体处理者继承AbstractHandler,在当前处理者对象无法处理时,将执行权传给下一个处理者对象

public class ConcreteHandlerA extends AbstractHandler{

    @Override
    public void handleRequest(String condition) {
        if("A".equals(condition)){
            System.out.println("ConcreteHandlerA 执行处理逻辑,,,");
        }else{
            System.out.println("ConcreteHandlerA 不处理,交由其他Handler处理");
            super.getAbstractHandler().handleRequest(condition);
        }

    }
}
public class ConcreteHandlerB extends AbstractHandler{

    @Override
    public void handleRequest(String condition) {
        if("B".equals(condition)){
            System.out.println("ConcreteHandlerB 执行处理逻辑,,,");
        }else{
            System.out.println("ConcreteHandlerB 不处理,交由其他Handler处理");
            super.getAbstractHandler().handleRequest(condition);
        }

    }
}
public class ConcreteHandlerC extends AbstractHandler{

    @Override
    public void handleRequest(String condition) {
        //如果这是最后一个handler则处理
        System.out.println("ConcreteHandlerC 执行处理逻辑,,,");
    }
}

(3)创建测试类

public class ChainTest {

    public static void main(String[] args) {
        AbstractHandler handlerA = new ConcreteHandlerA();
        AbstractHandler handlerB = new ConcreteHandlerB();
        AbstractHandler handlerC = new ConcreteHandlerC();

        //如果A处理不掉转交给B
        handlerA.setAbstractHandler(handlerB);
        //如果B处理不掉转交给C
        handlerB.setAbstractHandler(handlerC);
        //执行请求
        handlerA.handleRequest("C");
    }
}

输出结果:

ConcreteHandlerA 不处理,交由其他Handler处理
ConcreteHandlerB 不处理,交由其他Handler处理
ConcreteHandlerC 执行处理逻辑,,,

6.3 Dubbo中的责任链模式

Dubbo的调用链组织是用责任链模式串连起来的。责任链中的每个节点实现Filter接口,然后由ProtocolFilterWrapper,将所有Filter串连起来。

@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    if (UrlUtils.isRegistry(invoker.getUrl())) {
        return protocol.export(invoker);
    }
    // invoker就是直接调用对应方法的代码,JavassistProxyFactory
    return protocol.export(buildInvokerChain(invoker, SERVICE_FILTER_KEY, CommonConstants.PROVIDER));
}

然后利用org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper#buildInvokerChain构建Filter链的逻辑,通过SPI方式进行加载,FIlter配置路径为META-INF/dubbo/internal/org.apache.dubbo.rpc.Filter,配置内容如下:

echo=org.apache.dubbo.rpc.filter.EchoFilter
generic=org.apache.dubbo.rpc.filter.GenericFilter
genericimpl=org.apache.dubbo.rpc.filter.GenericImplFilter
token=org.apache.dubbo.rpc.filter.TokenFilter
accesslog=org.apache.dubbo.rpc.filter.AccessLogFilter
activelimit=org.apache.dubbo.rpc.filter.ActiveLimitFilter
classloader=org.apache.dubbo.rpc.filter.ClassLoaderFilter
context=org.apache.dubbo.rpc.filter.ContextFilter
consumercontext=org.apache.dubbo.rpc.filter.ConsumerContextFilter
exception=org.apache.dubbo.rpc.filter.ExceptionFilter
executelimit=org.apache.dubbo.rpc.filter.ExecuteLimitFilter
deprecated=org.apache.dubbo.rpc.filter.DeprecatedFilter
compatible=org.apache.dubbo.rpc.filter.CompatibleFilter
timeout=org.apache.dubbo.rpc.filter.TimeoutFilter
tps=org.apache.dubbo.rpc.filter.TpsLimitFilter

注意,上述配置的Filter是包括Consumer和Provider的,初始化provider只会用到provider侧的Filter配置,具体Filter实现类会通过group来区分PROVIDER或者CONSUMER。

7. 观察者模式

7.1 定义

观察者模式(Observer Pattern)指对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新,它属于对象行为型模式。

7.2 观察者模式的实现

(1)创建观察者接口

public interface Observer {
    void update(String status);
}

(2)创建观察者实现类

public class ConcreteObserver implements Observer{
    @Override
    public void update(String status) {
        System.out.println("我是观察者,接收到改变的状态:"+ status);
    }
}

(3)创建目标对象

public class Subject {
    /**
     * 定义观察者列表,观察者注册到其中
     */
    private List<Observer> observers = new ArrayList<>();

    /**
     * 注册观察者
     * @param observer
     */
    void attach(Observer observer){
        observers.add(observer);
    }

    /**
     * 通知所有注册的观察者
     * @param status
     */
    void notifyObservers(String status){
        for(Observer observer:observers){
            observer.update(status);
        }
    }
}

(4)创建具体的目标对象

public class ConcreteSubject extends Subject {

    private String status;

    public String getStatus(){
        return status;
    }

    public void change(String status){
        this.status = status;
        //状态发生改变,通知各个观察者
        this.notifyObservers(status);
    }
}

(5)创建测试类

public class ObserverTest {
    public static void main(String[] args) {
        //创建目标
        ConcreteSubject concreteSubject = new ConcreteSubject();
        //创建观察者对象
        Observer observer = new ConcreteObserver();
        //将观察者注册到目标对象中
        concreteSubject.attach(observer);
        //目标对象状态发生改变
        concreteSubject.change("change status");
    }
}

输出结果:

我是观察者,接收到改变的状态:change status

7.3 Dubbo中的观察者模式

在Dubbo provider服务启动时候要向注册中心注册自己的服务,在Dubbo consumer向注册中心订阅服务时则是一种观察者模式,它开启了一个listener,注册中心会每5秒定时检查是否有服务更新,如果有更新,向该服务的提供者发送一个notify消息, provider接受到notify消息后,即运行NotifyListener 的notify方法,执行监听器方法。此外,Dubbo中的InvokerListener、ExporterListener也实现了观察者模式,只要实现该接口,并注册,就可以接收到consumer端调用refer和provider端调用export的通知。

public interface NotifyListener {
    /**
     * 当收到服务变更通知时触发。
     * @param urls 已注册信息列表,总不为空
     * 含义同{@link com.alibaba.dubbo.registry.RegistryService#lookup(URL)}的返回值。
     */
    void notify(List<URL> urls);
}

八、基于Springboot之Dubbo整合Zookeeper项目实战

代码地址:https://gitee.com/tudedong/springboot-dubbo-zookeeper.git

1. Dubbo项目搭建

1.1 搭建父工程(springboot-dubbo-zookeeper)

注意:
(1)修改父工程的packaging为pom

<packaging>pom</packaging>

(2)依赖版本

jdk: 1.8
dubbo: 2.7.3
zookeeper(curator): 2.13.0

1.2 搭建公共接口API工程(user-common)

提供公共接口及方法

1.3 搭建dubbo服务工程服务提供者(user-service)

服务名:dubbo-user-service
服务端口:8080
注意:
(1)dubbo的配置方式,官网提供四种配置方式,分别为XML配置、属性配置、API配置、注解配置,这里采用XML配置:dubbo-user-service.xml
(2)dubbo-user-service.xml使用dubbo:service/进行服务的注册
(3)工程中service实现类的注解@Service注解是spring下的注解,不是dubbo包下的注解
(4)在启动类上添加注解 @ImportResource(“classpath:dubbo-user-service.xml”)

1.4 搭建dubbo服务工程服务服务消费者(consumer-service)

服务名:dubbo-consumer-service
端口:8081
注意:
(1)引入dubbo-consumer-service.xml配置文件,使用dubbo:reference/进行服务的消费
(2)在启动类上添加注解 @ImportResource(“classpath:dubbo-consumer-service.xml”)

1.5 测试

(1)启动服务提供者,启动类:UserServiceApplication.java
(2)启动服务消费者,启动类:UserWebApplication.java
(3)在浏览器上访问 http://localhost:8081/sayHello,返回结果如下:

hello,xiao xi yuan

2. Dubbo配置项说明

【dubbo:application】

对应org.apache.dubbo.config.ApplicationConfig, 代表当前应用的信息。
(1)name: 当前应用程序的名称,在dubbo-admin中我们也可以看到,这个代表这个应用的名称。我们在真正时是时也会根据这个参数来进行聚合应用请求
(2)owner: 当前应用程序的负责人,可以通过这个负责人找到其相关的应用列表,用于快速定位到责任人
(3)qosEnable : 是否启动QoS,默认true
(4)qosPort : 启动QoS绑定的端口,默认22222
(5)qosAcceptForeignIp: 是否允许远程访问,默认是false

【dubbo:registry】

对应org.apache.dubbo.config.RegistryConfig, 代表该模块所使用的注册中心。一个模块中的服务可以将其注册到一个上注册中心上,也可以注册到多个注册中心上。后面再service和reference也会引入这个注册中心。
(1)id : 当当前服务中provider或者consumer中存在多个注册中心时,则使用需要增加该配置。在一些公司,会通过业务线的不同选择不同的注册中心,所以一般都会配置该值
(2)address : 当前注册中心的访问地址
(3)protocol : 当前注册中心所使用的协议是什么。也可以直接在 address 中写入,比如使用zookeeper,就可以写成 zookeeper://xx.xx.xx.xx:2181
(4)timeout : 当与注册中心不再同一个机房时,大多会把该参数延长

【dubbo:protocol】

对应org.apache.dubbo.config.ProtocolConfig, 指定服务在进行数据传输所使用的协议。
(1)id : 在大公司,可能因为各个部门技术栈不同,所以可能会选择使用不同的协议进行交互。这里在多个协议使用时,需要指定
(2)name : 指定协议名称,默认使用dubbo

【dubbo:service】

对应org.apache.dubbo.config.ServiceConfig, 用于指定当前需要对外暴露的服务信息,和 dubbo:reference 大致相同。
(1)interface : 指定当前需要进行对外暴露的接口
(2)ref : 具体实现对象的引用,一般我们在生产级别都是使用Spring去进行Bean托管的,所以这里面一般也指的是Spring中的BeanId
(3)version : 对外暴露的版本号。不同的版本号,消费者在消费的时候只会根据固定的版本号进行消费

【dubbo:reference】

对应org.apache.dubbo.config.ReferenceConfig, 消费者消费服务的配置
(1)id : 指定该Bean在注册到Spring中的id
(2)interface: 服务接口名
(3)version : 指定当前服务版本,与服务提供者的版本一致
(4)registry : 指定所具体使用的注册中心地址。这里面也就是使用上面在 dubbo:registry 中所声明的id

【dubbo:method】

对应org.apache.dubbo.config.MethodConfig, 用于在制定的dubbo:service或dubbo:reference中的更具体一个层级,指定具体方法级别在进行RPC操作时候的配置,可以理解为对这上面层级中的配置针对于具体方法的特殊处理。
(1)name : 指定方法名称,用于对这个方法名称的RPC调用进行特殊配置
(2)async: 是否异步,默认false

3. Dubbo启动时依赖检查问题

3.1 依赖检查问题

问题1:当Zookeeper注册中心没启动的时候,生产者和消费者都会报错,影响项目的运行。
问题2:在实际的应用生产中,如果我们的服务的提供方没有启动服务,这个时候消费者工程就会报错影响项目的运行,如上面项目,启动服务消费端运行报错:
在这里插入图片描述

3.2 依赖检查问题概述

出现上面的问题是Dubbo缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认check=“true” 。

3.3 依赖检查问题的解决方案

可以通过check="false"关闭检查,比如,测试时,有些服务不关心,或者出现了循环依赖,必须有一方先启动,或Spring 容器是懒加载的,或者通过API编程延迟引用服务,需要关闭检查,否则服务临时不可用时,会抛出异常,拿到null引用,如果check=“false” ,总是会返回引用,当服务恢复时,能自动连上。
对于属性配置可以使用以下两种方式修改:
(1)在消费者consumer-service 中@Reference 加入check=false即可
(2)修改配置的属性文件,格式是:dubbo.reference.(报名+接口名).check=false

dubbo.reference.com.tdd.service.UserService.check=false 

对于XML配置可以修改配置文件如下:

<dubbo:reference version="1.0" interface="com.tdd.service.UserService" id="userService" check="false"/>

4. Dubbo的直连操作

4.1 概述

在开发及测试环境下,或注册中心不可用的情况下,经常需要绕过注册中心,只测试指定服务提供者,这时可能需要点对点直连,点对点直连将以服务接口为单位,忽略注册中心的提供者列表,A接口配置点对点,不影响B接口从注册中心获取列表。这种点对点的直接连接指定的服务就是Dubbo的直连模式。所以,Dubbo服务消费者Consumer不一定要从注册中心获取Dubbo服务提供者列表也可以工作,即消费端强制直连提供端。
注意:一旦使用了直连就失去负载均衡的能力。

4.2 配置使用

使用属性配置如下:

@Reference(url = "dubbo://localhost:8080")
private Userservice userservice;

使用XML配置如下:

<dubbo:reference version="1.0" interface="com.tdd.service.UserService" id="userService" check="false" url="dubbo://localhost:8080"/>

5. Dubbo调用结果的缓存机制

5.1 概述

当服务启动完毕后,客户端发起一次调用请求,Dubbo会先去查询缓存,如果能找到结果,则返回。否则执行完整的调用请求,最后将结果缓 存。
Dubbo提供了三种结果缓存机制:
(1)lru:基于最近最少使用原则删除多余缓存,保持最热的数据被缓存
(2)threadlocal:当前线程缓存
(3)jcache:可以桥接各种缓存实现

5.2 配置使用

可以在服务消费配置文件中进行配置:

<dubbo:reference version="1.0" interface="com.tdd.service.UserService" id="userService" cache="lru"/>

或在服务消费配置文件中对方法级进行配置:

<dubbo:reference version="1.0" interface="com.tdd.service.UserService" id="userService">
        <dubbo:method name="sayHello" cache="lru"/>
</dubbo:reference>

6. Dubbo的异步调用

6.1 概述

Dubbo不只提供了堵塞式的的同步调用,同时提供了异步调用的方式。当你需要同时调用多个Dubbo服务,这些服务又没有先后顺序,就可以使用异步调用。异步调用主要应用于提供者响应耗时明显的接口方法,比如商品详情页需要调用优惠服务和库存服务就可以使用异步调用。假设每个服务调用耗时10ms,如果顺序调用则商品详情页需要耗时20ms,如果使用异步调用则只需要耗时10ms。
如何获取异步结果?
当使用异步调用Dubbo接口服务时,服务会立刻返回结果,但是结果为null。我们需要通过RpcContext获取到该服务调用的Future,然后通过调用Future.get()获取真正的结果。详见前面源码分析。

6.2 异步调用的配置使用

基于XML的配置,可以在服务消费配置文件中对方法级进行配置:

<dubbo:reference id="userService" interface="com.tdd.service.UserService">
	<dubbo:method name="sayHello" async="true" />
</dubbo:reference>

注意:在Dubbo 2.7.x的异步化实现中,无需在相关配置中进行特殊配置,显示声明异步接口即可,然后使用callback方式处理返回值。

7. Dubbo服务限流

7.1 概述

为了防止某个消费者的QPS或是所有消费者的QPS总和突然飙升而导致的重要服务的失效,系统可以对访问流量进行控制,这种对集群的保护措施称为服务限流。
Dubbo有多种限流方式,可以使用以下参数进行多维度的限流:
(1)accepts:服务端最大可接受连接数,可以理解为可以接受的最大消费者数;
(2)connections:每个Reference开启的连接数;
(3)actives:消费端控制每个接口的最大并发数;
(4)executes:服务端控制每个接口的最大并发数;

7.2 使用介绍

(1)accepts的配置:
accepts配置数表示服务端配置最大可接受连接数,这是项目级别设置,它仅可设置在服务端。
比如一个Provider设置了accepts=2,该Provider3个消费者分别为C1,C2,C3。假如这3个消费者的启动顺序为C1,C2,C3,则C3会无法启动,因为服务已经达到了最大连接数限制;
基于XML配置如下:

<!--限制当前提供者在使用dubbo协议最多接受10个消费者链接-->
<dubbo:protocol name="dubbo" port="20880" accepts="10"/>

(2)connections的配置:
connections配置数表示每个Reference开启的长连接数,默认是0,表示所有的Reference共享同一条连接;如果大于0,则单独为此Reference设置connections条长连接。
比如同一个项目有3个reference:
@Reference(connections=3) HelloService helloService;
@Reference TestService testService;
@Reference FooService fooService;
则该项目会生成4条连接,其中helloService有3条,testService与fooService共用一条
connections可以设置在提供者端,也可以设置在消费者端。限定连接的个数。对于短连接,该属性效果与actives相同。但对于长连接,其限制的是长连接的个数。 一般情况下,会使connectons与actives联用,让connections限制长连接个数,让actives限制一个长连接中可以处理的请求个数。联用前提:使用默认的Dubbo服务暴露协议
服务提供端限流配置:
接口级别:

<!--限制当前接口中每个方法的链接数不能超过10-->
<dubbo:service interface="com.tdd.service.UserService" ref="userService" connections="10"/>

方法级别:

<!--限制当前接口中sayHello方法的链接数不能超过10-->
    <dubbo:service version="1.0" interface="com.tdd.service.UserService" ref="userService">
        <dubbo:method name="sayHello" connections="10"/>
    </dubbo:service>

服务消费端限流配置:
接口级别:

<!--设置当前消费者对指定接口的每一个方法的链接数不能超过10-->
<dubbo:reference interface="com.tdd.service.UserService" id="userService" connections="10"/>

方法级别:

<!--设置当前消费者对指定接口的sayHello方法的链接数不能超过10-->
<dubbo:reference interface="com.tdd.service.UserService" id="userService" connections="10">
        <dubbo:method name="sayHello" connections="10"/>
</dubbo:reference>

(3)actives的配置:
actives配置数表示服务消费端每个接口的最大并发数,默认是0,如果是0则没有限制。该限流方式可以设置在服务供者端,也可以设置在服务消费端。可以设置为接口级别,也可以设置为方法级别。
服务提供端限流配置:
接口级别:

<!--设置当前服务提供端对指定接口的每一个方法的并发连接数不能超过10-->
<dubbo:service interface="com.tdd.service.UserService" ref="userService" actives="10"/>

方法级别:

<!--限制当前接口中sayHello方法的并发链接数不能超过10个-->
    <dubbo:service interface="com.tdd.service.UserService" ref="userService">
        <dubbo:method name="sayHello" actives="10"/>
    </dubbo:service>

服务消费端限流配置:
接口级别:

<!--设置当前消费者对指定接口的每一个方法的并发链接数不能超过10个-->
<dubbo:reference interface="com.tdd.service.UserService" id="userService" actives="10"/>

方法级别:

<!--设置当前消费者对指定接口的sayHello方法的并发链接数不能超过10-->
    <dubbo:reference interface="com.tdd.service.UserService" id="userService" connections="10">
        <dubbo:method name="sayHello" actives="10"/>
    </dubbo:reference>

(4)executes的配置:
executes的配置数表示服务提供端每个接口的最大并发数,默认是0,如果是0则没有限制。它仅可设置在服务端。
基于XML配置如下:
接口级别:

<!--服务器端并发执行(或占用线程池线程数)不能超过 10-->
<dubbo:service interface="com.tdd.service.UserService" ref="userService" executes="10"/>

方法级别:

<!--服务器端并发执行(或占用线程池线程数)不能超过 10-->
<dubbo:service interface="com.tdd.service.UserService" ref="userService">
	<dubbo:method name="sayHello" executes="10"/>
</dubbo:service>

8. Dubbo服务分组应用

8.1 概述

在Dubbo服务中,当一个接口有多种实现时,可以用group区分。

public interface PayService {
    String pay(String name);
}

public class WeixinPayServiceImpl implements PayService {
    @Override
    public String pay(String name) {
        return "Weixin";
    }
}

public class ZhifubaoPayServiceImpl implements PayService {
    @Override
    public String pay(String name) {
        return "Zhifubao";
    }
}

配置使用:

<!-- 注册service实现类 -->
<bean id="weixinService" class="com.tdd.service.WeixinPayServiceImpl"/>
<bean id="zhifubaoService" class="com.tdd.service.ZhifubaoPayServiceImpl"/>
<!-- 暴露服务 -->
<dubbo:service interface="com.tdd.service.PayService" ref="weixinService" group="weixin"/>
<dubbo:service interface="com.tdd.service.PayService" ref="zhifubaoService" group="zhifubao"/>
<!-- 指定调用微信服务 -->
<dubbo:reference interface="com.tdd.service.PayService" id="weixin" group="weixin"/>
<!-- 指定调用支付宝服务 -->
<dubbo:reference interface="com.tdd.service.PayService" id="zhifubao" group="zhifubao"/>

9. Dubbo整合Hystrix服务降级

9.1 整合步骤

(1)在服务消费端引入Hystrix依赖
(2)在服务端启动类和消费端启动类上添加@EnableHystrix注解,启动Hystrix服务
(3)在服务端配置降级超时等信息,这里配置Hystrix超时时间为2s,睡眠时间为3s,如下:

@Override
    @HystrixCommand(commandProperties = {
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000") })
    public String sayHello(String message) {

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("服务被调用,方法执行了...");
        return "hello," + message;
    }

(4)在服务消费端配置降级方法等信息,如下:

@HystrixCommand(fallbackMethod = "sayHelloError")
    @GetMapping("/sayHello")
    public String sayHello(){
        System.out.println("调用了sayHello方法了...");
        return userService.sayHello("xiao xi yuan");
    }

    public String sayHelloError(){
        return "hystrix fallback value...";
    }

从上,可知当调用服务端方法时,会出现超时,然后会调用降级方法。

9.2 测试

在浏览器上输入:http://localhost:8081/sayHello 输出如下:

hystrix fallback value...

10. Dubbo升级接口灰度发布

10.1 灰度发布概述

灰度发布是实现新旧版本平滑过渡的一种发布方式,即让一部分服务更新到新版本,如果这部分服务没有什么问题,再将其它旧版本的服务更新。而实现简单的灰度发布我们可以使用版本号控制,每次发布都更新版本号,新更新的服务就不会调用旧的服务提供者。
当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。
一般处理步骤:
(1)在低压力时间段,先升级一半提供者为新版本
(2)再将所有消费者升级为新版本
(3)然后将剩下的一半提供者升级为新版本

10.2 配置使用

在不同服务提供端配置不同的版本号:

<dubbo:service version="1.0" interface="com.tdd.service.UserService" ref="userService"/>
<dubbo:service version="2.0" interface="com.tdd.service.UserService" ref="userService"/>

11. Dubbo中常见异常及解决方式

11.1 Dubbo整合springboot,使用spring-boot-devtools时创建Rest服务异常

具体报错如下:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘feignTargeter’ defined in class path resource [org/springframework/cloud/openfeign/FeignAutoConfiguration$DefaultFeignTargeterConfiguration.class]: Initialization of bean failed; nested exception is java.lang.IllegalArgumentException: non-public interface is not defined by the given loader
问题原因分析:
问题原因是采用spring-boot-devtools会存在两个classloader,一个是应用类加载器AppClassLoader,一个RestartClassLoader,AppClassLoader用于加载第三方jar包,而RestartClassLoader用于加载用户目录下的class。使用Rest协议时,会为每个service创建代理类,com.alibaba.dubbo.common.bytecode.Proxy创建代理类时,是用proxy所在classloader为AppClassLoader,去加载用户目录下的class,所以就会报对应的class找不到。
问题解决:
(1)去掉spring-boot-devtools依赖;
(2)如果必须要用spring-boot-devtools依赖(提供热加载,代码修改立即生效),可以尝试修改源码com.alibaba.dubbo.common.bytecode.Proxy.getProxy()方法。
将 public static Proxy getProxy(Class… ics) { return getProxy(ClassHelper.getCallerClassLoader(Proxy.class), ics); }
改成: public static Proxy getProxy(Class[] ics) {
return getProxy(Thread.currentThread().getContextClassLoader(), ics);
}

11.2 Dubbo服务启动时报错

具体报错如下:
Configuration problem: Unable to locate Spring NamespaceHandler for XML schema
问题原因分析:
Spring容器在启动时,找不到对应的<dubbo: …>配置的解析处理器,即Dubbo的jar包没引入或引入的版本不对;或Dubbo的jar包和xml中使用的对应的命名空间不匹配;或对应的加载器加载的配置不对等
问题解决:
(1)引入Dubbo的依赖jar包,确保对应的版本是正确的;
(2)检查xml中的Dubbo的xsd配置的路径跟你所引入的jar包的包路径是否一致,修改为一致;
(3)检查是否使用了spring-boot-devtools等热加载,加载的配置类不对,可以去掉spring-boot-devtools热加载,使之加载隔离。

11.3 Dubbo服务服务端地址找不到

具体报错如下:
No provider available for the service
问题原因分析:
没有可用的服务,说明服务提供者Provider服务没启动或启动失败了,若使用了注册中心,可能注册中心ZooKeeper、Nacos等宕机了;Dubbo的服务配置错误,比如服务名,组名,版本配置错误;配置中心配置错误,导致访问的环境错误或使用路由规则,路由规则错误等
问题解决:
(1)可用访问注册中心控制台,检查注册中心服务以及Dubbo服务提供者服务是否正常运行;
(2)检查调用服务和被调用服务的配置,确保服务名、组名和版本配置一致;
(3)若使用了配置中心,检查配置中心配置是否正确,若使用了负载均衡和路由服务,检查负载均衡的地址配置和路由规则配置是否正确等。

11.4 Dubbo服务消费端调用超时问题

具体报错如下:
Caused by: com.alibaba.dubbo.remoting.TimeoutException: Waiting server-side response timeout.
问题原因分析:
一般调用超时是指调用端发生在请求发出后,无法在指定的时间内获得对应的响应,可能是如下原因:
服务端确实处理比较慢,无法在指定的时间返回结果,消费端就自动返回一个超时的异常响应来结束此次调用;服务端响应比较快,但客户端Load很高,负载压力很大,这时客户端请求发不出去、响应卡在TCP Buffer等问题,造成超时。因为客户端接收到服务端发来的数据或者请求服务端的数据,都会在系统层面排队,如果系统负载比较高,在内核态的时间占比就会加长,从而造成客户端获取到值时已经超时;
问题解决:
(1)适当调高消费端和服务端的超时时间,适应当前的请求调用;
(2)检查消费端或者服务端的资源(CPU,内存)处理能力是否出现了瓶颈,可以增强其处理能力或调整负载均衡策略或采用限流措施等;
(3)检查网络问题或检查消费端和服务端的GC,进行性能优化等。
注意:具体超时问题的细节可以查看本文的【第六部分-Dubbo服务调用过程超时实现原理源码剖析】

11.5 Dubbo服务端的线程资源耗尽

具体报错如下:
java.util.concurrent.RejectedExecution或Thread pool is EXHAUSTED
问题原因分析:
Dubbo服务端的默认业务线程数是200个,如果多个并发请求量超过了200个,就会拒绝新的请求,或服务调用中发生处理阻塞,使处理线程数打满,都会抛出此错误。
问题解决:
(1)调整Consumer端的dubbo.consumer.actives参数的大小,调小一些即可;
(2)调整Provider端的dubbo.provider.threads参数的大小,调大一些即可;
(3)增加Provider服务的数量,分担压力。
注意:具体线程资源耗尽问题的分析排查可以查看本文的【第五部分-Dubbo线程池打满异常分析及解决】

11.6 Hessian序列化失败

具体报错如下:
org.apache.dubbo.remoting.RemotingException: message can not send, because channel is closed或HessianRuntimeException
问题原因分析:
Dubbo服务间的调用,也就是跨tomcat应用服务器之间的服务调用。在调用中,数据需要进行网络传输,在传输过程中需要序列化和反序列化,在java中,只有POJO实现了序列化接口,JVM才会把POJO转换成需要对应的信号进行传输。
问题解决:
(1)检查服务方法的传入传出参数(POPJ)是否实现Serializable接口,实现Serializable接口;
(2)检查服务方法的传入传出参数(POPJ)是否继承了Number、Date、ArrayList、HashMap等Hessian特殊化处理的类。

11.7 Dubbo服务调用请求中断

具体报错如下:
Could not deserialize parameter instance, error is: readObject: unexpected end of file
问题原因分析:
消费端发送请求或服务端接收请求被中断;或网络抖动原因,导致传输流被中断。
问题解决:
(1)通常是消费端或提供端的内存不足,导致buffer不能分配,使发送到一半的请求被中断了,提高消费端或提供端的内存;
(2)检查网络,确保网络正常。

11.8 Dubbo调用服务提供者的实现方法失败

具体报错如下:
Remote server returns error: [6], Got invocation exception
问题原因分析:
此异常表示Dubbo框架调用服务提供者的实现方法失败,不是方法本身的业务异常。通常是服务消费者和服务提供者的API签名不一致引起,或者提供方比消费方少此函数。一般是服务增加方法,或修改了方法签名,而双方用的服务API的jar包不一致。
问题解决:
先看下客户端的异常信息里的errorcode的含义:
1:收到消息的时候线程池拒绝处理
2:服务提供者端未能根据服务名找到相应服务
3:该服务调用时,服务提供者端不能加载参数类型对应的class
4:参数不能被正确的反序列化
5:不能正确从Class中create该调用所指示的方法
6:不能正确调用该方法
可以看出,6代表不能正确的调用该方法,检查服务提供端此方法的调用权限或签名,使消费端和服务端的API签名一致等。

文章内容输出来源:拉勾教育Java高薪训练营
若有错误之处,欢迎留言指正~~~

  • 7
    点赞
  • 0
    评论
  • 15
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值