nacos是什么

Nacos 注册中心用于统一保存和管理微服务实例信息,让服务可以通过服务名找到其他服务,并结合健康检查和负载均衡完成可靠的服务调用。

nacos的工作原理

在这里插入图片描述

以上面单机版 Nacos 为例,假设系统中存在一个服务提供者 (Provider)有一个订单服务 order-service,以及一个服务消费者(Consumer)有一个用户服务 user-service。用户服务需要通过远程调用访问订单服务。

  1. 服务提供者 Provider 启动并读取注册配置

    当订单服务 Provider 启动时,Spring Boot 会先完成应用初始化,同时读取项目中的 Nacos 注册中心配置

    这些配置通常包括 Nacos Server 地址、服务名称、命名空间、分组、集群名称等信息。例如,订单服务的服务名称为 order-service,Nacos 地址为 127.0.0.1:8848,命名空间为 prod,分组为 DEFAULT_GROUP。

    除了项目中配置的信息,Nacos 客户端还会获取当前服务实例自身的运行信息,例如当前服务器 IP 地址为 192.168.1.10,应用端口为 8081,实例权重为 1.0,实例是否启用以及实例所属集群等。

    此时,Nacos 客户端还可以携带一些自定义元数据。元数据就是对服务实例的补充说明,例如:

    version=v1,表示当前实例属于 v1 版本;
    environment=prod,表示当前实例属于生产环境;
    region=tokyo,表示当前实例部署在东京区域;
    protocol=http,表示当前实例使用 HTTP 协议提供服务。
    

    因此,订单服务启动后准备注册的信息大致包括:服务名称 order-service、IP 地址 192.168.1.10、端口 8081、权重 1.0、集群名称、命名空间、分组以及版本、环境、区域等元数据。

  2. Provider 向 Nacos 服务注册中心发送注册请求
    订单服务收集完自身信息后,内置的 Nacos 客户端会向 Nacos Server 发送服务注册请求。

    这个注册请求的核心含义就是告诉 Nacos:

    “我是一台名为 order-service 的服务实例,我的 IP 地址是 192.168.1.10,端口是 8081,我当前可以对外提供服务,请把我的地址记录下来。”

    Provider 不需要由开发人员手动调用注册接口。只要项目正确引入 Nacos 服务发现依赖,并配置好 Nacos Server 地址和服务名称,应用启动时,Nacos 客户端通常就会自动完成服务注册。

  3. Nacos 服务注册中心接收并校验注册信息

    Nacos Server 收到注册请求后,会先校验注册信息是否完整,例如检查服务名称是否为空、IP 地址是否合法、端口是否合法、命名空间是否存在以及实例参数是否正确。

    校验通过后, Nacos 会根据命名空间、分组和服务名称确定这个实例属于哪个服务。

    服务通常不是只通过服务名称进行区分,而是可以理解为由以下几个信息共同确定:

    命名空间 + 分组 + 服务名称。

    例如,同样都是 order-service,开发环境和生产环境可以放在不同的命名空间中。这样开发环境的消费者就不会错误地发现生产环境中的订单服务。

  4. Nacos 创建服务并将实例加入服务注册表

    如果 Nacos 中还不存在 order-service,Nacos 会先创建对应的服务记录,然后将 192.168.1.10:8081 这个实例加入该服务的实例列表。

    如果 order-service 已经存在,Nacos 就直接在这个服务下面新增一个实例。

    例如,订单服务部署了三台服务器,那么 Nacos 中可以形成这样的实例关系:

    order-service 服务下面存在 192.168.1.10:8081、192.168.1.11:8081 和 192.168.1.12:8081 三个实例。

    它们的服务名称完全相同,但是 IP 地址不同。对于消费者来说,这三台服务器都能够提供订单服务。

    Nacos 会维护每个实例的 IP、端口、权重、健康状态、启用状态、集群名称和元数据等信息。

    在运行过程中,服务注册表通常会保存在 Nacos Server 的内存结构中,以便快速完成服务查询。部分需要长期保存的数据还会根据 Nacos 的部署方式和实例类型写入持久化存储。

  5. Nacos 返回服务注册结果
    Nacos 成功保存实例后,会向订单服务返回注册成功结果。

    订单服务收到成功响应后,就表示当前服务实例已经进入 Nacos 服务注册表,其他服务可以通过 order-service 这个服务名称发现它

    如果注册失败,例如 Nacos Server 无法连接、配置错误或者网络异常,Nacos 客户端通常会记录错误日志,并根据客户端机制尝试重新连接和注册。

  6. Provider 注册成功后开始心跳续约或连接保活

    服务注册成功并不代表注册过程永久结束。

    因为 Nacos 需要知道订单服务是否还在正常运行,所以 Provider 还要持续向 Nacos 证明自己处于存活状态。

    在常见的临时实例模式下,Nacos 客户端会定期发送心跳,或者通过长连接保活机制维持客户端与 Nacos Server 之间的连接。

    心跳中通常会携带服务名称、实例 IP、端口、集群、命名空间等信息。

    每次 Nacos 收到心跳后,都会更新该实例最近一次存活时间,并将该实例保持为健康状态。

    心跳续约可以理解为订单服务定期对 Nacos 说:“我是 192.168.1.10:8081,我还在正常运行,请继续把我保留在可用实例列表中。”

  7. Nacos 根据心跳和健康检查维护实例状态

    如果 Nacos 能够持续收到订单服务的心跳,或者客户端长连接一直正常,Nacos 就认为该实例仍然健康。

    如果订单服务突然宕机、服务器断电、网络中断或者应用进程被终止,Nacos 将无法继续收到该实例的心跳。

    当实例超过一定时间没有心跳后,Nacos 会认为该实例可能已经出现故障,并将实例状态由健康修改为不健康。

    实例被标记为不健康后,一般不会再被作为正常实例提供给消费者,防止消费者继续调用已经故障的服务器。

    如果实例在更长时间内仍然没有恢复,临时实例可能会被 Nacos 从服务注册表中删除。

    整个过程可以理解为:正常接收心跳时,实例保持健康;心跳超时时,实例被标记为不健康;长时间没有恢复时,实例可能被清理。

    除了由客户端主动发送心跳,Nacos 在部分模式下也可以通过主动探测等方式检查实例是否正常。

  8. 服务消费者 Consumer 启动并订阅目标服务

    用户服务 user-service 启动时,也会连接 Nacos Server。

    但是用户服务在调用订单服务时,并不需要提前知道订单服务部署在哪一台服务器,也不需要在配置文件中写死 192.168.1.10:8081。

    用户服务只需要知道目标服务的名称是 order-service。

    用户服务中的 Nacos 客户端会向 Nacos 服务发现模块查询或订阅 order-service,表示:

    “我需要调用 order-service,请告诉我这个服务目前有哪些可以使用的实例,并在实例发生变化时通知我。”

  9. Consumer 发起远程调用时,根据服务名称查找实例

    当用户服务准备调用订单服务时,程序中使用的是订单服务的服务名称,而不是固定 IP 地址。

    例如,消费者要调用的逻辑目标是:http://order-service/order/query

    这里的 order-service 并不是一个真实的域名,而是注册到 Nacos 中的服务名称。

    消费者侧的服务发现组件会根据这个服务名称查找对应的实例列表。

    如果消费者本地还没有 order-service 的实例信息,或者本地缓存需要更新,Nacos 客户端就会向 Nacos Server 查询这个服务。

    需要注意的是,消费者通常不是每发送一次业务请求都访问一次 Nacos。第一次查询成功后,实例列表会被保存到消费者本地缓存中,后续调用通常直接使用本地实例列表。

  10. Nacos 服务发现模块查询服务注册表

    Nacos 收到 order-service 的查询请求后,会从服务注册表中找到这个服务下面的全部实例。

    假设此时存在以下三个实例:

    192.168.1.10:8081,健康状态为正常;
    192.168.1.11:8081,健康状态为正常;
    192.168.1.12:8081,由于心跳超时,健康状态为异常。
    

    Nacos 会根据消费者所在的命名空间、分组、集群以及查询条件进行筛选。

    通常只有命名空间和分组匹配、实例已经启用并且健康状态正常的实例,才适合返回给消费者。

    因此,Nacos 最终可能只返回 192.168.1.10:8081 和 192.168.1.11:8081 两个健康实例,而不会把已经异常的 192.168.1.12:8081 作为正常实例提供给消费者。

  11. Nacos 将可用实例列表返回给 Consumer

    Nacos 会将查询到的服务实例列表返回给用户服务。

    返回的信息不仅包括 IP 地址和端口,还可能包括实例权重、健康状态、集群名称、启用状态以及实例元数据。

    例如,Nacos 返回两个订单服务实例:

    第一个实例的地址为 192.168.1.10:8081,版本为 v1,权重为 1.0;
    
    第二个实例的地址为 192.168.1.11:8081,版本为 v1,权重为 2.0。
    

    这些信息可以帮助消费者进行实例选择。例如,消费者可以优先选择相同集群的实例,也可以根据权重让性能更好的服务器承担更多请求。

  12. Consumer 将实例列表保存到本地缓存

    用户服务收到实例列表后,会将这些实例保存到本地缓存中。

    本地缓存非常重要,因为消费者调用订单服务的频率可能很高。如果每一次调用都先访问 Nacos 查询实例,会产生大量额外网络请求,也会给 Nacos Server 带来较大压力。

    保存本地缓存后,用户服务后续调用订单服务时,可以直接从内存中获取 order-service 的实例列表。

    本地缓存还有一个作用:如果 Nacos Server 短时间内出现故障,消费者仍然可以暂时使用之前缓存的实例地址继续调用服务。

    但是本地缓存中的实例可能不是永远有效的,因此消费者还需要订阅服务变化,并在实例发生变化时及时更新缓存。

  13. Consumer 使用负载均衡选择一个 Provider 实例

    用户服务得到两个订单服务实例后,不能同时把一个请求发送给所有实例,而是需要通过负载均衡算法选择其中一个。

    常见的负载均衡方式包括轮询、随机、加权随机、加权轮询以及优先选择同集群实例等。

    例如,当前存在:

    192.168.1.10:8081,权重为 1.0;
    
    192.168.1.11:8081,权重为 2.0。
    

    那么负载均衡组件可能让第二个实例承担更多请求

    假设本次请求最终选择了 192.168.1.10:8081,消费者就会把原来的逻辑地址:http://order-service/order/query转换为真实地址:http://192.168.1.10:8081/order/query

  14. Consumer 直接向 Provider 发送业务请求
    选出具体实例后,用户服务会直接向订单服务发送 HTTP、RPC 或其他协议的远程调用请求。此时实际的业务调用链路是:Consumer 直接调用 Provider。

    Nacos 不负责转发业务请求,也不会成为每一次业务请求的中转站。

    Nacos 只负责保存服务地址、发现服务实例、维护健康状态以及通知实例变化。

    因此,Nacos 参与的是“调用之前如何找到目标服务”, 而不是“替消费者转发业务请求”。

    如果每个请求都必须经过 Nacos 转发,Nacos 就会成为整个系统的流量瓶颈。实际上,消费者拿到地址后会直接访问服务提供者。

  15. Provider 实例上线、下线或故障时,Nacos 更新注册表
    在系统运行过程中,订单服务的实例数量可能发生变化。

    例如,系统访问量增加时,新启动了一台订单服务 192.168.1.13:8081。这个新实例启动后会按照前面的注册流程,将自身信息注册到 Nacos。Nacos 会把它加入 order-service 的实例列表。

    如果某台订单服务正常关闭,Nacos 客户端可以在应用关闭前主动向 Nacos 发送注销请求。Nacos 收到注销请求后,会将该实例从服务注册表中移除。

    如果某台订单服务突然宕机,来不及主动注销,Nacos 就会通过心跳超时或连接断开检测到异常,然后将其标记为不健康,并在必要时删除。

    因此,服务注册表不是固定不变的,而是会随着服务实例的启动、停止和故障不断更新。

  16. Nacos 将服务实例变化通知给 Consumer

    用户服务在查询 order-service 后,通常还会订阅这个服务的变化。

    当订单服务增加新实例、实例主动下线、实例心跳超时、健康状态变化、权重变化或者元数据变化时,Nacos 会更新内部服务注册表。

    注册表变化后,Nacos 会通过服务变更通知机制告诉已经订阅 order-service 的消费者。

    消费者收到通知后,会重新获取最新实例列表,或者使用通知中携带的新实例信息更新本地缓存。

    例如,原来本地缓存中存在: 192.168.1.10:8081 和 192.168.1.11:8081。

    当 192.168.1.10:8081 宕机后,Nacos 将其标记为不健康,并通知用户服务。

    用户服务更新本地缓存后,只保留 192.168.1.11:8081,后续请求就不会继续发送给已经宕机的实例。

    通过这种机制,即使微服务实例不断扩容、缩容或发生故障,消费者也能及时获得最新的可用服务地址。

  17. 管理员通过 Nacos 控制台管理服务

    除了 Provider 和 Consumer,管理员还可以通过 Nacos 控制台查看和管理服务。

    管理员可以在控制台中查看当前注册了哪些服务、每个服务有多少实例、实例的 IP 和端口、实例是否健康、实例权重以及实例元数据等内容。

    管理员也可以手动修改实例权重、将实例设置为不启用,或者查看服务所在的命名空间和分组。

    控制台本身并不是独立保存这些数据,而是调用 Nacos Server 的管理功能,对服务注册中心中的数据进行查询和修改。

  18. 管理员通过 Nacos 配置中心发布配置

    除了服务注册和服务发现,Nacos 还可以作为配置中心使用。

    管理员可以通过 Nacos 控制台创建配置。一份配置通常由 Namespace、Group 和 Data ID 共同确定。例如:

    Namespace 为 prod;
    
    Group 为 DEFAULT_GROUP;
    
    Data ID 为 order-service.yaml。
    

    配置内容可能为:

    order.timeout=3000,表示订单接口调用超时时间为 3000 毫秒;
    
    order.maxRetry=3,表示订单调用失败后最多重试 3 次;
    
    order.discountEnabled=true,表示是否开启优惠功能。
    

    管理员点击发布后,控制台会将配置提交给 Nacos Server。

  19. Nacos 配置管理模块保存配置

    Nacos 配置管理模块收到配置后,会校验 Data ID、Group、Namespace 和配置内容。

    校验通过后,Nacos 会保存配置内容,并记录配置的修改时间、版本信息和内容摘要等数据。

    配置数据通常会写入嵌入式数据库或 MySQL 等持久化存储中,防止 Nacos Server 重启后配置丢失。

    同时,Nacos 还可以在内存或本地文件中保存部分配置缓存,提高配置查询速度。

    Nacos 也会记录配置变更日志,方便管理员查看配置历史或者在配置错误时进行回滚。

  20. 微服务启动时从 Nacos 获取配置

    订单服务启动时,除了向 Nacos 注册服务,还可以从 Nacos 配置中心获取远程配置。

    订单服务会根据 Namespace、Group 和 Data ID 查找自己的配置,例如获取 prod 命名空间、DEFAULT_GROUP 分组下的 order-service.yaml。

    Nacos Server 查询到配置后,将配置内容返回给订单服务。

    订单服务收到配置后,会把这些配置加载到 Spring 环境中,供项目中的 Bean 和业务代码使用。

    这样,数据库连接参数、接口超时时间、业务开关等内容就不需要全部写死在应用本地配置文件中,而可以由 Nacos 统一管理。

  21. 微服务监听 Nacos 配置变化

    订单服务获取配置后,还会继续监听这份配置是否发生变化。

    假设原来的超时时间为:order.timeout=3000。

    管理员在 Nacos 控制台中将其修改为:order.timeout=5000。

    配置发布后,Nacos 会发现配置内容发生变化,并通知正在监听这份配置的订单服务。

    订单服务收到配置变化通知后,会重新从 Nacos 拉取最新配置,然后更新本地配置内容。

    对于支持动态刷新的属性,新配置可以在应用不重启的情况下生效。对于不支持动态刷新的 Bean 或配置,可能仍然需要重新创建 Bean 或重启应用,具体取决于项目中的实现方式。

  22. Nacos 对配置变化进行版本和日志管理

    每次管理员发布配置时,Nacos 都可以记录本次变更。

    记录内容可能包括修改前的配置、修改后的配置、修改时间、操作人员和配置版本等信息。

    如果新发布的配置存在问题,例如将接口超时时间错误地设置为几十毫秒,管理员可以通过配置历史找到之前的正确版本并重新发布。

    因此,Nacos 配置中心不仅负责保存配置,还负责配置发布、配置获取、配置监听、配置变更通知以及配置历史管理。

  23. Nacos 使用缓存、数据库和日志存储数据

    为了同时保证查询性能和数据可靠性,Nacos 会使用不同的存储方式管理数据。

    服务实例列表、服务订阅关系以及一些频繁访问的数据,通常会放在内存或本地缓存中,这样消费者查询服务时能够快速获得结果。

    配置、命名空间、用户、权限以及需要长期保存的数据,会写入嵌入式数据库或 MySQL 等持久化存储中,保证 Nacos 重启后仍然能够恢复。

    Nacos 还会保存运行日志、错误日志、访问日志、配置变更日志和服务变更日志,方便管理员排查问题。

    例如,某个服务注册失败时,可以通过日志查看是网络连接失败、服务参数错误,还是 Nacos Server 本身发生异常。

  24. 单机版 Nacos 由一个 Nacos Server 完成全部功能

    在单机模式下,服务注册、服务发现、健康检查、配置管理、元数据管理和控制台管理都由同一个 Nacos Server 完成。

    所有 Provider 和 Consumer 都连接这个 Nacos Server。

    这种模式部署简单,适合本地学习、开发环境、测试环境和小型项目。

    但是单机模式存在单点故障。如果唯一的 Nacos Server 宕机,新的 Provider 可能无法完成注册,Consumer 无法及时获取最新的服务变化,应用也无法从 Nacos 获取最新配置。

    已经获取过实例列表的 Consumer,还能暂时使用本地缓存继续调用 Provider,但是无法及时感知新的实例上线、下线和健康状态变化。

Nacos 工作原理简述

  1. 当服务提供者 Provider 启动时,会将服务名称、IP 地址、端口、权重、集群以及版本、环境、区域等元数据注册到 Nacos 服务注册中心。

    Nacos 收到注册请求后,会校验注册信息,根据命名空间、分组和服务名称找到对应服务,并将实例保存到服务注册表中。

    注册成功后,Provider 会通过心跳或长连接保活机制持续向 Nacos 证明自己仍然存活。

    Nacos 根据心跳和健康检查结果维护实例状态。能够正常续约的实例保持健康;长时间没有心跳的实例会被标记为不健康;继续长时间没有恢复的临时实例可能被删除。

  2. 当服务消费者 Consumer 需要远程调用某个服务时,会根据服务名称从 Nacos 服务发现模块获取健康实例列表。

    Nacos 会从服务注册表中查询目标服务,过滤掉不健康、未启用或者不符合命名空间、分组和集群要求的实例,然后将可用实例列表返回给 Consumer。

    Consumer 将实例列表保存到本地缓存中,再通过负载均衡算法选择一个具体实例,最后直接向 Provider 发起业务请求。Nacos 不负责转发业务流量,只负责提供服务地址。

  3. 当 Provider 新增、下线、故障或者恢复时,Nacos 会更新服务注册表,并把服务变化通知给已经订阅该服务的 Consumer。Consumer 收到通知后更新本地实例列表,从而避免继续调用失效实例。

  4. 在配置管理方面,管理员可以通过 Nacos 控制台发布配置。Nacos 将配置保存到持久化存储中,应用启动时从 Nacos 获取配置并持续监听配置变化。配置被修改后,Nacos 通知应用重新获取最新配置,从而实现统一配置管理和部分配置的动态更新。

这就是 Nacos 在单机模式下完成服务注册、心跳续约、健康检查、服务发现、负载均衡配合、服务变更通知以及配置管理的完整工作原理。

服务注册

服务注册就是:
微服务启动以后,把自己的服务名称、IP 地址、端口号等信息,主动告诉注册中心。 在 Nacos 中,服务注册可以理解为:

服务启动
   ↓
向 Nacos 报到
   ↓
Nacos 记录这个服务实例

就像一个新员工入职后,需要到公司人事系统登记自己的姓名、部门和联系方式。

为什么要注册服务?

假设商品服务部署在:

192.168.1.10:8081

订单服务需要调用商品服务,就必须知道这个地址。但是在微服务系统中,服务地址可能经常发生变化:

服务重启,端口变了
服务部署到另一台服务器,IP 变了
服务扩容,增加了新实例
某个服务实例宕机了

如果把服务地址直接写死:http://192.168.1.10:8081/product/1,地址一变,调用就会失败。因此,服务启动后需要把自己的地址注册到 Nacos,由 Nacos 统一管理。

服务注册时注册什么信息?

一个服务实例注册到 Nacos 时,通常会携带以下信息:

服务名称
IP 地址
端口号
分组
命名空间
集群名称
实例权重
健康状态
是否为临时实例

例如:

服务名称:service-product
IP 地址:192.168.1.10
端口号:8081
健康状态:健康

Nacos 中就会形成一条记录:

service-product
└── 192.168.1.10:8081

同一个服务可以注册多个实例,假设商品服务部署了三份:

商品服务实例1:192.168.1.10:8081
商品服务实例2:192.168.1.11:8081
商品服务实例3:192.168.1.12:8081

它们的服务名称都叫:service-product。注册到 Nacos 后:

service-product
├── 192.168.1.10:8081
├── 192.168.1.11:8081
└── 192.168.1.12:8081

这里要区分两个概念:

  • 服务:
    service-product
  • 服务实例:
    192.168.1.10:8081
    192.168.1.11:8081
    192.168.1.12:8081

服务注册原理

在这里插入图片描述

  1. 当服务提供者 Provider 启动时,应用中的 Nacos Client 会随之完成初始化,并读取 Nacos 的相关配置,例如 Nacos Server 地址、服务名称、命名空间、分组、集群名称以及实例类型等信息。

  2. Nacos Client 会收集并封装当前服务实例的信息,主要包括服务名称、IP 地址、端口、权重、命名空间、分组、集群以及元数据等。例如,订单服务可以注册为:服务名称 order-service、IP 地址 192.168.1.10、端口 8081、权重 1.0,元数据为 version=v1environment=prod。其中,元数据可以用于区分服务版本、部署环境、机房区域或灰度实例。

  3. 服务实例信息封装完成后,Nacos Client 会向 Nacos Server 的 Open API 或客户端通信接口发送注册请求。这个请求的含义可以理解为:当前有一个名为 order-service 的服务实例,运行在 192.168.1.10:8081,现在可以正常对外提供服务,请将它加入服务注册表。

  4. Nacos Server 的接入层收到注册请求后,会对请求参数进行校验,例如检查服务名称是否为空、IP 和端口是否合法、命名空间是否存在以及实例参数是否完整。如果参数不合法,Nacos 会返回注册失败信息;如果参数合法,则继续执行注册流程。

  5. 参数校验通过后,Nacos 会根据“命名空间、分组、服务名称、集群名称”等信息,确定当前实例应该属于哪个服务。例如,prod 命名空间、DEFAULT_GROUP 分组下的 order-service,与 dev 命名空间中的同名服务属于不同的服务范围,从而实现环境隔离。

  6. 如果 Nacos 中还不存在对应的服务,服务注册中心会先创建该服务;如果服务已经存在,则直接将当前实例加入该服务的实例列表。例如,一个 order-service 服务下面可以同时保存 192.168.1.10:8081192.168.1.11:8081192.168.1.12:8081 等多个实例。

  7. 实例管理模块会保存该实例的详细信息,包括 IP、端口、权重、健康状态、启用状态、集群、分组和元数据等。注册成功时,实例通常会被标记为可用状态,后续消费者便可以通过服务名称发现这个实例。

  8. Nacos 完成服务实例写入后,会向 Provider 返回注册成功响应。Provider 收到响应后,就表示当前实例已经成功加入 Nacos 服务注册表,其他消费者可以通过 order-service 这个服务名称查询到它。

  9. 服务注册成功后,Provider 并不是只注册一次就结束。为了让 Nacos 知道该实例仍然存活,Nacos Client 会通过心跳续约或长连接保活机制,持续向 Nacos Server 上报实例状态。心跳的含义就是告诉 Nacos:当前实例仍然在线,可以继续对外提供服务。

  10. Nacos 的心跳检测模块收到心跳后,会更新该实例最近一次存活时间,并继续将实例标记为健康状态。只要心跳能够正常到达,实例就会一直保留在可用实例列表中。

  11. 如果 Provider 因应用崩溃、服务器宕机、网络中断等原因无法继续发送心跳,Nacos 在一段时间内收不到该实例的存活信息,就会认为该实例可能出现异常,并将其标记为不健康。

  12. 实例被标记为不健康后,Nacos 会更新服务注册表中的实例状态。服务消费者再次查询实例时,通常不会再选择这个异常实例,从而避免把业务请求发送到已经不可用的服务器。

  13. 如果实例长时间没有恢复,临时实例可能会被 Nacos 从服务注册表中移除。如果 Provider 恢复运行并重新连接 Nacos,则会再次执行服务注册流程,重新加入服务实例列表。

  14. 如果 Provider 是正常关闭,例如 Spring Boot 应用被正常停止,Nacos Client 可以在应用销毁前主动向 Nacos 发送注销请求。Nacos 收到注销请求后,会立即将该实例从服务注册表中移除,而不必等待心跳超时。

  15. 当实例注册、下线、被标记为不健康或者恢复健康时,Nacos 会更新对应服务的实例列表,并将变化通知给订阅该服务的消费者。消费者收到通知后,会更新本地缓存中的实例信息,保证后续调用使用最新的可用实例。

综上,Nacos 服务注册的完整过程就是:Provider 启动后收集服务名称、IP、端口和元数据等实例信息,通过 Nacos Client 发送注册请求;Nacos Server 校验并保存实例,将其加入服务注册表;注册成功后,Provider 通过心跳或长连接持续维持实例健康状态;当实例心跳超时、主动下线或发生故障时,Nacos 会更新或移除该实例,并把实例变化通知给服务消费者。

服务发现

服务发现是什么?

Nacos 服务发现,就是帮助服务消费者找到可用服务提供者的过程。

在微服务系统中,服务提供者的 IP、端口可能会动态变化,消费者不能把调用地址写死。因此,消费者会从 Nacos 中获取目标服务当前可用的实例列表,再选择其中一个实例发起远程调用。

服务发现原理

在这里插入图片描述

  1. 当服务消费者 Consumer 启动时,应用中的 Nacos Client 会同步初始化,并读取 Nacos 服务发现相关配置,例如 Nacos Server 地址、当前应用名称、命名空间、分组、集群名称等信息。

  2. 假设用户服务 user-service 需要远程调用订单服务 order-service。在没有注册中心时,用户服务必须在配置文件中写死订单服务的 IP 和端口,例如 192.168.1.10:8081。使用 Nacos 后,消费者只需要知道目标服务名称 order-service,不需要关心订单服务具体部署在哪台服务器上。

  3. 当 Consumer 第一次准备调用 order-service 时,Nacos Client 会根据目标服务名称查找本地缓存,判断本地是否已经保存了该服务的实例列表。

  4. 如果本地缓存中不存在 order-service 的实例信息,或者缓存中的实例信息需要刷新,Nacos Client 就会向 Nacos Server 发起服务查询或服务订阅请求。请求中通常会携带服务名称、命名空间、分组、集群名称等条件。

  5. Nacos Server 的接入层收到查询请求后,会根据“命名空间、分组和服务名称”定位目标服务。例如,消费者查询的可能是 prod 命名空间、DEFAULT_GROUP 分组下的 order-service

  6. Nacos 的服务发现模块会从服务注册表中查询 order-service 当前注册的全部实例。假设订单服务中存在以下三个实例:192.168.1.10:8081192.168.1.11:8081192.168.1.12:8081

  7. Nacos 查询到实例后,会根据实例状态进行筛选。例如,检查实例是否已经启用、是否健康、是否属于指定集群、是否符合命名空间和分组要求。

  8. 假设 192.168.1.12:8081 因为长时间没有发送心跳,已经被标记为不健康,那么 Nacos 不会把它作为正常可用实例提供给消费者。最终返回的可用实例可能只有 192.168.1.10:8081192.168.1.11:8081

  9. Nacos 返回的内容不仅包含实例的 IP 和端口,还可能包含实例权重、健康状态、启用状态、集群名称以及元数据等信息。例如,一个实例的元数据可能包含 version=v2,表示该实例属于 v2 版本;也可能包含 region=tokyo,表示该实例部署在东京区域。

  10. Consumer 收到 Nacos 返回的实例列表后,会将其保存到本地缓存中。保存本地缓存后,Consumer 后续调用 order-service 时,通常直接从内存中获取实例列表,而不是每次发送业务请求都重新访问 Nacos。

  11. 本地缓存可以提高服务调用效率,同时减少 Nacos Server 的查询压力。即使 Nacos Server 短时间无法访问,Consumer 仍然可以暂时使用已经缓存的服务实例继续发送请求。

  12. 当 Consumer 从本地缓存中获得多个可用实例后,会通过客户端负载均衡组件选择其中一个实例。常见的选择方式包括轮询、随机、权重以及优先选择同集群实例等。

  13. 例如,当前存在两个实例:192.168.1.10:8081 的权重为 1.0192.168.1.11:8081 的权重为 2.0。负载均衡组件可能让权重较高的第二个实例承担更多请求。

  14. 假设本次调用最终选择了 192.168.1.10:8081,Consumer 就会把原来的逻辑服务地址 http://order-service/order/query 转换为真实地址 http://192.168.1.10:8081/order/query

  15. 获取真实地址后,Consumer 会直接向选中的 Provider 发送 HTTP、RPC 或其他协议的业务请求。Nacos 不参与业务请求的转发,它只负责帮助 Consumer 找到可用的 Provider 地址。

  16. 因此,服务发现阶段的交互是 Consumer 向 Nacos 查询服务地址,而真正的业务调用是 Consumer 直接访问 Provider,不是先把业务请求发送给 Nacos,再由 Nacos 转发。

  17. Consumer 第一次查询 order-service 时,通常还会订阅这个服务。订阅的含义是告诉 Nacos:“我正在使用 order-service,如果这个服务的实例列表发生变化,请及时通知我。”

  18. Nacos 会维护 Consumer 对服务的订阅关系。当 order-service 有新实例上线、旧实例下线、实例发生故障、实例恢复健康、权重被修改或者元数据发生变化时,Nacos 会更新服务注册表。

  19. 服务注册表发生变化后,Nacos 会通过客户端长连接或服务变更通知机制,将最新的实例变化推送给已经订阅该服务的 Consumer。

  20. Consumer 收到变更通知后,会更新本地缓存中的实例列表。例如,原来缓存中存在 192.168.1.10:8081192.168.1.11:8081 两个实例。

  21. 如果 192.168.1.10:8081 因为服务宕机而被 Nacos 标记为不健康,Nacos 会通知 Consumer。Consumer 更新本地缓存后,只保留 192.168.1.11:8081,后续请求就不会继续发送给已经故障的实例。

  22. 如果新启动了一个订单服务实例 192.168.1.13:8081,该实例注册成功后,Nacos 同样会通知 Consumer。Consumer 更新本地缓存后,就可以在后续请求中将新实例加入负载均衡范围。

  23. 如果 Consumer 调用某个 Provider 时发生连接失败或请求异常,客户端可能会根据负载均衡和重试机制选择其他可用实例重新调用。具体是否重试以及重试次数,取决于项目使用的负载均衡组件和远程调用组件配置。

  24. 当 Nacos Server 暂时发生故障时,已经获取过实例列表的 Consumer 通常还可以使用本地缓存继续调用 Provider。但是在这段时间内,Consumer 可能无法及时感知新实例上线、旧实例下线以及实例健康状态变化。

  25. 当 Nacos Server 恢复后,Nacos Client 会重新建立连接、恢复服务订阅,并重新获取最新的实例列表,使本地缓存逐渐恢复到最新状态。

  26. 当 Provider 正常关闭时,会主动向 Nacos 注销服务实例;当 Provider 异常宕机时,Nacos 会根据心跳超时或连接断开判断实例异常。无论是哪种情况,Nacos 都会更新服务注册表,并把变化通知给 Consumer。

  27. Consumer 更新实例列表后,负载均衡组件只会从当前可用实例中选择目标地址,从而避免继续调用已经下线或不健康的 Provider。

综上,Nacos 服务发现的完整过程就是:

  • Consumer 根据服务名称向 Nacos 查询目标服务,Nacos 从服务注册表中查找并筛选健康实例,然后将可用实例列表返回给 Consumer;Consumer 将实例列表保存到本地缓存,通过负载均衡选择一个具体实例,最后直接向 Provider 发起远程调用;当服务实例发生上线、下线或健康状态变化时,Nacos 会通知 Consumer 更新本地缓存,保证后续请求尽量发送到最新的可用实例。

nacos注册中心使用案例

项目准备

安装启动nacos:官网下载地址
下载好后进入程序包的bin目录下,在文件资源路径输入cmd回车进入黑窗口,执行:startup.cmd -m standalone 命令启动nacos。

创建一个聚合工程,项目结构如下:

cloud-demo/
├── cloudOrder/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/
│   │   │   │   └── com/order/
│   │   │   │       └── OrderMainApplication.java       # 订单服务启动类
│   │   │   │
│   │   │   └── resources/
│   │   │       └── application.yml                     # 订单服务配置文件
│   │   │
│   │   └── test/
│   │       └── java/                                   # 订单服务测试代码
│   │
│   └── pom.xml                                         # 订单服务 Maven 配置
│
├── cloudProduct/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/
│   │   │   │   └── com/product/
│   │   │   │       └── ProductMainApplication.java     # 商品服务启动类
│   │   │   │
│   │   │   └── resources/
│   │   │       └── application.yml                     # 商品服务配置文件
│   │   │
│   │   └── test/
│   │       └── java/                                   # 商品服务测试代码
│   │
│   └── pom.xml                                         # 商品服务 Maven 配置
│
└── pom.xml                                             # 父工程 Maven 配置

在这里插入图片描述
父pom文件、cloudOrder模块pom、cloudProduct模块pom内容:

=================================================父pom内容=================================================
<?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>
    <parent>
        <groupId>com.study</groupId>
        <artifactId>cloud-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloudProduct</artifactId>

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

    <dependencies>
        <!--SpringBoot启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--注册中心启动器-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

</project>

=================================================cloudOrder模块pom内容=================================================
<?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>
    <parent>
        <groupId>com.study</groupId>
        <artifactId>cloud-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloudOrder</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--SpringBoot启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>

=================================================cloudProduct模块pom内容=================================================
<?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>
    <parent>
        <groupId>com.study</groupId>
        <artifactId>cloud-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloudProduct</artifactId>

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

    <dependencies>
        <!--SpringBoot启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>

两个子模块配置文件就定义了端口和名称,重点标记: 使用nacos必须要给项目定义name属性,因为:Nacos 默认使用 ${spring.application.name} 作为注册到服务中心的服务名;没有配置应用名称,Nacos 就不知道该把当前实例注册成什么服务。 否则就会:Nacos 客户端已经启动,但没有找到要注册的“服务名称”,所以跳过了注册。No service to register for nacos client… 这个日志。

ok,现在配置文件内容如下:

==============order配置==============
server:
  port: 8000
spring:
  application:
    name: cloud-order
==============product配置==============
server:
  port: 9000
spring:
  application:
    name: cloud-product

添加好两个模块的启动类,保证项目正常启动。order占用8000端口,product占用9000端口


添加服务注册功能

把order和product两个模块都注册到nacos中,所以先给两个子模块添加服务注册的功能,给两个模块都添加以下maven依赖:

<!--注册中心启动器-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

怎么使用呢?我们只需要在启动类添加@EnableDiscoveryClient注解,然后再配置文件添加好nacos服务端的地址就可以开启服务注册功能了。

@EnableDiscoveryClient
@SpringBootApplication
public class OrderMainApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderMainApplication.class,args);
    }
}

@EnableDiscoveryClient
@SpringBootApplication
public class ProductMainApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductMainApplication.class, args);
    }
}
server:
  port: 8000
spring:
  application:
    name: cloud-order
  # 添加nacos地址  
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
=============================================================================
server:
  port: 9000
spring:
  application:
    name: cloud-product
  # 添加nacos地址  
  cloud:
    nacos:
      discovery:
        server-addr=127.0.0.1:8848

OK,启动项目就可以在idea的控制台和nacos的管理界面看到order、product模块注册成功了
在这里插入图片描述
在这里插入图片描述

使用idea复制功能注册多个实例

我们测试把order、product模块都复制一份出来,再nacos都注册两个实例,看看怎么做?
1、利用idea的复制功能,把模块复制一份
在这里插入图片描述
2、修改参数,点击Modify options按钮,点击Program arguments添加参数。
在这里插入图片描述
在这里插入图片描述
注意重点: 只要是配置文件能写的配置,都可以在这里写。
OK,复制完毕,启动多份看看结果:
在这里插入图片描述

٩(◕‿◕。)۶ 服务注册搞定 ٩(◕‿◕。)۶


添加服务发现功能

DiscoveryClient、NacosServiceDiscovery

这两个类都是去nacos拉取服务列表的,功能上和用法上基本上一模一样,只是DiscoveryClient属于spring加的NacosServiceDiscovery是nacos原生的。下面是使用案例。

直接在order或者product模块引入测试依赖且创建一个测试类,此时项目结构如下:

只是在cloudOrder模块的加了一个测试类OrderTest.java

cloud-demo/
├── cloudOrder/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/
│   │   │   │   └── com/order/
│   │   │   │       └── OrderMainApplication.java      # 订单服务启动类
│   │   │   │
│   │   │   └── resources/
│   │   │       └── application.yml                    # 订单服务配置文件
│   │   │
│   │   └── test/
│   │       └── java/
│   │           └── OrderTest.java                     # 订单服务测试类
│   │
│   └── pom.xml                                        # 订单服务 Maven 配置
│
├── cloudProduct/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/
│   │   │   │   └── com/product/
│   │   │   │       └── ProductMainApplication.java    # 商品服务启动类
│   │   │   │
│   │   │   └── resources/
│   │   │       └── application.yml                    # 商品服务配置文件
│   │   │
│   │   └── test/
│   │       └── java/                                  # 商品服务测试代码目录
│   │
│   └── pom.xml                                        # 商品服务 Maven 配置
│
└── pom.xml                                            # 父工程 Maven 配置
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
import com.alibaba.cloud.nacos.discovery.NacosServiceDiscovery;
import com.alibaba.nacos.api.exception.NacosException;
import com.order.OrderMainApplication;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;

import java.util.List;
@SpringBootTest(classes = OrderMainApplication.class)
public class OrderTest {
    @Resource
    private DiscoveryClient discoveryClient;

    @Resource
    private NacosServiceDiscovery nacosServiceDiscovery;

    @Test
    public void discoveryClientTest() {
        for (String service : discoveryClient.getServices()) {
            List<ServiceInstance> instances = discoveryClient.getInstances(service);
            for (ServiceInstance serviceInstance : instances) {
                System.out.println("服务名称:" + service + "。ip:" + serviceInstance.getHost() + "。port:"
                        + serviceInstance.getPort() + "。uri:" + serviceInstance.getUri());
            }

        }
    }

    @Test
    public void nacosServiceDiscoveryTest() throws NacosException {
        for (String service : nacosServiceDiscovery.getServices()) {
            System.out.println("service = " + service);
            List<ServiceInstance> instances = nacosServiceDiscovery.getInstances(service);
            for (ServiceInstance instance : instances) {
                System.out.println("ip:" + instance.getHost() + "。" + "port = " +
                        instance.getPort() + "。uri=" + instance.getUri());
            }
        }
    }

}

控制台输出结果:
在这里插入图片描述
在这里插入图片描述
现在,我们成功的nacos拿到了服务列表了。可有有什么用呢?然后呢?怎么调用?

远程调用

我们现在模拟一个调用场景,用户在浏览器购买商品,Order模块调用Product模块查询商品库存,商品库存查完库返回订单数量是否足够,订单根据数量足够告诉用户是否购买成功。流程:

用户下单
	↓
订单模块(Order)
	↓
商品模块(Prduct)

为了测试远程调用,我们去order、product两个模块中添加一个controller接口。然后使用RestTemplate调用远程接口,RestTemplate是什么:RestTemplate 是 Spring Framework 提供的一个同步 HTTP 客户端工具类,用于简化 RESTful Web Service 的调用。

项目结构如下:

新增了两个控制器:OrderController.java、ProductController.java,一个配置类:OrderConfig.java

cloud-demo/
├── cloudOrder/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/
│   │   │   │   └── com/order/
│   │   │   │       ├── config/
│   │   │   │       │   └── OrderConfig.java              # 订单服务配置类
│   │   │   │       │
│   │   │   │       ├── controller/
│   │   │   │       │   └── OrderController.java          # 订单服务控制层
│   │   │   │       │
│   │   │   │       └── OrderMainApplication.java         # 订单服务启动类
│   │   │   │
│   │   │   └── resources/
│   │   │       └── application.yml                       # 订单服务配置文件
│   │   │
│   │   └── test/
│   │       └── java/
│   │           └── OrderTest.java                        # 订单服务测试类
│   │
│   └── pom.xml                                           # 订单服务 Maven 配置
│
├── cloudProduct/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/
│   │   │   │   └── com/product/
│   │   │   │       ├── controller/
│   │   │   │       │   └── ProductController.java       # 商品服务控制层
│   │   │   │       │
│   │   │   │       └── ProductMainApplication.java      # 商品服务启动类
│   │   │   │
│   │   │   └── resources/
│   │   │       └── application.yml                       # 商品服务配置文件
│   │   │
│   │   └── test/
│   │       └── java/                                     # 商品服务测试代码目录
│   │
│   └── pom.xml                                           # 商品服务 Maven 配置
│
└── pom.xml                                               # 父工程 Maven 配置

OrderConfig.java代码:

package com.order.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class OrderConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

OrderController代码:

package com.order.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.Random;

@RestController
public class OrderController {
    @Autowired
    private DiscoveryClient discoveryClient;   // 服务发现
    @Autowired
    private RestTemplate restTemplate;         // 远程调用

    @GetMapping
    public String order(){
        // 随机生成10以内整数,模拟用户下单数量
        int orderNum = new Random().nextInt(10);
        System.out.println("用户下单,数量:"+ orderNum +"。准备远程调用Product服务查询商品库存");
        // 获取商品服务实例
        List<ServiceInstance> instances = discoveryClient.getInstances("cloud-product");
        // 选择实例准备远程调用
        ServiceInstance instance = instances.get(0);

        //远程URL
        String url = "http://"+instance.getHost() +":" +instance.getPort();
        // 给远程发送请求
         int productNum = restTemplate.getForObject(url, Integer.class);

         return orderNum >= productNum ? "下单成功" : "下单失败,商品库存不足。";

    }
}

ProductController代码:

package com.product.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;

@RestController
public class ProductController {
    @GetMapping
    public int  getProduct(){
        // 生成10以内的整数,模拟从数据库查回来的商品数量
        int num = new Random().nextInt(10);
        System.out.println("商品服务,商品数量:" + num);
        return num;
    }
}

结果演示:
在这里插入图片描述

٩(◕‿◕。)۶ 远程调用搞定 ٩(◕‿◕。)۶ 。。。。了吗? (・_・?)

restTemplate远程调用的问题

看代码:

@GetMapping
    public String order(){
       ......
       ......
        List<ServiceInstance> instances = discoveryClient.getInstances("cloud-product");
        ServiceInstance instance = instances.get(0);

        String url = "http://"+instance.getHost() +":" +instance.getPort();
        // 给远程发送请求
         int productNum = restTemplate.getForObject(url, Integer.class);
		......
		......
    }

重点在这两句:

List instances = discoveryClient.getInstances(“cloud-product”);
ServiceInstance instance = instances.get(0);

逻辑是:回去nacos的注册中心拿到cloud-product的服务列表,这里没有问题。

问题是ServiceInstance instance = instances.get(0);
get(0),就算地球爆炸了也永远只会拿cloud-product服务的实例列表中的第一个实例进行远程调用,也就是说你哪怕部署10台product商品服务,但是只会有一台能够被使用

这怎么解决呢?难道要我自己对instances 这个列表写个算法来保证每个实例都能被公平的调用吗? 安啦。Spring Cloud官方早就给你准备好了。

loadBalancerClient + restTemplate

LoadBalancerClient:是Spring Cloud官方提供的负载均衡。

直接在order模块中添加LoadBalancerClient的依赖:

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

在order模块的控制器添加新的远程调用 + 负载均衡的方法,OrderController代码:

package com.order.controller;

import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.Random;


@RestController
public class OrderController {
    @Autowired
    private DiscoveryClient discoveryClient;   // 服务发现
    @Autowired
    private RestTemplate restTemplate;         // 远程调用
    @Autowired
    private LoadBalancerClient loadBalancerClient;



    /*第二阶段:LoadBalancer负载均衡获取实例 + restTemplate远程调用*/
    @GetMapping("loadBalancerOrder")
    public String loadBalancerOrder(){
        // 随机生成10以内整数,模拟用户下单数量
        int orderNum = new Random().nextInt(10);
        System.out.println("用户下单,数量:"+ orderNum +"。准备远程调用Product服务查询商品库存");

        // 负载均衡的获取服务实例
        ServiceInstance choose = loadBalancerClient.choose("cloud-product");
        System.out.println("负载均衡获取cloud-product服务实例:" + choose.getHost() + ":" + choose.getPort());

        //远程URL
        String url = "http://"+choose.getHost() +":" +choose.getPort();
        // 给远程发送请求
        int productNum = restTemplate.getForObject(url, Integer.class);

        String uri = choose.getUri().toString();
        return orderNum >= productNum ? "下单成功,调用" + uri +
                "服务查询商品库存成功" : "下单失败,调用" + uri + "服务查询商品库存不足";

    }

    /*第一阶段:远程调用*/
    @GetMapping
    public String order(){
        // 随机生成10以内整数,模拟用户下单数量
        int orderNum = new Random().nextInt(10);
        System.out.println("用户下单,数量:"+ orderNum +"。准备远程调用Product服务查询商品库存");
        // 获取商品服务实例
        List<ServiceInstance> instances = discoveryClient.getInstances("cloud-product");
        // 选择实例准备远程调用
        ServiceInstance instance = instances.get(0);

        //远程URL
        String url = "http://"+instance.getHost() +":" +instance.getPort();
        // 给远程发送请求
         int productNum = restTemplate.getForObject(url, Integer.class);

         return orderNum >= productNum ? "下单成功" : "下单失败,商品库存不足。";

    }
}

测试结果展示,复制三个商品模块(product)注册的nacos,然后再浏览器调用三次order订单接口,结果如下:
在这里插入图片描述
在这里插入图片描述

@LoadBalanced+ restTemplate

基于注解的负载均衡,修改order模块代码,在OrderConfig给restTemplate方法添加@LoadBalanced注解,直接在控制器使用。

OrderConfig代码:

package com.order.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class OrderConfig {

    @LoadBalanced  // 注解负载均衡
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

OrderController代码:

package com.order.controller;

import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.Random;

@RestController
public class OrderController {
    @Autowired
    private DiscoveryClient discoveryClient;   // 服务发现
    @Autowired
    private RestTemplate restTemplate;         // 远程调用
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    /*第三阶段阶段:基于注解LoadBalancer负载均衡获取实例 + restTemplate远程调用*/
    @GetMapping("loadBalancerAnnotationOrder")
    public String loadBalancerAnnotationOrder(){
        // 随机生成10以内整数,模拟用户下单数量
        int orderNum = new Random().nextInt(10);
        System.out.println("用户下单,数量:"+ orderNum +"。准备远程调用Product服务查询商品库存");
        
        String url = "http://cloud-product";
        // 给远程发送请求,cloud-product服务名称会被动态的替换成实例名,比如cloud-product会被替换成192.168.1.1:9000、9001、9002
        int productNum = restTemplate.getForObject(url, Integer.class);
        return orderNum >= productNum ? "下单成功" : "下单失败,商品库存不足。";

    }


    /*第二阶段:LoadBalancer负载均衡获取实例 + restTemplate远程调用*/
    @GetMapping("loadBalancerOrder")
    public String loadBalancerOrder(){
        // 随机生成10以内整数,模拟用户下单数量
        int orderNum = new Random().nextInt(10);
        System.out.println("用户下单,数量:"+ orderNum +"。准备远程调用Product服务查询商品库存");

        // 负载均衡的获取服务实例
        ServiceInstance choose = loadBalancerClient.choose("cloud-product");
        System.out.println("负载均衡获取cloud-product服务实例:" + choose.getHost() + ":" + choose.getPort());

        //远程URL
        String url = "http://"+choose.getHost() +":" +choose.getPort();
        // 给远程发送请求
        int productNum = restTemplate.getForObject(url, Integer.class);

        String uri = choose.getUri().toString();
        return orderNum >= productNum ? "下单成功,调用" + uri +
                "服务查询商品库存成功" : "下单失败,调用" + uri + "服务查询商品库存不足";

    }

    /*第一阶段:远程调用*/
    @GetMapping
    public String order(){
        // 随机生成10以内整数,模拟用户下单数量
        int orderNum = new Random().nextInt(10);
        System.out.println("用户下单,数量:"+ orderNum +"。准备远程调用Product服务查询商品库存");
        // 获取商品服务实例
        List<ServiceInstance> instances = discoveryClient.getInstances("cloud-product");
        // 选择实例准备远程调用
        ServiceInstance instance = instances.get(0);

        //远程URL
        String url = "http://"+instance.getHost() +":" +instance.getPort();
        // 给远程发送请求
         int productNum = restTemplate.getForObject(url, Integer.class);

         return orderNum >= productNum ? "下单成功" : "下单失败,商品库存不足。";
    }
}

测试结果:
在这里插入图片描述
在这里插入图片描述

(◕‿◕✿)服务注册 ✅(◕‿◕✿)服务发现 ✅(◕‿◕✿)远程调用 ✅(◕‿◕✿)负载均衡 ✅(◕‿◕✿)

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐