RPC 框架 Dubbo 从理解到使用(一)

技术架构演变

学习 Dubbo 之前我们有必要先来了解一下互联网技术架构的演变过程及通信方式,方便我们搞清楚为什么需要使用基于 RPC 思想的系列框架。

单一应用架构

通俗地讲,“单体应用(monolith application)”就是将应用程序的所有功能都打包成一个独立的单元。当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。

特点

  • 所有的功能集成在一个项目工程中;
  • 所有的功能打一个 war 包部署到服务器;
  • 应用与数据库分开部署;
  • 通过部署应用集群和数据库集群来提高系统的性能。

优点:

  • 开发简单:一个 IDE 就可以快速构建单体应用;

  • 便于共享:单个归档文件包含所有功能,便于在团队之间以及不同的部署阶段之间共享;

  • 易于测试:单体应用一旦部署,所有的服务或特性就都可以使用了,这简化了测试过程,因为没有额外的依赖,每项测试都可以在部署完成后立刻开始;

  • 容易部署:整个项目就一个 war 包,Tomcat 安装好之后,应用扔上去就行了。群化部署也很容易,多个 Tomcat + 一个 Nginx 分分钟搞定。

缺点:

  • 妨碍持续交付:随着时间的推移,单体应用可能会变得比较大,构建和部署时间也相应地延长,不利于频繁部署,阻碍持续交付。在移动应用开发中,这个问题会显得尤为严重;
  • 不够灵活:随着项目的逐渐变大,整个开发流程的时间也会变得很长,即使在仅仅更改了一行代码的情况下,软件开发人员需要花费几十分钟甚至超过一个小时的时间对所有代码进行编译,并接下来花费大量的时间重新部署刚刚生成的产品,以验证自己的更改是否正确。如果多个开发人员共同开发一个应用程序,那么还要等待其他开发人员完成了各自的开发。这降低了团队的灵活性和功能交付频率;
  • 受技术栈限制:项目变得越来越大的同时,我们的应用所使用的技术也会变得越来越多。这些技术有些是不兼容的,就比如在一个项目中大范围地混合使用 C++ 和 Java 几乎是不可能的事情。在这种情况下,我们就需要抛弃对某些不兼容技术的使用,而选择一种不是那么适合的技术来实现特定的功能。
  • 可靠性差:某个环节出现了死循环,导致内存溢出,会影响整个项目挂掉。
  • 伸缩性差:系统的扩容只能针对应用进行扩容,不能做到对某个功能进行扩容,扩容后必然带来资源浪费的问题。
  • 技术债务:假设我的代码库中有一个混乱的模块结构。此时,我需要添加一个新功能。如果这个模块结构清晰,可能我只需要2天时间就可以添加好这个功能,但是如今这个模块的结构很混乱,所以我需要4天时间。多出来的这两天就是债务利息。随着时间推移、人员变动,技术债务必然也会随之增多。

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。

特点

  • 以单体结构规模的项目为单位进行垂直划分,就是将一个大项目拆分成一个一个单体结构项目。
  • 项目与项目之间存在数据冗余,耦合性较大,比如上图中三个项目都存在用户信息。
  • 项目之间的接口多为数据同步功能,如:数据库之间的数据库,通过网络接口进行数据库同步。

优点

  • 开发成本低,架构简单;

  • 避免单体应用的无限扩大;

  • 系统拆分实现了流量分担,解决了并发问题;

  • 可以针对不同系统进行扩容、优化;

  • 方便水平扩展,负载均衡,容错率提高;

  • 不同的项目可采用不同的技术;

  • 系统间相互独立。

缺点

  • 系统之间相互调用,如果某个系统的端口或者 IP 地址发生改变,调用系统需要手动变更;
  • 垂直架构中相同逻辑代码需要不断的复制,不能复用。
  • 系统性能扩展只能通过扩展集群结点,成本高、有瓶颈。

SOA 面向服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心。当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。

P.S. 从软件设计的角度上来说,ESB 是一个抽象的间接层,提取了服务调用过程中调用与被调用动态交互中的一些共同的东西,减轻了服务调用者的负担。Java 编程思想里提到:“所有的软件设计的问题都可以通过增加一个抽象的间接层而得到解决或者得到简化!”简单来说 ESB 就是一根管道,用来连接各个服务节点。为了集成不同系统,不同协议的服务,ESB 做了消息的转化解释和路由工作,让不同的服务互联互通。

特点

  • 基于 SOA 的架构思想将重复公用的功能抽取为组件,以服务的形式给各系统提供服务。
  • 各项目(系统)与服务之间采用 WebService、RPC 等方式进行通信。
  • 使用 ESB 企业服务总线作为项目与服务之间通信的桥梁。

优点

  • 将重复的功能抽取为服务,提高开发效率,提高系统的可重用性、可维护性。
  • 可以针对不同服务的特点制定集群及优化方案;
  • 采用 ESB 减少系统中的接口耦合。

缺点

  • 系统与服务的界限模糊,不利于开发及维护。
  • 虽然使用了 ESB,但是服务的接口协议不固定,种类繁多,不利于系统维护。
  • 抽取的服务的粒度过大,系统与服务之间耦合性高。
  • 涉及多种中间件,对开发人员技术栈要求高。
  • 服务关系复杂,运维、测试部署困难

微服务架构

特点

  • 将系统服务层完全独立出来,并将服务层抽取为一个一个的微服务。
  • 微服务中每一个服务都对应唯一的业务能力,遵循单一原则。
  • 微服务之间采用 RESTful 等轻量协议传输。

优点

  • 团队独立:每个服务都是一个独立的开发团队,这个小团队可以是 2 到 5 人的开发人员组成;
  • 技术独立:采用去中心化思想,服务之间采用 RESTful 等轻量协议通信,使用什么技术什么语言开发,别人无需干涉;
  • 前后端分离:采用前后端分离开发,提供统一 Rest 接口,后端不用再为 PC、移动端开发不同接口;
  • 数据库分离:每个微服务都有自己的存储能力,可以有自己的数据库。也可以有统一数据库;
  • 服务拆分粒度更细,有利于资源重复利用,提高开发效率;
  • 一个团队的新成员能够更快投入生产;
  • 微服务易于被一个开发人员理解,修改和维护,这样小团队能够更关注自己的工作成果。无需通过合作才能体现价值;
  • 可以更加精准的制定每个服务的优化方案(比如扩展),提高系统可维护性;
  • 适用于互联网时代,产品迭代周期更短。

缺点

  • 微服务过多,服务治理成本高,不利于系统维护;
  • 分布式系统开发的技术成本高(网络问题、容错问题、调用关系、分布式事务等),对团队挑战大;
  • 微服务将原来的函数式调用改为服务调用,不管是用 rpc,还是 http rest 方式,都会增大系统整体延迟。这个是再所难免的,这个就需要我们将原来的串行编程改为并发编程甚至异步编程,增加了技术门槛;
  • 多服务运维难度,随着服务的增加,运维的压力也在增大;
  • 测试的难度提升。服务和服务之间通过接口来交互,当接口有改变的时候,对所有的调用方都是有影响的,这时自动化测试就显得非常重要了,如果要靠人工一个个接口去测试,那工作量就太大了,所以 API 文档的管理尤为重要。

总结

分享两个小故事,帮助大家更好的理解 SOA 与微服务的区别。

故事一:

很久以前的一天,Martin 在跟好友的交流中悟到了一种很棒的架构设计。他总结了一下,然后告诉了好友,好友说,这不是新鲜东西,早有人总结了,叫做 SOA。

Martin 很高兴,开始在公司内外推广 SOA。结果,不停有人质疑和挑战他。

你这不是 SOA 吧?SOA 这里应该是如此这般的。对,这里我对 SOA 的理解是这样的。你看,这本 SOA 的书说的和你说的有出入。粒度?SOA 没有谈到这个呀,你这不是 SOA。分层跟 SOA 没有关系,你为什么要说这个呢?…

Martin 没办法,心一横,老子就叫它 Martin's SOA。老子发明的词,老子就是最高权威,有最终解释权。还有谁不服?

同事们一看,这思想本身很不错,值得推广,但叫 Martin's SOA 太没品了吧?还是要取个好一点的名字,最好还能跟 SOA 有某种暗示传承。干脆就叫 Microservices 好了,又新,又有服务含在其中。

Martin 忿忿地接受了这个建议,心里想着:妈的,明明就是 SOA,一群**非要逼我取个新名字。

后来 Martin 发现每次提一个东西就会收到旧恶傻势力对解释权的挑战,所以每次要提一个什么概念,都去发明一个新词,免得一群人在那一边质疑挑战,一边大谈“我的理解”。

这就是微服务、敏捷、精益企业、持续集成、持续交付背后的故事。

一个名词产生之后,命名者的解释权就会随着时间而弱化(比如 Cooper 发明的 Persona 就被无数设计师乱用)。敏捷已经有点烂了,等微服务也烂了,我们还会发明新词。

实在没辙,都是被逼的啊。

故事二:

话说1979年,又是一个春天,莆田乡下的赤脚医生吴大牛被改革的春风吹的心潮澎湃,说干就干,吴大牛趁着夜色朦胧找大队支书汇报了汇报思想,第二天就承包了村卫生室,开启了自己的在医疗圈的传奇历程。

乡村诊所大家都知道,没什么复杂的东东,房子只有一间,一个大柜台中间隔开,一半是诊疗兼候诊区,一半是药房,看病就直接找医生,如果前面有人就自己找个位子坐下,排队等一会,秩序倒也井然,看完病了医生直接给抓药,然后下一个继续,也不需要护士和药剂师,吴大牛一个人全部包办。

辛辛苦苦忙碌了十年,时间来到了八九年,又是一个春天,昔日的单身汉吴大牛已成为十里八乡的知名人物,媳妇娶上了不说,家里还增加了一对双胞胎儿子,二层的小洋房也甚是气派。可是也有烦心事,尽管乡村诊所扩大到了两间,媳妇还偶尔能去帮帮忙,但是医生还是只有自己一个,天天从早忙到晚挣的都是一份钱,想多挣点怎么办?吴大牛日思夜想,还真给他想出来一招,怎么办,扩大规模,多招几个医生一起干。原来吴大牛只能治头疼脑热和跌打损伤,现在新招了一个医科大学的毕业生刘小明专治感冒发烧,又从邻村请来了老大夫李阿花专治妇科病,现在一个普通的小诊所就变成了有三个独立科室加一个公共药房(吴大牛媳妇负责)的小医院了,吴大牛是外科主任兼院长,收入那可比之前翻了三番。人逢喜事精神爽,大牛院长请县里的书法名家为新医院书写了牌匾--“博爱医院”,挑了一个黄道吉日正式挂了上去。

一晃十年过去了,又是一个春天,吴大牛的博爱医院已经发展到了内科外科妇科五官科骨科生殖科六个科室,每个科室3到5名医生不等,也耗费巨资购进了血液化验B超等先进仪器,大牛院长也早已脱离了医疗一线,成为了专职的管理者,但是医院的大事小事大家都找他,就这三十多号员工搞的他每天是焦头烂额,想再扩大规模实在是有心无力了。要说还是大学生有水平,老部下刘小明给大牛院长献了一计,把各个科室独立出去,让各个科室主任自己管理,大牛院长只管科室之间的协调和医院发展的大事,这样既能调动基层的积极性,又能把大牛院长解放出来扩大生产抓大事谋大事,岂不妙哉?就这样,博爱医院的新一轮改革轰轰烈烈的展开了。

又是一个十年,又是一个春天,大牛院长已成为本地知名的企业家,博爱医院也发展到了二十三个科室数百名员工,发展中也出现了新问题,由于各个科室独立挂号、收费、化验,有的科室整天忙忙碌碌效益好,有的科室就相对平庸些,连分到的各种检查仪器都不能满负荷运行,整个医院养了不少闲人。这时候大牛院长视野也开阔了,请来了管理专家进行了顶层设计,把原来分散到各个科室的非核心服务全部收归集中管理,把原来二十三个挂号窗口整合为十个,二十三个收费窗口整合为八个,集中布设在一楼大厅为全院服务,还把分散在各个科室的检查仪器集中起来成立独立的检验科,也为全院服务,这样人人有活干,整个医院的服务能力又上了一个新台阶,这轮改革后博爱医院通过了各级部门的鉴定成为了远近驰名的三甲医院,吴大牛也摇身一变成为了博爱集团的CEO兼董事长,下一步就准备IPO上市了。

说到这里大家可能有点糊涂,这个跟微服务有嘛关系?大牛诊所的1.0阶段就相当于软件开发的单体结构,一个程序员打天下,从头编到尾,很难做大做强。大牛诊所的2.0阶段就相当于软件开发的垂直结构,各科室按照业务划分,很容易横向扩展。博爱医院的1.0阶段就相当于软件开发的SOA结构,除了药房(数据库)外各个服务独立提供(科室主任负责),但需要大牛院长(ESB总线)来协调。博爱医院的2.0阶段就相当于软件开发的微服务结构,公共服务院内共享,科室主任管理功能弱化(只管医生业务),优点是扩容方便,哪个部门缺人直接加,不用看上下游,资源利用率高,人员和设备效率高。

微服务就是将一个单体架构的应用按业务划分为一个个的独立运行的程序即服务,它们之间通过 HTTP 协议进行通信(也可以采用消息队列来通信,如 RabbitMQ,Kafaka 等),可以采用不同的编程语言,使用不同的存储技术,自动化部署(如 Jenkins)减少人为控制,降低出错概率。服务数量越多,管理起来越复杂,因此采用集中化管理。例如 Eureka,Zookeeper 等都是比较常见的服务集中化管理框架。

微服务是一种架构风格,架构就是为了解耦,实际使用的是分布式系统开发。一个大型的复杂软件应用,由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好的完成该任务。

一句话总结:微服务是 SOA 发展出来的产物,它是一种比较现代化的细粒度的 SOA 实现方式。

通信方式

随着互联网的发展,应用程序从单机走向分布式,通信方式也产生了很多的变化。

TCP/UDP

都是传输协议,主要区别是 TCP 协议连接需要 3 次握手,断开需要四次挥手,是通过流来传输的,就是确定连接后,一直发送信息,传完后断开。UDP 不需要进行连接,直接把信息封装成多个报文,直接发送。所以 UDP 的速度更快,但是不保证数据的完整性。

一句话总结:最古老且最有效,永不过时,学习成本高。所有通信方式归根结底都是 TCP/UDP。

WebService

WebService(SOA,SOAP,WSDL,UDDI,XML)技术, 能使得运行在不同机器上的不同应用无须借助附加的、专门的第三方软件或硬件, 就可相互交换数据或集成。依据 WebService 规范实施的应用之间, 无论它们所使用的语言、 平台或内部协议是什么, 都可以相互交换数据。

WebService 就是一种跨编程语言和跨操作系统平台的远程调用技术。WebService 交互的过程就是遵循 SOAP 协议通过 XML 封装数据,然后由 Http 协议来传输数据。

一句话总结:基于 HTTP + XML 的标准化 Web API。

RESTful

Representational State Transfer,表现层状态转移。互联网通信协议 HTTP 协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer)。而这种转化是建立在表现层之上的,所以就是"表现层状态转移"。

客户端用到的手段,只能是 HTTP 协议。具体来说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源(也可以用于更新资源),PUT 用来更新资源,DELETE 用来删除资源。

  • 无状态协议 HTTP,具备先天优势,扩展能力很强。例如需要安全加密时,有现成的成熟方案 HTTPS 可用。
  • JSON 报文序列化,轻量简单,人与机器均可读,学习成本低,搜索引擎友好。
  • 语言无关,各大热门语言都提供成熟的 Restful API 框架。

一句话总结:基于 HTTP + JSON 的标准化 Web API。

RMI

Remote Method Invocation,远程方法调用。Java 中实现的分布式通信协议,它大大增强了 Java 开发分布式应用的能力。通过 RMI 技术,某一个本地的 JVM 可以调用存在于另外一个 JVM 中的对象方法,就好像它仅仅是在调用本地 JVM 中某个对象方法一样。

一句话总结:基于 Java 语言的分布式通信协议。

JMS

Java Message Service,Java 消息服务应用程序接口,是一个 Java 平台中关于面向消息中间件的 API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。绝大多数 MQ 都对 JMS 提供支持,如 RabbitMQ、ActiveMQ、Kafka、RocketMQ 以及 Redis 等。

一句话总结:JavaEE 消息框架标准。

RPC

Remont Proceduce Call,远程过程调用。它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。RPC 只是一个概念,它不是一个协议也不是一个框架。

RPC 的具体实现可以使用 RMI 或 RESTful 等,但一般不用,因为 RMI 不能跨语言,RESTful 效率太低。

RPC 多用于服务器集群内部通信,因此常使用更加高效、短小精悍的传输模式以提高效率。RPC 框架有很多:Apache Thrift、Apache Dubbo、Google Grpc 等。

一句话总结:解决分布式系统中,服务之间的调用问题。远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。

为什么需要 RPC?

主要就是因为在几个进程内(应用分布在不同的机器上),无法共用内存空间,比如不同的系统之间的通讯,甚至不同组织之间的通讯。此外由于机器的横向扩展,需要在多台机器组成的集群上部署应用等等。

比如现在有两台机器:A 机器和 B 机器,并且分别部署了应用 A 和应用 B。假设此时位于 A 机器上的 A 应用想要调用位于 B 机器上的 B 应用提供的函数或是方法,由于 A 应用和 B 应用不在一个内存空间里面,所以不能直接调用,此时就需要通过网络来表达调用的方式和传输调用的数据。也即所谓的远程调用。

RPC 实现原理

一次完整的 RPC 调用流程包含了四个核心部分,分别是 ClientServerClient Stub 以及 Server Stub,这个 Stub 大家可以理解为存根。分别说说这几个部分:

客户端(Client):服务的调用方。

服务端(Server):服务的提供方。

客户端存根:存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。

服务端存根:接收客户端发送过来的消息,将消息解包,并调用本地的方法。

  1. 客户端(Client)以本地调用方式(即以接口的方式)调用服务;
  2. 客户端存根(Client Stub)接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体(将消息体对象序列化为二进制);
  3. 客户端通过 Socket 将消息发送到服务端;
  4. 服务端存根(Server Stub)收到消息后进行解码(将消息对象反序列化);
  5. 服务端存根(Server Stub)根据解码结果调用本地的服务;
  6. 本地服务进行业务逻辑处理;
  7. 本地服务将业务逻辑处理后的结果返回给服务端存根(Server Stub);
  8. 服务端存根(Server Stub)将返回结果打包成消息(将结果消息对象序列化);
  9. 服务端(Server)通过 Socket 将消息发送到客户端;
  10. 客户端存根(Client Stub)接收到结果消息,并进行解码(将结果消息反序列化);
  11. 客户端(Client)得到最终结果。

RPC 的目标是要把 2、3、4、5、7、8、9、10 这些步骤都封装起来。

建立通信

解决通讯的问题,主要是通过在客户端和服务器之间建立 TCP 连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。

服务寻址

A 服务器上的应用怎么告诉底层的 RPC 框架,如何连接到 B 服务器(如主机或 IP 地址)以及特定的端口,方法的名称是什么,这样才能完成调用。比如基于 Web 服务协议栈的 RPC,就要提供一个 endpoint URI,或者是从 UDDI(一种目录服务,通过该目录服务进行服务注册与搜索)服务上查找。如果是 RMI 调用的话,还需要一个 RMI Registry 来注册服务的地址。

网络传输

序列化

A 服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如 TCP 传递到 B 服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给 B 服务器。

反序列化

B 服务器收到请求后,需要对参数进行反序列化(序列化的逆操作),恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理 Proxy 去调用,通常会有 JDK 动态代理、CGLIB 动态代理、Javassist 生成字节码技术等),之后得到调用的返回值。

服务调用

B 机器进行本地调用(通过代理 Proxy)之后得到了返回值,此时还需要再把返回值发送回 A 机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回 A 机器,而当 A 机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给 A 机器上的应用进行相关处理(一般是业务逻辑处理操作)。

RPC 基于 RMI 的简单实现

搭建 Maven 聚合项目

父工程 rmi-demo

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>rmi-demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>rmi-api</module>
        <module>rmi-server</module>
        <module>rmi-client</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

</project>

子工程 rmi-api

这个工程主要是存放 client 和 server 都会用到的公共接口。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>rmi-demo</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>rmi-api</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
    </dependencies>

</project>
实体类

由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,所以实体类需要实现 Serializable 接口。

package org.example.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private static final long serialVersionUID = 2159427410483687648L;
    private Integer id;
    private String username;

}
服务接口

使用 JavaRMI 对外暴露的服务接口必须继承 java.rmi.Remote.Remote 类,方法必须抛出 java.rmi.RemoteException 异常。

package org.example.service;

import org.example.pojo.User;

import java.rmi.Remote;
import java.rmi.RemoteException;

/**
 * 用户管理服务
 */
public interface UserService extends Remote {

    User selectUserById(Integer userId) throws RemoteException;

}

子工程 rmi-server

主要提供服务接口的实现以及 RMI 的服务配置。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>rmi-demo</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>rmi-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>rmi-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
服务实现

服务接口实现必须继承 java.rmi.server.UnicastRemoteObject 类。

package org.example.service.impl;

import org.example.pojo.User;
import org.example.service.UserService;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

/**
 * 用户管理服务
 */
public class UserServiceImpl extends UnicastRemoteObject implements UserService {

    public UserServiceImpl() throws RemoteException {
    }

    @Override
    public User selectUserById(Integer userId) throws RemoteException {
        System.out.println("用户管理服务接收到客户端请求,请求参数 userId = " + userId);
        // 模拟假数据返回
        return new User(userId, "张三");
    }

}
发布服务

将服务发布在指定的 IP + 端口上。

package org.example;

import org.example.service.UserService;
import org.example.service.impl.UserServiceImpl;

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

/**
 * 发布服务
 */
public class Publish {

    public static void main(String[] args) throws RemoteException {
        UserService userService = new UserServiceImpl();
        try {
            // 对外暴露的服务端口
            LocateRegistry.createRegistry(8888);
            // 对外暴露的服务地址
            Naming.bind("rmi://localhost:8888/userService", userService);
            System.out.println("服务发布成功!");
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }

}

子工程 rmi-client

本地 client 如何实现调用远程接口。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>rmi-demo</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>rmi-client</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>rmi-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
远程服务调用

通过指定的 IP + 端口进行远程服务调用。

package org.example.controller;

import org.example.pojo.User;
import org.example.service.UserService;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class UserController {

    public static void main(String[] args) {
        try {
            UserService userService = (UserService) Naming.lookup("rmi://localhost:8888/userService");
            User user = userService.selectUserById(1);
            System.out.println("远程服务调用成功,返回值信息为:" + user);
        } catch (NotBoundException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

}

测试

通过测试可以看到 RPC 基于 RMI 的远程服务调用已经完成,接下来我们一起学习一下如何使用 RPC 框架 Dubbo 来完成远程服务调用。

RPC 框架

一个典型 RPC 框架使用场景中,包含了服务注册与发现(注册中心)、负载均衡、容错、网络传输、序列化等组件,其中“RPC 相关协议”就指明了程序如何进行网络传输和序列化。RPC 框架有很多:Apache Thrift、Apache Dubbo、Google Grpc 等。下图为完整的 RPC 框架架构图:

Dubbo 介绍

官网:http://dubbo.apache.org/zh-cn/

Github:https://github.com/apache/dubbo

2018 年 2 月 15 日,阿里巴巴的服务治理框架 dubbo 通过投票,顺利成为 Apache 基金会孵化项目。

Apache Dubbo 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用智能容错和负载均衡,以及服务自动注册和发现

Dubbo 架构

Dubbo 提供三个核心功能:面向接口的远程方法调用智能容错和负载均衡,以及服务自动注册和发现。Dubbo 框架广泛的在阿里巴巴内部使用,以及当当、去哪儿、网易考拉、滴滴等都在使用。

节点角色说明

节点 角色说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心
Container 服务运行容器

调用关系说明

  1. 服务容器负责启动,加载,运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

Dubbo 快速入门

下面我们基于 SpringBoot 环境整合 Dubbo 完成一个入门案例。

搭建 Maven 聚合项目

父工程 dubbo-demo

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>dubbo-demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>dubbo-api</module>
        <module>dubbo-provider</module>
        <module>dubbo-consumer</module>
    </modules>

    <!-- 继承 spring-boot-starter-parent 依赖 -->
    <!-- 使用继承方式,实现复用,符合继承的都可以被使用 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.4.1</version>
        </dependency>
    </dependencies>

</project>

子工程 dubbo-api

这个工程主要是存放 provider 和 consumer 都会用到的公共接口。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>dubbo-demo</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>dubbo-api</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>
实体类

由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,所以实体类需要实现 Serializable 接口。

package org.example.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private static final long serialVersionUID = 2159427410483687648L;
    private Integer id;
    private String username;

}
服务接口
package org.example.service;

import org.example.pojo.User;

/**
 * 用户管理服务
 */
public interface UserService {

    User selectUserById(Integer userId);

}

子工程 dubbo-provider

主要提供服务接口的实现以及服务提供者的服务配置。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>dubbo-demo</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>dubbo-provider</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>dubbo-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
配置文件

配置文件需要配置服务提供者应用信息,配置注册中心及暴露服务的地址,还有采用什么样的协议及协议端口和扫描服务接口的包地址。

server:
  port: 7070 # 端口

# Dubbo 配置
dubbo:
  # 提供方应用信息,用于计算依赖关系
  application:
    name: product-service
  # 配置注册中心
  registry:
    address: multicast://224.5.6.7:1234 # 使用 Multicast 注册中心暴露服务地址
  # 用 dubbo 协议在 20880 端口暴露服务
  protocol:
    name: dubbo # 协议名称
    port: 20880 # 协议端口
  # 扫描需要暴露的服务
  scan:
    base-packages: org.example.service
服务实现(服务提供者)
package org.example.service.impl;

import org.example.pojo.User;
import org.example.service.UserService;
import org.apache.dubbo.config.annotation.Service;

/**
 * 用户管理服务
 *      timeout 调用该服务的超时时间
 *      version 为版本号
 *      group 为分组
 *      interface、group、version 三者可确定一个服务
 *      parameters = {"unicast", "false"}
 *          建议服务提供者和服务消费者在不同机器上运行,
 *          如果在同一机器上,需设置 unicast = false 禁用单播订阅,只有 multicast 注册中心有此问题。
 */
@Service(timeout = 5000, version = "1.0", group = "user-provider", parameters = {"unicast", "false"})
public class UserServiceImpl implements UserService {

    @Override
    public User selectUserById(Integer userId) {
        System.out.println("用户管理服务接收到客户端请求,请求参数 userId = " + userId);
        // 模拟假数据返回
        return new User(userId, "张三");
    }

}

注意:parameters = {"unicast", "false"}:建议服务提供者和服务消费者在不同机器上运行,如果在同一机器上,需设置 unicast = false 禁用单播订阅,只有 multicast 注册中心有此问题。

启动类
package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// 扫描需要暴露的服务,如果配置文件中已声明则无需添加该注解
// @EnableDubbo(scanBasePackages = "org.example.service")
@SpringBootApplication
public class DubboProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(DubboProviderApplication.class, args);
    }

}

子工程 dubbo-consumer

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>dubbo-demo</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>dubbo-consumer</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>dubbo-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
配置文件

配置文件需要配置服务消费者应用信息,配置注册中心用于发现注册中心暴露的服务。

server:
  port: 9090 # 端口

# Dubbo 配置
dubbo:
  # 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样
  application:
    name: dubbo-consumer
  # 配置注册中心
  registry:
    address: multicast://224.5.6.7:1234 # 发现 Multicast 注册中心暴露的服务
远程服务调用
package org.example.controller;

import org.apache.dubbo.config.annotation.Reference;
import org.example.pojo.User;
import org.example.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/consumer")
public class ConsumerController {

    // dubbo 提供了 @Reference 注解,可替换 @Autowired 注解,用于引入远程服务
    // 如果注册服务时设置了版本及分组信息,调用远程服务时也要设置对应的版本及分组信息
    @Reference(timeout = 5000, version = "1.0", group = "user-provider", parameters = {"unicast", "false"})
    private UserService userService;

    @GetMapping("/{id}")
    public User selectUserById(@PathVariable("id") Integer id) {
        return userService.selectUserById(id);
    }

}
启动类
package org.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DubboConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(DubboConsumerApplication.class, args);
    }

}

测试

启动服务提供者和服务消费者,访问:http://localhost:9090/consumer/1 结果如下:

Dubbo 常用标签

  • dubbo:application:应用程序名称
  • dubbo:registry:连接注册中心信息(配置注册中心)
  • dubbo:protocol:服务提供者注册服务采用的协议
  • dubbo:service:声明需要暴露的服务接口
  • dubbo:reference:配置订阅的服务(生成远程服务代理)

更多配置信息请参考:http://dubbo.apache.org/zh-cn/docs/user/references/xml/introduction.html

下一篇我们讲解 Dubbo 支持的注册中心,Dubbo 负载均衡策略和 Dubbo 控制台的安装,记得关注噢~

本文采用 知识共享「署名-非商业性使用-禁止演绎 4.0 国际」许可协议

大家可以通过 分类 查看更多关于 Dubbo 的文章。


🤗 您的点赞转发是对我最大的支持。

📢 关注公众号 哈喽沃德先生「文档 + 视频」每篇文章都配有专门视频讲解,学习更轻松噢 ~


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342