【Java开发】Spring Cloud 04 :服务治理Nacos

本章节正式进入 Spring Cloud 环节了,首先介绍微服务架构中一个最重要的原理概念:服务治理,在概念讲解之后,讲解介绍 Nacos 服务注册中心的体系结构。

项目源码:尹煜 / coupon-yinyu · GitCode

1 服务治理

1.1 服务治理介绍

首先通过一个例子告诉你服务治理解决了什么问题。

比如系统包含两个微服务(服务 A 和服务 B),每一个微服务有 10 个虚拟节点,两个服务组成了一个 20 台虚拟机的微服务集群。如果此时微服务 A 想要调用微服务 B,我们怎么来发起这个调用呢?

一种通用做法是:在服务 A 的配置文件中添加一个指向服务 B 的地址,但这个地址并不直接指向任何一台服务 B 集群中的节点,而是指向一个 VIP(虚拟 IP 地址)或者是一个网关。这个 VIP 或网关背后维护了 B 集群的服务节点列表,VIP 层通过负载均衡策略再将请求转到后面配置的某一台服务器。

从上面的图中我们可以看出,服务 A 与服务 B 之间互相不直接通信,服务调用完全依靠 VIP 作为中间人来完成。我们如果想要为服务集群扩容或缩容,必须将服务器配置到对应的 VIP 地址上。

那在微服务架构中,怎么才能实现一种简单可靠的远程服务调用,不让 VIP 中间商赚差价呢?

如果我们要解决中间商赚差价的问题,那么最好的办法就是让双方直连。因此,服务治理要解决的首要任务就是服务注册服务发现,通过这两项技术,我们就能让微服务之间发起面对面的直接调用。

那么服务 A 怎么知道服务 B 中每台机器的地址呢?为了让服务 A 拿到服务 B 的机器清单,我们需要搭建一个中心化的服务注册中心,服务 B 只要将自己的信息添加到注册中心里,服务 A 就能够从注册中心获取到服务 B 的所有节点列表。

从上图中的步骤中我们可以看出,首先,服务 B 集群向注册中心发起了注册,将自己的地址信息上报到注册中心,这个过程就是服务注册。接下来,每隔一段时间,服务 A 就会从服务中心获取服务 B 集群的服务列表,或者由服务中心将服务列表的变动推送给服务 A,这个过程叫做服务发现;最后,服务 A 根据本地负载均衡策略,从服务列表中选取某一个服务 B 的节点,发起服务调用。

在这个过程中,注册中心的角色是一个中心化的信息管理者,所有的微服务节点在启动后都会将自己的地址信息添加到注册中心。在服务注册的过程中,有两个关键信息是最为重要的:

  • 服务名称:服务名称通常默认是 spring.application.name 属性,在服务注册过程中我们必须将应用服务名上报到注册中心,这样其他服务才能根据服务名称找到对应的服务节点列表

  • 地址信息:包括服务节点的 IP 地址和端口

通过上面这两个信息,调用方就能精准定位到目标微服务。除此之外,服务注册请求中还包含一些额外的注册信息。通过服务注册和服务发现,我们已经能够实现端到端的服务调用链路,但这个方案似乎还并不完善,因为它缺少了异常容错的机制。

如果服务 B 集群因为未知的网络故障导致无法响应服务,这时候服务 A 向服务 B 发起了服务调用,就会发生超时或者服务无响应的异常情况。那我们如何在服务治理方案中规避这类问题呢?

先说一个大前提。所有的服务都要在注册中心进行注册,而且每个节点都需要每隔一段时间向注册中心同步自己当前的状态,我们很形象地称这个过程为 heartbeat(心跳)。

如果节点持续发送心跳信息,则一切正常,服务可以被发现;如果注册中心在一段时间内没有收到 Client 的心跳包,注册中心就会将这个节点标记为下线状态,进而将该服务从服务列表中剔除。

我们上面说的“服务剔除”是由注册中心主导的“被动下线”场景。除此之外还有一类服务“主动下线”的场景,也就是当服务节点关闭或者重启的时候,通过发送一条“服务下线”指令给到注册中心,将当前节点标记为下线状态。

到这里,相信你已经完全理解了微服务生命周期各个状态间的流转,也知道了服务注册中心在微服务生命周期中扮演了什么角色。接下来,我们来了解 Spring Cloud 中的服务注册中心 Nacos。

1.2 Nacos 体系架构

Nacos 有三个核心知识点:领域模型、数据模型和基本架构,这是我们整体把握 Nacos 架构的关键。下面我们来依次看看。

① 领域模型

Nacos 领域模型描述了服务与实例之间的边界和层级关系。Nacos 的服务领域模型是以“服务”为维度构建起来的,这个服务并不是指集群中的单个服务器,而是指微服务的服务名。

“服务”是 Nacos 中位于最上层的概念,在服务之下,还有集群和实例的概念:

从上面的图中你可以看出,Nacos 的服务领域模型从上到下分为了服务、集群和实例三层,我分别介绍一下这三个层次所包含的重要数据内容。

Ⅰ服务

在服务这个层级上我们可以配置元数据和服务保护阈值等信息。服务阈值是一个 0~1 之间的数字,当服务的健康实例数与总实例的比例小于这个阈值的时候,说明能提供服务的机器已经没多少了。这时候 Nacos 会开启服务保护模式,不再主动剔除服务实例,同时还会将不健康的实例也返回给消费者。尽管这样做可能造成请求失败,但间接保证了最低限度的服务可用性。

Ⅱ 集群

一个服务由很多服务实例组成,在每个服务实例启动的时候,我们可以设置它所属的集群,在集群这个层级上,我们也可以配置元数据。除此之外,我们还可以为持久化节点设置健康检查模式。

所谓持久化节点,是一种会保存到 Nacos 服务端的实例,即便该实例的客户端进程没有在运行,实例也不会被服务端删除,只不过 Nacos 会将这个持久化节点状态标记为不健康,Nacos 可以采用一种“主动探活”的方式来对持久化节点做健康检查。

除了持久化节点以外,大部分服务节点在 Nacos 中以“临时节点”的方式存在,它是默认的服务注册方式,从名字中我们就可以看出,这种节点不会被持久化保存在 Nacos 服务器,临时节点通过主动发送 heartbeat 请求向服务器报送自己的状态。

Ⅲ 实例

这里所说的实例就是指服务节点,我们可以在 Nacos 控制台查看每个实例的 IP 地址和端口、编辑实例的元数据信息、修改它的上线 / 下线状态或者配置路由权重等等。

你会发现,在这三个层级上都有“元数据”这一数据结构,你可以把它理解为一组包含了服务描述信息(如服务版本等)和自定义标签的数据集合。Client 端通过服务发现技术可以获取到每个服务实例的元数据,你可以将自定义的属性加入到元数据并在 Client 端实现某些定制化的业务场景。

了解了领域模型之后,你知道服务调用的发起方是如何定位到领域模型中的服务实例的吗?这就要说起 Nacos 的数据模型了。

② 数据模型

Nacos 的数据模型有三个层次结构,分别是 Namespace、Group 和 Service/DataId:

从上图中你可以看出,Namespace、Group 和 Service/DataId 是一个依次包含的结构:

  • Namespace:即命名空间,它是最顶层的数据结构,我们可以用它来区分开发环境、生产环境等不同环境。默认情况下,所有服务都部署到一个叫做“public”的公共命名空间

  • Group:在命名空间之下有一个分组结构,默认情况下所有微服务都属于“DEFAULT_GROUP”这个分组,不同分组间的微服务是相互隔离的

  • Service/DataID:在 Group 分组之下,就是具体的微服务了,比如订单服务、商品服务等等

通过 Namespace + Group + Service/DataID,我们就可以精准定位到一个具体的微服务。比如,我想调用生产环境下 A 分组的订单服务,那么对应的服务寻址的 Key 就是类似 Production.A.orderService 的组合。

了解了 Nacos 的数据模型之后,再来带你看一下 Nacos 的基本架构。

③ 基本架构

Nacos 的核心功能有两个,一个是 Naming Service,也就我们用来做服务发现的模块;另一个是 Config Service,用来提供配置项管理、动态更新配置和元数据的功能。

从上面的图中你可以看出,Provider APP 和 Consumer APP 通过 Open API 和 Nacos 服务器的核心模块进行通信。这里的 Open API 是一组对外暴露的 RESTful 风格的 HTTP 接口。如果你对 Open API 里具体的接口感兴趣,可以从Nacos 官方网站获取更多的关于 Open API 的详细信息。

在 Nacos 和核心模块里,Naming Service 提供了将对象和实体的“名字”映射到元数据的功能,这是服务发现的基础功能之一。例如,我想要调用 OrderService,我手里有这个服务的 Namespace 和 Group 信息,那么我就可以通过 Naming Service 定位到这个服务对应的实例列表。同理,如果我有一个 DNS 名称,同样可以借助 Naming Service 获取 DNS 背后配置的 IP 列表。以上两个场景就分别对应了服务发现和 DNS 功能,这两个场景都是 Naming Service 的核心场景。

Nacos 还有一个相当重要的模块:Nacos Core 模块。它可以提供一系列的平台基础功能,是支撑 Nacos 上层业务场景的基石:

除了 Nacos Core 提供的这些功能以外,Nacos 还有一个“一致性协议”,用来确保 Nacos 集群中各个节点之间的数据一致性。Nacos 内部支持两种一致性协议,一种是侧重一致性的 Raft 协议,基于集群中选举出来的 Leader 节点进行数据写入;另一种是针对临时节点的 Distro 协议,它是一个侧重可用性(或最终一致性)的分布式一致性协议。

2 Nacos集群环境搭建

我们已经对 Nacos 功能体系有了全面的认识,接下来动手搭建 Nacos 服务注册中心。本章节如何搭建一个高可用的 Nacos 服务集群,以及如何使用 MySQL 作为 Nacos 的底层数据存储方案。这些内容可以帮助你理解什么是“高可用架构”。

保障系统的高可用性有两个大道至简的方向:

  • 避免单点故障:在做系统架构的时候,你应该假设任何服务器都有可能挂掉。如果某项任务依赖单一服务资源,那么这就会成为一个“单点”,一旦这个服务资源挂掉就表示整个功能变为不可用。所以你要尽可能消灭一切“单点”

  • 故障机器状态恢复:尽快将故障机器返回到故障前的状态。对于像 Nacos 这类中心化注册中心来说,因故障而下线的机器在重新上线后,应该有能力从某个地方获取故障发生前的服务注册列表

那 Nacos 是如何解决上面这两个问题,来保证自己的高可用性的呢?很简单,就是构建服务集群。集群环境不仅可以有效规避单点故障引发的问题,同时对于故障恢复的场景来说,重新上线的机器也可以从集群中的其他节点同步数据信息,恢复到故障前的状态。

2.1 下载 Nacos Server

Nacos Server 的安装包可以从 Alibaba 官方 GitHub 中的Release 页面下载。当前最新的稳定版本是 2.2.0,本项目也借助 Nacos 做为注册中心和配置中心。在选择 Nacos 版本的时候你要注意,一定要选择稳定版使用,不要选择版本号中带有 BETA 字样的版本(比如 2.0.0-BETA)。

Nacos 2.2.0 Release note 下方的 Assets 面板中包含了该版本的下载链接,你可以在 nacos-server-2.2.0.tar.gz(用于linux) 和 nacos-server-2.2.0.zip(用于windows) 这两个压缩包中选择。如果你对 Nacos 的源码比较感兴趣,也可以下载 Source code 源码包来学习。

下载完成后,你可以在本地将 Nacos Server 压缩包解压,并将解压后的目录名改为“nacos-cluster1”,再复制一份同样的文件到 nacos-cluster2,我们以此来模拟一个由两台 Nacos Server 组成的集群。

至此完成了 Nacos 服务器的下载安装,接下来修改 Nacos Server 的启动项参数。

2.2 修改启动项参数

Nacos Server 的启动项位于 conf 目录下的 application.properties 文件里,别看这个文件里的配置项密密麻麻一大串,但大部分都不用你操心,直接使用默认值就好。

你只需要修改这里面的服务启动端口和数据库连接串就好了。因为你需要在一台机器上同时启动两台 Nacos Server 来模拟一个集群环境,所以这两台 Nacos Server 需要使用不同的端口,否则在启动阶段会报出端口冲突的异常信息。

Nacos Server 的启动端口由 server.port 属性指定,默认端口是 8848。我们在 nacos-cluster1 中仍然使用 8848 作为默认端口,你只需要把 nacos-cluster2 中的端口号改掉就可以了,这里我把它改为 8948。

接下来,你需要对 Nacos Server 的 DB 连接串做一些修改。在默认情况下,Nacos Server 会使用 Derby 作为数据源,用于保存配置管理数据。Derby 是 Apache 基金会旗下的一款非常小巧的嵌入式数据库,可以随 Nacos Server 在本地启动。但从系统的可用性角度考虑,我们需要将 Nacos Server 的数据源迁移到更加稳定的 MySQL 数据库中。

你需要修改三处 Nacos Server 的数据库配置:

  • 指定数据源:spring.datasource.platform=mysql 这行配置默认情况下被注释掉了,它用来指定数据源为 mysql,你需要将这行注释放开

  • 指定 DB 实例数:放开 db.num=1 这一行的注释

  • 修改 JDBC 连接串:db.url.0 指定了数据库连接字符串,我指向了 localhost 3306 端口的 nacos 数据库,稍后我将带你对这个数据库做初始化工作;db.user.0 和 db.password.0 分别指定了连接数据库的用户名和密码,我使用了默认的无密码 root 账户

下面的图是完整的数据库配置项:

修改完数据库配置项之后,接下来就是在 MySQL 中创建 Nacos Server 所需要用到的数据库 Schema 和数据库表。

2.3 创建数据库 Schema 和 Table

Nacos Server 的数据库用来保存配置信息、Nacos Portal 登录用户、用户权限等数据,下面我们分两步来创建数据库。

① 创建 Schema

可以通过数据库控制台或者 DataGrip 之类的可视化操作工具,执行下面这行 SQL 命令,创建一个名为 nacos 的 schema。

create schema nacos;

② 创建数据库表

Nacos 已经把建表语句准备好了,就放在你解压后的 Nacos Server 安装目录中。打开 Nacos Server 安装路径下的 conf 文件夹,找到里面的 nacos-mysql.sql 文件,你所需要的数据库建表语句都在这了。你也可以直接到源码仓库的资源文件中获取 Nacos 建表语句的 SQL 文件。

将文件中的 SQL 命令复制下来,在第一步中创建的 schema 下执行这些 SQL 命令。执行完之后,你就可以在在数据库中看到这些 tables 了,总共有 12 张数据库表。

数据库准备妥当之后,我们还剩最后一项任务:添加集群机器列表。添加成功后就可以完成集群搭建了。

2.4 添加集群机器列表

Nacos Server 可以从一个本地配置文件中获取所有的 Server 地址信息,从而实现服务器之间的数据同步。

所以现在我们要在 Nacos Server 的 conf 目录下创建 cluster.conf 文件,并将 nacos-cluster1 和 nacos-cluster2 这两台服务器的 IP 地址 + 端口号添加到文件中。下面是我本地的 cluster.conf 文件的内容。

## 注意,这里的IP不能是localhost或者127.0.0.1
192.168.1.100:8848
192.168.1.100:8948

这里需要注意的是,你不能在 cluster.conf 文件中使用 localhost 或者 127.0.0.1 作为服务器 IP,否则各个服务器无法在集群环境下同步服务注册信息。这里的 IP 应该使用你本机分配到的内网 IP 地址。

如果你使用的是 mac 或者 linux 系统,可以在命令行使用 ifconfig | grep “inet” 命令来获取本机 IP 地址,下图中红框标出的这行 inet 地址就是本机的 IP 地址。

如果你使用的是 windows系统,可以在命令行使用 ipconfig 命令获取本机 IP 地址。

到这里,我们已经完成了所有集群环境的准备工作,接下来我带你去启动 Nacos Server 验证一下效果。

2.5 启动 Nacos Server

Nacos 的启动脚本位于安装目录下的 bin 文件夹,下图是 bin 目录下的启动脚本。其中 Windows 操作系统对应的启动脚本和关闭脚本分别是 startup.cmd 和 shutdown.cmd,Mac 和 Linux 系统对应的启动和关闭脚本是 startup.sh 和 shutdown.sh。

如果你希望以单机模式(非集群模式)启动一台 Nacos 服务器,可以在 bin 目录下通过命令行执行下面这行命令:

## 如果是 Mac 操作系统
sh startup.sh -m standalone
## 如果是 Windows 操作系统
startup.cmd -m standalone

通过 -m standalone 参数,指定了服务器以单机模式启动。Nacos Server 在单机模式下不会主动向其它服务器同步数据,因此这个模式只能用于开发和测试阶段,对于生产环境来说,我们必须以 Cluster 模式启动。

如果希望将 Nacos Server 以集群模式启动,只需要在命令行直接执行 sh startup.sh 命令就可以了,因为 Nacos 默认是以集群形式启动的。这时控制台会打印以下两行启动日志。

nacos is starting with cluster
nacos is starting,you can check the /Users/banxian/workspace/dev/middleware/nacos-cluster1/logs/start.out

这两行启动日志没有告诉你 Nacos Server 最终是启动成功还是失败,不过你可以在第二行日志中找到一些蛛丝马迹。这行日志告诉了我们启动日志所在的位置是 nacos-cluster1/logs/start.out,在启动日志中你可以查看到一行成功消息“Nacos started successfully in cluster mode”。当然了,如果启动失败,你也可以在这里看到具体的 Error Log。

我们用同样的方式先后启动 nacos-cluster1 和 nacos-cluster2,如上图所示,在启动日志中显示了成功消息“started successfully in cluster mode”,这代表服务器已经成功启动了,接下来你就可以登录 Nacos 控制台了。

2.6 登录 Nacos 控制台

在 Nacos 的控制台中,我们可以看到服务注册列表、配置项管理、集群服务列表等信息。在浏览器中打开nacos-cluster1 或者nacos-cluster2 的地址,注意这两台服务器的端口分别是 8848 和 8948。你可以看到下面的 Nacos 的登录页面。

你可以使用 Nacos 默认创建好的用户 nacos 登录系统,用户名和密码都是 nacos。当然了,你也可以在登录后的权限控制 -> 用户列表页面新增系统用户。成功登录后,你就可以看到 Nacos 控制台首页了。

为了验证集群环境处于正常状态,你可以在左侧导航栏中打开“集群管理”下的“节点列表”页面,在这个页面上显示了集群环境中所有的 Nacos Server 节点以及对应的状态,在下面的图中我们可以看到 192.168.1.100:8848 和 192.168.1.100:8948 两台服务器,并且它们的节点状态都是绿色的“UP”,这表示你搭建的集群环境一切正常。

好,到这里,我们的 Nacos 集群环境搭建就完成了。如果你在搭建环境的过程中发现 Nacos 无法启动,只需要到启动日志 /logs/start.out 中就能找到具体的报错信息。如果你碰到了启动失败的问题,不妨先去检查以下两个地方:

  • 端口占用:即 server.port 所指定的端口已经被使用,你需要更换一个端口重新启动服务

  • MySQL 连不上:你需要检查 application.properties 里配置的 MySQL 连接信息是否正确,并确认 MySQL 服务处于运行状态

在实际的项目中,如果某个微服务 Client 要连接到 Nacos 集群做服务注册,我们并不会把 Nacos 集群中的所有服务器都配置在 Client 中,否则每次 Nacos 集群增加或删除了节点,我都要对所有 Client 做一次代码变更并重新发布。
常见的一个做法是提供一个 VIP URL 给到 Client,VIP URL 是一个虚拟 IP 地址,我们可以把真实的 Nacos 服务器地址列表“隐藏”在虚拟 IP 后面,客户端只需要连接到虚 IP 即可,由提供虚 IP 的组件负责将请求转发给背后的服务器列表。这样一来,即便 Nacos 集群机器数量发生了变动,也不会对客户端造成任何感知。提供虚 IP 的技术手段有很多,比如通过搭建 Nginx+LVS 或者 keepalived 技术实现高可用集群。

3 集成 Nacos--将服务提供者注册到 Nacos 服务器

接下来将动手集成优惠券平台项目到 Nacos 服务器,分为两个章节来介绍

在 Nacos 的地盘上,下游服务需要先将自己作为“服务提供者”注册到 Nacos,这个流程叫做“服务注册”;而上游服务作为“服务消费者”,需要到 Nacos 中获取可供调用的下游服务的列表,这个流程就是“服务发现”。

本章节将先从两个下游服务 coupon-template-serv 和 coupon-calculation-serv 开始,利用 Nacos 的服务注册功能,将服务提供者注册到 Nacos 服务器。下一章节展示作为服务消费者的 coupon-customer-serv 是如何通过服务发现机制向服务提供者发起调用。

在集成 Nacos 之前,我们需要先把 Nacos 的依赖项引入到项目中。

3.1 添加 Nacos 依赖项

Nacos 是 Sping Cloud Alibaba 项目的一款组件,在引入 Nacos 的依赖项之前,我们需要在项目的顶层 pom 中定义所要使用的 Spring Cloud 和 Spring Cloud Alibaba 的版本。

Spring Boot、Spring Cloud 和 Spring Cloud Alibaba 三者之间有严格的版本匹配关系,这三个组件的口味非常刁钻,只能和特定版本区间的搭档相互合作,一旦用错了版本,就会产生各种莫名其妙的兼容性问题。

Spring Boot 和 Spring Cloud 的版本匹配关系可以从Spring 社区网站获取,访问该网址后你会看到一串 JSON 文件,在这个文件中,你可以看到 Spring 官方给出的 Spring Cloud 最新分支所支持的 Spring Boot 版本范围。

另外,Spring Cloud Alibaba、Spring Boot 和 Spring Cloud 的匹配关系可以从 Spring Cloud Alibaba 的官方GitHub wiki 页中获取,下边是一些常用的版本绘制成的表格,你可以作为参考。

这里列出的组合都是经过兼容性测试的稳定版本,可以用在生产环境中。本项目选择 Spring Cloud 2020.0.1、Spring Cloud Alibaba 2021.1 和 Spring Boot 2.4.2 作为实战项目的依赖版本。

接下来,我们将 Spring Cloud Alibaba 和 Spring Cloud 的依赖项版本添加到顶层项目 yinyu-coupon 下的 pom.xml 文件中。

路径:pom.xml


<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2020.0.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2021.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
   </dependencies>   
   <!-- 省略部分代码 -->  
</dependencyManagement>

定义了组件的大版本之后,我们就可以直接把 Nacos 的依赖项加入到 coupon-template-serv 和 coupon-calculation-serv 的 impl 子模块,分别在这两个子模块的 pom.xml 文件中加入了 Nacos 的依赖项 spring-cloud-starter-alibaba-nacos-discovery。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

由于我已经将 Spring Cloud Alibaba 的依赖项版本加到了顶层项目 geekbang-coupon,因此,在添加 Nacos 依赖项到子模块的时候不需要特别指定 < version > 内容,当前子模块会尝试从父级项目读取正确的版本信息。

在添加完依赖项之后,我们就可以通过配置项开启 Nacos 的服务治理功能了。Nacos 通过自动装配流程(auto configuration)加载配置项并开启服务注册。这个功能可不是 Nacos 所独有的,Spring Cloud 各个组件都采用了自动装配器实现了轻量级的组件集成功能,你只需要几行配置,剩下的初始化工作都可以交给背后的自动装配器来实现。

3.2 Nacos 自动装配原理

在 Spring Cloud 稍早一些的版本中,我们需要在启动类上添加 @EnableDiscoveryClient 注解开启服务治理功能,而在新版本的 Spring Cloud 中,这个注解不再是一个必须的步骤,我们只需要通过配置项就可以开启 Nacos 的功能。那么 Nacos 是怎么在启动阶段自动加载配置项并开启相关功能的呢?这就要从 Spring Framework 的自动装配流程(Auto Configuration)说起了。

我们将 Nacos 依赖项添加到项目中,同时也引入了 Nacos 自带的自动装配器,比如下面这几个被引入的自动装配器就掌管了 Nacos 核心功能的初始化任务。

  • NacosDiscoveryAutoConfiguration:服务发现功能的自动装配器,它主要做两件事儿:加载 Nacos 配置项,声明 NacosServiceDiscovery 类用作服务发现;

  • NacosServiceAutoConfiguration:声明核心服务治理类 NacosServiceManager,它可以通过 service id、group 等一系列参数获取已注册的服务列表;

  • NacosServiceRegistryAutoConfiguration:Nacos 服务注册的自动装配器。

我们以 NacosDiscoveryAutoConfiguration 类为例,了解一下它是怎么样工作的,先来看一下它的源码。

@Configuration(proxyBeanMethods = false)
// 当spring.cloud.discovery.enabled=true时才生效
@ConditionalOnDiscoveryEnabled
// 当spring.cloud.nacos.discovery.enabled=true时生效
@ConditionalOnNacosDiscoveryEnabled
public class NacosDiscoveryAutoConfiguration {

   // 读取Nacos所有配置项并封装到NacosDiscoveryProperties中
   @Bean
   @ConditionalOnMissingBean
   public NacosDiscoveryProperties nacosProperties() {
      return new NacosDiscoveryProperties();
   }
 
   // 声明服务发现的功能类NacosServiceDiscovery
   @Bean
   @ConditionalOnMissingBean
   public NacosServiceDiscovery nacosServiceDiscovery(
         NacosDiscoveryProperties discoveryProperties,
         NacosServiceManager nacosServiceManager) {
      return new NacosServiceDiscovery(discoveryProperties, nacosServiceManager);
   }
}

NacosDiscoveryAutoConfiguration 自动装配器有两个开启条件,分别是 spring.cloud.discovery.enabled=true 和 spring.cloud.nacos.discovery.enabled=true。当我们引入 Nacos 的依赖项后,默认情况下这两个开关参数的值就已经是 True 了。也就是说,除非你主动关闭 Spring Cloud 和 Nacos 的服务发现开关,否则这个自动装配器就会自动执行加载。

接下来,我们来了解一下 NacosDiscoveryAutoConfiguration 中声明的两个方法,也就是 nacosProperties 方法和 nacosServiceDiscovery 方法都有什么功能。

在上面的源码中,我们看到,nacosProperties 方法返回了一个 NacosDiscoveryProperties 类,这个类是专门用来读取和封装 Nacos 配置项的类,它的源码如下:

// 定义了配置项读取的路径
@ConfigurationProperties("spring.cloud.nacos.discovery")
public class NacosDiscoveryProperties {
 // 省略类属性
 // 这里定义的类属性和接下来我们要介绍的配置项是一一对应的
}

NacosDiscoveryProperties 类通过 ConfigurationProperties 注解从 spring.cloud.nacos.discovery 路径下获取配置项,Spring 框架会自动将这些配置项解析到 NacosDiscoveryProperties 类定义的类属性中。这样一来 Nacos 就完成了配置项的加载,在其它业务流程中,只需要注入 NacosDiscoveryProperties 类就可以读取 Nacos 的配置参数。

NacosDiscoveryAutoConfiguration 中的另一个方法 nacosServiceDiscovery 声明了一个服务发现的功能类 NacosServiceDiscovery,它的核心方法的源码如下:

public class NacosServiceDiscovery {
   // 封装了Nacos配置项的类
   private NacosDiscoveryProperties discoveryProperties;
   // 另一个自动装配器声明的核心服务治理类
   private NacosServiceManager nacosServiceManager;

   // 根据服务名称获取所有已注册服务
   public List<ServiceInstance> getInstances(String serviceId) throws NacosException {
      String group = discoveryProperties.getGroup();
      List<Instance> instances = namingService().selectInstances(serviceId, group,
            true);
      return hostToServiceInstanceList(instances, serviceId);
   }

   // 返回所有服务的服务名称
   public List<String> getServices() throws NacosException {
      String group = discoveryProperties.getGroup();
      ListView<String> services = namingService().getServicesOfServer(1,
            Integer.MAX_VALUE, group);
      return services.getData();
   }
   // 省略部分代码...
}

通过 NacosServiceDiscovery 暴露的方法,我们就能够根据 serviceId(注册到 nacos 的服务名称)查询到可用的服务实例,获取到服务实例列表之后,调用方就可以发起远程服务调用了。

3.3 添加 Nacos 配置项

Nacos 的配置项包含了服务注册参数与各项运行期参数,你可以使用标准的 Spring Boot 配置管理的方式设置 Nacos 的运行参数。

以我们今天要改造的服务 coupon-template-impl 为例,我们先来看一下 application.yml 文件中添加了哪些 Nacos 核心配置。Nacos 相关的配置项位于 spring.cloud.nacos 路径下,本项目配置的 Nacos 常用参数如下:

spring:
  cloud:
    nacos:
      discovery:
        # Nacos的服务注册地址,可以配置多个,逗号分隔
        server-addr: localhost:8848
        # 服务注册到Nacos上的名称,一般不用配置
        service: coupon-customer-serv
        # nacos客户端向服务端发送心跳的时间间隔,时间单位其实是ms
        heart-beat-interval: 5000
        # 服务端没有接受到客户端心跳请求就将其设为不健康的时间间隔,默认为15s
        # 注:推荐值该值为15s即可,如果有的业务线希望服务下线或者出故障时希望尽快被发现,可以适当减少该值
        heart-beat-timeout: 20000
        # 元数据部分 - 可以自己随便定制
        metadata:
          mydata: abc
        # 客户端在启动时是否读取本地配置项(一个文件)来获取服务列表
        # 注:推荐该值为false,若改成true。则客户端会在本地的一个
        # 文件中保存服务信息,当下次宕机启动时,会优先读取本地的配置对外提供服务。
        naming-load-cache-at-start: false
        # 命名空间ID,Nacos通过不同的命名空间来区分不同的环境,进行数据隔离,
        namespace: dev
        # 创建不同的集群
        cluster-name: Cluster-A
        # [注意]两个服务如果存在上下游调用关系,必须配置相同的group才能发起访问
        group: myGroup
        # 向注册中心注册服务,默认为true
        # 如果只消费服务,不作为服务提供方,倒是可以设置成false,减少开销
        register-enabled: true

深入了解下上面的每个参数,以及它们背后的一些使用场景,掌握了这些 Nacos 的核心配置项,你就可以平稳驾驭 Nacos 服务治理了。

在这些参数中,Namespace 和 Group 经常搭配在一块来使用,它俩在功能上也有相似之处,在实际应用中经常傻傻分不清楚它俩的用途。为了帮你更好地理解这两个参数,我来带你深入了解下这两个属性各自的使用场景。

Namespace 可以用作环境隔离或者多租户隔离,其中:

  • 环境隔离:比如设置三个命名空间 production、pre-production 和 dev,分别表示生产环境、预发环境和开发环境,如果一个微服务注册到了 dev 环境,那么他无法调用其他环境的服务,因为服务发现机制只会获取到同样注册到 dev 环境的服务列表。如果未指定 namespace 则服务会被注册到 public 这个默认 namespace 下

  • 多租户隔离:即 multi-tenant 架构,通过为每一个用户提供独立的 namespace 以实现租户与租户之间的环境隔离

Group 的使用场景非常灵活,简单列举几个:

  • 环境隔离:在多租户架构之下,由于 namespace 已经被用于租户隔离,为了实现同一个租户下的环境隔离,你可以使用 group 作为环境隔离变量

  • 线上测试:对于涉及到上下游多服务联动的场景,我将线上已部署的待上下游测服务的 group 设置为“group-A”,由于这是一个新的独立分组,所以线上的用户流量不会导向到这个 group。这样一来,开发人员就可以在不影响线上业务的前提下,通过发送测试请求到“group-A”的机器完成线上测试

  • 单元封闭:什么是单元封闭呢?为了保证业务的高可用性,通常我们会把同一个服务部署在不同的物理单元(比如张北机房、杭州机房、上海机房),当某个中心机房出现故障的时候,我们可以在很短的时间内把用户流量切入其他单元机房。由于同一个单元内的服务器资源通常部署在同一个物理机房,因此本单元内的服务调用速度最快,而跨单元的服务调用将要承担巨大的网络等待时间。这种情况下,我们可以为同一个单元的服务设置相同的 group,使微服务调用封闭在当前单元内,提高业务响应速度

接下来,我们只需要启动 Nacos 注册中心,尝试将改造好的 coupon-template-serv 和 coupon-calculation-serv 注册到 Nacos 服务器,验证 Nacos 的服务注册功能。

3.4 验证 Nacos 服务注册功能

首先,我们需要开启 Nacos 服务器,可以参考2.5章节,以单机模式或者集群模式开启 Nacos 服务器。

接下来,我们需要在 Nacos 上创建若干个 namespace(命名空间),你需在下图右侧导航栏找到“命名空间”页面,进入该页面点击“新增命名空间”按钮,分别创建三个不同的环境:production、pre-production 和 dev,用来表示生产环境、预发环境和开发环境。在创建 namespace 的过程中,一定要保证命名空间的 ID 和项目中的 namespace 属性是一致的。

创建好命名空间之后,我们就可以在本地尝试启动 coupon-template-impl 和 coupon-calculation-impl 两个服务的 Main 方法。在这个过程中,你需要注意控制台打印出来的日志是否包含错误信息,如果有异常抛出则要 case by case 仔细分析。

如果应用启动一切正常,那么你就可以在下图的 Nacos 的服务列表中找到这两个服务。记得要选中你在应用中配置的 namespace(下图上方红框处标记的“开发环境”)才能看到对应的服务注册情况。

好,到这里,我们的服务注册就已经完成了。

如果你是第一次接触 Nacos 注册中心,可能会不小心踩到一些小坑。比如说“我的服务为什么注册不上”就是一个常见的问题,这时候有几个排查问题的方向:

  1. 你可以先查看下你的应用启动日志,看启动过程中是否有向 Nacos 服务器发起注册请求,如果没有相关日志,那极大可能是你的服务注册功能被手动关闭了,或者没有引入 Nacos 的依赖项以启动自动装配功能

  1. 如果日志中抛出了异常,那么要 case-by-case 去检查异常发生的原因,是 Nacos 服务器地址不正确,还是参数设置不正确导致的

  1. 当日志一切正常,可服务列表还看不到数据,那么你就要看一下是不是你开启了多个 Nacos 服务器,而服务器之间没有通过集群模式启动注册表同步

  1. 另外你还要特别注意下配置参数中是否指定了 Namespace,如果是的话,那么服务只会出现在服务列表页面中对应的 Namespace 标签下

4 集成 Nacos--通过服务发现机制向服务提供者发起调用

coupon-template-serv 和 coupon-calculation-serv 这两个服务已经做了微服务化改造,通过服务注册流程将它们注册到了 Nacos Server。这两个服务是以服务提供者的身份注册的,它们之间不会发生相互调用。为了发起一次完整的服务调用请求,我们还需要构建一个服务消费者去访问 Nacos 上的已注册服务。

coupon-customer-serv 就扮演了服务消费者的角色,它需要调用 coupon-template-serv 和 coupon-calculation-serv 完成自己的业务流程。今天我们就来动手改造 coupon-customer-serv 服务,借助 Nacos 的服务发现功能从注册中心获取可供调用的服务列表,并发起一个远程服务调用。

本章节将如何使用 Webflux 发起远程调用,并熟练掌握如何搭建一套基于 Nacos 的服务治理方案。

4.1 添加 Nacos 依赖项和配置信息

在开始写代码之前,需要将以下依赖项添加到 customer-customer-impl 子模块的 pom.xml 文件中。

<!-- Nacos服务发现组件 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!-- 负载均衡组件 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<!-- webflux服务调用 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>    

第一个依赖项你一定很熟悉了,它是 Nacos 服务治理的组件,后面两个依赖项你简单介绍一下:

  • spring-cloud-starter-loadbalancer:Spring Cloud 御用负载均衡组件 Loadbalancer,用来代替已经进入维护状态的 Netflix Ribbon 组件。下一章节深入了解 Loadbalancer 的功能,今天我们只需要简单了解下它的用法就可以了

  • spring-boot-starter-webflux:Webflux 是 Spring Boot 提供的响应式编程框架,响应式编程是基于异步和事件驱动的非阻塞程序。Webflux 实现了 Reactive Streams 规范,内置了丰富的响应式编程特性。本项目将用 Webflux 组件中一个叫做 WebClient 的小工具发起远程服务调用

添加好这两个依赖之后,你还需要做一番清理门户的工作,让 coupon-customer-serv 和另外两个微服务之间划清界限。

  • 删除实现层依赖:从 coupon-customer-impl 的依赖项中删除 coupon-template-impl 和 coupon-calculation-impl

  • 添加接口层依赖:在 coupon-customer-impl 的依赖项中添加 coupon-template-api 和 coupon-calculation-api

这样做的目的是划清服务之间的依赖关系,由于 coupon-customer-serv 是一个独立的微服务,它不需要将其他服务的“代码逻辑实现层”打包到自己的启动程序中一同启动。如果某个应用场景需要调用其它微服务,我们应该使用远程接口调用的方式对目标服务发起请求。因此,我们需要将对应接口的 Impl 实现层从 coupon-customer-impl 的依赖中删除,同时引入 API 层的依赖,以便构造请求参数和接收服务响应。

接下来,你还需要在 coupon-customer-impl 项目的 application.yml 文件中添加 Nacos 的配置项,我们直接从 coupon-template-impl 的配置项里抄作业就好了。

修改完依赖项和配置信息之后,你的代码一定冒出了不少编译错误。因为尽管我们已经将 coupon-template-impl 和 coupon-calculation-impl 依赖项删除,但 coupon-customer-impl 中的 CouponCustomerServiceImpl 仍然使用 Autowire 注入的方式调用本地服务。

所以接下来,我们就需要对调用层做一番改造,将 Autowire 注入本地服务的方式,替换为使用 WebClient 发起远程调用。

4.2 添加 WebClient 对象

为了可以用 WebClient 发起远程调用,你还需要在 Spring 上下文中构造一个 WebClient 对象。标准的做法是创建一个 Configuration 类,并在这个类中通过 @Bean 注解创建需要的对象。

所以需要在 coupon-customer-impl 子模块下创建com.yinyu.coupon.customer.Configuration 类,并声明 WebClient 的 Builder 对象。

package com.yinyu.coupon.customer;
// Configuration注解用于定义配置类
// 类中定义的Bean方法会被AnnotationConfigApplicationContext和AnnotationConfigWebApplicationContext扫描并初始化
@org.springframework.context.annotation.Configuration
public class Configuration {

    @Bean
    @LoadBalanced
    public WebClient.Builder register() {
        return WebClient.builder();
    }

}
  • @Configuration 注解:定义一个配置类。在 Configuration 类中定义的 @Bean 注解方法会被 AnnotationConfigApplicationContext 或者 AnnotationConfigWebApplicationContext 扫描并在上下文中进行构建

  • @Bean 注解:声明一个受 Spring 容器托管的 Bean

  • @LoadBalanced 注解:为 WebClient.Build 构造器注入特殊的 Filter,实现负载均衡功能,下一章节会详细解释负载均衡的知识点。今天咱就好读书不求甚解就可以了,只需要知道这个注解的作用是在远程调用发起之前选定目标服务器地址

WebClient 创建好了之后,你就可以在业务类中注入 WebClient 对象,并发起服务调用了。接下来,将 CouponCustomerServiceImpl 里的本地方法调用替换成 WebClient 远程调用。

4.3 使用 WebClient 发起远程方法调用

首先,我们将 Configuration 类中声明的 WebClient 的 Builder 对象注入到 CouponCustomerServiceImpl 类中,两行代码简单搞定:

@Autowired
private WebClient.Builder webClientBuilder;

接下来,我们开始改造第一个接口 requestCoupon。需要将 requestCoupon 接口实现的第一行代码中的 CouponTemplateService 本地调用替换为 WebClient 远程调用。下面是改造之前的代码。

CouponTemplateInfo templateInfo = templateService.loadTemplateInfo(request.getCouponTemplateId());

远程接口调用的代码改造可以通过 WebClient 提供的“链式编程”轻松实现,下面是代码的完整实现。

CouponTemplateInfo templateInfo = webClientBuilder.build()
        .get()
        .uri("http://coupon-template-serv/template/getTemplate?id=" + request.getCouponTemplateId())
        .retrieve()
        .bodyToMono(CouponTemplateInfo.class)
        .block();

在这段代码中,我们应用了几个关键方法发起远程调用。

  • get:指明了 Http Method 是 GET,如果是其他请求类型则使用对应的 post、put、patch、delete 等方法;

  • uri:指定了访问的请求地址;

  • retrieve + bodyToMono:指定了 Response 的返回格式;

  • block:发起一个阻塞调用,在远程服务没有响应之前,当前线程处于阻塞状态。

在使用 uri 指定调用服务的地址时,你并不需要提供目标服务的 IP 地址和端口号,只需要将目标服务的服务名称 coupon-template-serv 告诉 WebClient 就好了。Nacos 在背后会通过服务发现机制,帮你获取到目标服务的所有可用节点列表。然后,WebClient 会通过负载均衡过滤器,从列表中选取一个节点进行调用,整个流程对开发人员都是透明的、无感知的

你可以看到,在代码中我使用了 retrieve + bodyToMono 的方式接收 Response 响应,并将其转换为 CouponTemplateInfo 对象。在这个过程中,我只接收了 Response 返回的 Body 内容,并没有对 Response 中包含的其它字段进行处理。

如果你需要获取完整的 Response,包括 Http status、headers 等额外数据,就可以使用 retrieve + toEntity 的方式,获取包含完整 Response 信息的 ResponseEntity 对象。示例如下,你可以自己在项目中尝试这种调用方式,体验下 toEntity 和 bodyToMono 的不同之处。

Mono<ResponseEntity<CouponTemplateInfo>> entityMono = client.get()
  .uri("http://coupon-template-serv/template/xxxx")
  .accept(MediaType.APPLICATION_JSON)
  .retrieve()
  .toEntity(CouponTemplateInfo.class);

WebClient 使用了一种链式编程的风格来构造请求对象,本人在 访问 Web 资源( 借助 RestTemplate 或 WebClient )也介绍了 WebClient ,链式编程就是我们熟悉的 Builder 建造者模式。仔细观察你会发现,大部分开源应用都在使用这种设计模式简化对象的构建。如果你需要在自己的项目中使用 Builder 模式,你可以借助 Lombok 组件的 @Builder 注解来实现。如果你对此感兴趣,可以自行了解 Lombok 组件的相关用法。

到这里,我们已经完成了 requestCoupon 方法的改造,接下来我们趁热打铁,动手去替换 findCoupon 和 placeOrder 方法中的本地调用。

在 findCoupon 方法中,我们需要调用 coupon-template-serv 的服务批量查询 CouponTemplate。这里的方式和前面一样,我使用 WebClient 对本地调用进行了替换,你可以参考下面的源码。

Map<Long, CouponTemplateInfo> templateMap = webClientBuilder.build().get()
        .uri("http://coupon-template-serv/template/getBatch?ids=" + templateIds)
        .retrieve()
        .bodyToMono(new ParameterizedTypeReference<Map<Long, CouponTemplateInfo>>() {})
        .block();

由于方法的返回值不是一个标准的 Json 对象,而是 Map<Long, CouponTemplateInfo> 类型,因此你需要构造一个 ParameterizedTypeReference 实例丢给 WebClient,告诉它应该将 Response 转化成什么类型。

现在,我们还剩下一个关键方法没有改造,那就是 placeOrder,它调用了 coupon-calculation-serv 计算最终的订单价格,你可以参考以下源码。

ShoppingCart checkoutInfo = webClientBuilder.build()
        .post()
        .uri("http://coupon-calculation-serv/calculator/checkout")
        .bodyValue(order)
        .retrieve()
        .bodyToMono(ShoppingCart.class)
        .block();

和前面几处改造不同的是,这是一个 POST 请求,因此在使用 webClient 构造器的时候我调用了 post 方法;除此之外,它还需要接收订单的完整信息作为请求参数,因此我这里调用了 bodyValue 方法,将封装好的 Order 对象塞了进去。在 coupon-customer-impl 中剩下的一些远程调用方法,就留给你来施展拳脚做改造了,也可在远程仓库查看。

到这里,我们整个 Nacos 服务改造就已经完成了。你可以在本地依次启动 coupon-template-serv、coupon-calculation-serv 和 coupon-customer-serv。启动成功后,再到 Nacos 控制台查看这三个服务是否已经全部注册到了 Nacos。

如果你是以集群模式启动了多台 Nacos 服务器,那么即便你在实战项目中只配置了一个 Nacos URL,并没有使用虚拟 IP 搭建单独的集群地址,注册信息也会传播到 Nacos 集群中的所有节点。

现在,动手搭建一套基于 Nacos 的服务治理方案对你而言一定不是难事儿了。动手能力是有了,但我们也不能仅仅满足于学会使用一套技术,你必须要深入到技术的具体实现方案,才能从中汲取到养分,为你将来的技术方案设计提供参考。

那么接下来,了解一下 Nacos 服务发现的底层实现,学习一下 Client 端是通过什么途径从 Nacos Server 获取服务注册表的。

4.4 Nacos 服务发现底层实现

Nacos Client 通过一种主动轮询的机制从 Nacos Server 获取服务注册信息,包括地址列表、group 分组、cluster 名称等一系列数据。简单来说,Nacos Client 会开启一个本地的定时任务,每间隔一段时间,就尝试从 Nacos Server 查询服务注册表,并将最新的注册信息更新到本地。这种方式也被称之为“Pull”模式,即客户端主动从服务端拉取的模式。

负责拉取服务的任务是 UpdateTask 类,它实现了 Runnable 接口。Nacos 以开启线程的方式调用 UpdateTask 类中的 run 方法,触发本地的服务发现查询请求。

UpdateTask 这个类隐藏得非常深,它是 HostReactor的一个内部类,带你看一下经过详细注释的代码走读:

public class UpdateTask implements Runnable {

    // ....省略部分代码
    
    // 获取服务列表
    @Override
    public void run() {
        long delayTime = DEFAULT_DELAY;
        
        try {
            // 根据service name获取到当前服务的信息,包括服务器地址列表
            ServiceInfo serviceObj = serviceInfoMap
                .get(ServiceInfo.getKey(serviceName, clusters));
            
            // 如果为空,则重新拉取最新的服务列表
            if (serviceObj == null) {
                updateService(serviceName, clusters);
                return;
            }
            
            // 如果时间戳<=上次更新的时间,则进行更新操作
            if (serviceObj.getLastRefTime() <= lastRefTime) {
                updateService(serviceName, clusters);
                serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
            } else {
                // 如果serviceObj的refTime更晚,
                // 则表示服务通过主动push机制已被更新,这时我们只进行刷新操作
                refreshOnly(serviceName, clusters);
            }
            // 刷新服务的更新时间
            lastRefTime = serviceObj.getLastRefTime();
            
            // 如果订阅被取消,则停止更新任务
            if (!notifier.isSubscribed(serviceName, clusters) && !futureMap
                    .containsKey(ServiceInfo.getKey(serviceName, clusters))) {
                // abort the update task
                NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters);
                return;
            }
            // 如果没有可供调用的服务列表,则统计失败次数+1
            if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
                incFailCount();
                return;
            }
            // 设置延迟一段时间后进行查询
            delayTime = serviceObj.getCacheMillis();
            // 将失败查询次数重置为0
            resetFailCount();
        } catch (Throwable e) {
            incFailCount();
            NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
        } finally {
            // 设置下一次查询任务的触发时间
            executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60), TimeUnit.MILLISECONDS);
        }
    }
}

在 UpdateTask 的源码中,它通过调用 updateService 方法实现了服务查询和本地注册表更新,在每次任务执行结束的时候,在结尾处它通过 finally 代码块设置了下一次 executor 查询的时间,周而复始循环往复。

以上,就是 Nacos 通过 UpdateTask 来查询服务端注册表的底层原理了。

总结

到这里,我们就完成了 yinyu-coupon-center 的 Nacos 服务治理改造目前已经完整搭建了整个 Nacos 服务治理链路。在这条链路中,通过服务注册流程实现了服务提供者的注册,又通过服务发现机制让服务消费者获取服务注册信息,还能通过 WebClient 发起远程调用。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值