Skip to content

第4章 分布式服务治理——基于Nacos

应用被水平拆分和垂直拆分后,在线上部署时,应用的实例数几何倍数增加,对开发人员和运维人员的技术挑战性非常高。

服务治理本质上是管理服务与服务之间的调用关系,并维持服务链路关系稳定的一系列架构方法论

🚀 4.1 分布式服务治理

万丈高楼从地起。开发人员可以先认识分布式服务治理,再去熟悉分布式服务治理领域的核心技术。

🚀 4.1.1 什么是分布式服务治理

在分布式系统的构建中,一个好的服务治理平台可以大大降低协作开发的成本和版本的迭代效率。

服务治理主要包括如下几点:

  • 服务注册与发现:在单体架构被拆分为微服务架构后,如果在微服务之间存在调用依赖,则进行服务治理需要得到目标服务的服务地址,即微服务治理的“服务发现”。要完成服务发现,则需要将服务信息存储在某个载体中。载体本身即微服务治理的“注册中心”,而存储到载体的动作即“服务注册”。
  • 可观测性:在改为微服务架构之后,应用有了更多的部署载体,所以需要对众多服务间的调用关系、状态有清晰的掌控。可观测性包括调用拓扑关系、监控、日志、调用追踪等。
  • 流量管理:由于微服务本身存在不同版本,所以在版本迭代过程中,需要对微服务间的调用进行控制,以完成微服务版本的平滑升级。在这个过程中,需要根据流量控制规则将流量路由到不同版本的服务,这也孵化出了灰度发布、蓝绿发布、A/B 测试等服务治理的细分主题。
  • 安全性管理:不同微服务担当不同的业务角色,对于业务敏感的微服务,需要对其访问进行认证与鉴权(即安全问题)。
  • 权限控制:在服务治理过程中,需要比较完善的权限控制体系,以控制服务之间的调用权限。

🚀 4.1.2 为什么需要分布式服务治理

在没有服务治理前,服务之间是通过直连的方式来相互访问的,如图4-1所示。比如,订单服务要访问支付服务的支付接口,所以订单服务需要在本地配置IP 地址、端口号及支付接口 URL。

从图 4-1可以看出,订单服务是直接调用支付服务。如果系统不复杂,则这样的调用是没什么问题的。但在采用微服务架构后,采用这种调用方式就会有问题了。

微服务架构中的服务非常多,并且都是采用集群部署的。每个服务都要维护一套服务之间的调用关系,非常烦琐。这时就需要一个中间代理来维护调用关系,注册中心应运而生。服务之间的调用如图 4-2 所示。

从图 4-2可以看出,订单服务和支付服务都要注册到注册中心中,并且可以订阅注册中心所有的服务。订单服务通过订阅服务,可以间接地调用支付服务。

4.2 主流的注册中心

目前有很多注册中心的技术解决方案,Nacos、ZooKeeper、Sofa 等是主流的注册中心。

🚀 4.2.1 Nacos

Nacos 致力于发现、配置和管理微服务。它提供了一组简单易用的功能,帮助开发人员快速实现动态服务发现、服务配置、服务元数据及流量管理,从而更敏捷和更容易地构建、交付和管理服务平台。

Nacos 是构建以“服务”为中心的现代应用架构(例如微服务范式、云原生范式)的基础设施。

Nacos 的主要特性如下:

1.服务发现与服务健康检查

Nacos 支持基于 DNS 和 HTTP&API 的服务发现。服务提供者使用原生 SDK、OpenAPI 或一个独立的 Agent 注册 Service 后,服务消费者可以使用 DNS 或者 HTTP&API 查找和发现服务。

Nacos 提供了对服务的实时健康检查,阻止向不健康的主机或服务实例发送请求。Nacos支持传输层和应用层的健康检查。

对于复杂的云环境和网络拓扑环境(如 VPC、边缘网络等)中的服务健康检查,Nacos 提供了“Agent 上报”和“服务器端主动检测”两种健康检查模式。

Nacos 还提供了统一的健康检查仪表盘,帮助用户根据健康状态来管理服务的可用性及流量。

2.动态配置服务

动态配置服务可以让用户以中心化、外部化和动态化的方式,管理所有环境的应用配置和配置。动态配置消除了在配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。

配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。

3.动态 DNS 服务

动态 DNS 服务支持权重路由,能更容易、更灵活地实现中间层负载均衡、路由策略、以及数据中心内网的简单 DNS 解析。

利用动态 DNS 服务,能更容易地实现以 DNS 协议为基础的服务注册/订阅,以消除在客户耦合到厂商私有服务时注册/订阅的可能风险。

4.服务及其元数据管理

Nacos 能从微服务平台建设的视角,管理注册中心中的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态管理、服务的流量管理、路由及安全策略、服务的 SLA ,以及最重要的 metrics 统计数据。

🚀 4.2.2 ZooKeeper

ZooKeeper 从Apache Hadoop 的子项目发展而来,于2010年11月正式成为 Apache 的顶级项目。ZooKeeper为分布式应用提供了高效且可靠的分布式协调服务,以及统一命名服务、配置管理和分布式锁等分布式基础服务,在解决分布式数据一致性方面,ZooKeeper 并没有直接采用Paxos 算法,而是采用了一种被 称为 ZAB 的一致性算法协议。

ZooKeeper 可以保证如下分布式特性:

  • 顺序一致性:从同一个客户端发起的事务请求,将按照其发起顺序被严格地应用到ZooKeeper中。

  • 原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,即要么集群中的所有机器都成功应用了某一个事务,要么都没有应用,一定不会出现“集群中部分机器应用了该事务,而另外一部分没有应用”的情况,这是不符合数据一致性的。

  • 单一视图:无论客户端连接的是哪个ZooKeeper 服务器,其看到的服务器端数据模型都是一致的。

  • 可靠性:一旦服务器成功应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务器端状态变更会被一直保留下来,除非有另一个事务产生了状态变更。

  • 实时性:通常人们看到实时性的第一反应是:一旦一个事务被成功应用,那么客户端能够立即从服务器端读取这个事务变更之后的最新的数据。注意,ZooKeeper仅保证在一定的时间内,客户端能够从服务器端读取最新的数据,利用 ZooKeeper 进行集群管理的框架有很多,但只有 Apache Dubbo (以下简称 Dubbo)是基于 ZooKeeper 来实现分布式服务治理中的能力建设的,其他主流框架都只是用 ZooKeeper来确保分布式环境下集群内数据的一致性的。

因为 ZooKeeper 能够确保 CAP 理论中的C和P,即能够确保强一致性和分区容错性所以,依赖 ZooKecpcr的主流框架几乎都是与数据处理相关的,对数据强一致性要求非常高。

4.2.3 Consul

4.2.4 Sofa

4.2.5 Etcd

4.2.6 Eureka

4.2.7 对比Nacos、ZooKeeper、Sofa、Consul、Etcd和Euraka

🚀 4.3 将应用接入Nacos 注册中心

Nacos Naming 是 Nacos 注册中心的功能模块。开发者可以通过在 Nacos Client 中封装的NacosNamingService 类将应用快速接入Nacos 注册中心。

常规的接入方式有如下两种:

  • 利用 Nacos 官方提供的“Nacos Client+Spring Boot”模式,这是一个针对 Spring Boot应用提供的“开箱即用”解决方案。
  • 利用 Spring Cloud Alibaba 提供的“Spring Cloud Alibaba Discovery”模式,这是针对 Spring Cloud Alibaba 应用提供的定制化解决方案。

🚀 4.3.1 【实例】用“Nacos Client + Spring Boot”接入

源码:Demo4Book/sca/chapterfour/use-nacos-spring-boot

本实例,先用IDEA 创建一个 Spring Boot 项目,再在其中添加 Nacos Client 相关的依赖。

1.添加 POM 依赖

文件:Demo4Book/sca/chapterfour/use-nacos-spring-boot/pom.xml

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>chapterfour</artifactId>
        <groupId>com.alibaba.youxia</groupId>
        <version>1.0.0.release</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>use-nacos-spring-boot</artifactId>
    <groupId>com.alibaba.youxia</groupId>
    <version>1.0.0.release</version>
    <packaging>jar</packaging>
    <name>use-nacos-spring-boot</name>
    <description>用“SpringBoot + Nacos”开发Dubbo和Restful服务</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
            <version>2.3.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
            <version>2.3.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.boot</groupId>
            <artifactId>nacos-discovery-spring-boot-starter</artifactId>
            <version>0.2.7</version>
        </dependency>

        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.8</version>
        </dependency>
    </dependencies>
</project>

2.添加属性文件

在程序中添加配置文件 bootstrap.yaml,具体配置如下所示

文件:Demo4Book/sca/chapterfour/use-nacos-spring-boot/src/main/resources/application.properties

ini
spring.application.name=use-nacos-spring-boot
server.port=7823
dubbo.application.name=use-nacos-spring-boot
dubbo.registry.address=nacos://127.0.0.1:8848
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
dubbo.application.parameters.namespace=c7ba173f-29e5-4c58-ae78-b102be11c4f9
dubbo.application.parameters.group=use-nacos-spring-boot
nacos.discovery.server-addr=127.0.0.1:8848
nacos.discovery.namespace=c7ba173f-29e5-4c58-ae78-b102be11c4f9
nacos.discovery.register.group-name=use-nacos-spring-boot
nacos.discovery.auto-register=true
nacos.discovery.register.service-name=use-nacos-spring-boot

3.添加 Dubbo 接口

在程序中定义一个 Dubbo 接口 PayService,具体代码如下所示:

文件:Demo4Book/sca/chapterfour/use-nacos-spring-boot/src/main/java/com/alibaba/cloud/youxia/service/PayService.java

java
package com.alibaba.cloud.youxia.service;

public interface PayService {
    String pay();
}

文件:Demo4Book/sca/chapterfour/use-nacos-spring-boot/src/main/java/com/alibaba/cloud/youxia/service/impl/PayServiceImpl.java

java
package com.alibaba.cloud.youxia.service.impl;

import com.alibaba.cloud.youxia.service.PayService;
import org.apache.dubbo.config.annotation.DubboService;

@DubboService(group = "use-nacos-spring-boot",version = "1.0.0")
public class PayServiceImpl implements PayService {
    @Override
    public String pay() {
        return "payResult";
    }
}

4.添加 RESTfu API接口

文件:Demo4Book/sca/chapterfour/use-nacos-spring-boot/src/main/java/com/alibaba/cloud/youxia/controller/PayController.java

java
package com.alibaba.cloud.youxia.controller;

import com.alibaba.cloud.youxia.service.PayService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController(value = "/pay")
public class PayController {

    @Resource
    private PayService payService;

    @GetMapping(value = "/toPay")
    public String pay(){
        payService.pay();
        return "success!";
    }
}

5.注册实例

启动服务后,在 Nacos 注册中心的控制台中可以看到服务“use-nacos-spring-boot”已经注册成功,如图 4-4 所示。

🚀 4.3.2 【实例】用Spring Cloud Alibaba Discovery接入

项目代码:Demo4Book/sca/chapterfour/use-spring-cloud-alibaba-discovery

本实例,先用 IDEA 创建一个 Spring Cloud Alibaba 项目,然后用 Spring Cloud Alibaba Discovery 将其接入 Nacos 注册中心。

1.添加 POM 依赖

文件:Demo4Book/sca/chapterfour/use-spring-cloud-alibaba-discovery/pom.xml

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>
        <artifactId>chapterfour</artifactId>
        <groupId>com.alibaba.youxia</groupId>
        <version>1.0.0.release</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>use-spring-cloud-alibaba-discovery</artifactId>
    <groupId>com.alibaba.youxia</groupId>
    <version>1.0.0.release</version>
    <packaging>jar</packaging>
    <name>use-spring-cloud-alibaba-discovery</name>
    <description>用“Spring Cloud Alibaba + Nacos”开发Dubbo应用和Restful应用</description>

    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.httpcomponents</groupId>
                    <artifactId>httpclient</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-dubbo</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
    </dependencies>
</project>

2.添加属性配置

文件:Demo4Book/sca/chapterfour/use-spring-cloud-alibaba-discovery/src/main/resources/bootstrap.yaml

yaml
dubbo:
  scan:
    base-packages: com.alibaba.cloud.youxia
  protocol:
    name: dubbo
    port: -1
spring:
  application:
    name: use-spring-cloud-alibaba-discovery
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: c7ba173f-29e5-4c58-ae78-b102be11c4f9
        group: use-spring-cloud-alibaba-discovery
server:
  port: 8089

3.其他代码

文件:Demo4Book/sca/chapterfour/use-spring-cloud-alibaba-discovery/src/main/java/com/alibaba/cloud/youxia/service/GoodServiceImpl.java

java
package com.alibaba.cloud.youxia.service;

import org.apache.dubbo.config.annotation.DubboService;

@DubboService(group = "example11",version = "1.0.0")
public class GoodServiceImpl implements GoodService{
    @Override
    public String getGoodName() {
        return "goodName";
    }
}

文件:Demo4Book/sca/chapterfour/use-spring-cloud-alibaba-discovery/src/main/java/com/alibaba/cloud/youxia/controller/GoodController.java

java
package com.alibaba.cloud.youxia.controller;

import com.alibaba.cloud.youxia.service.GoodService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController(value = "/good")
public class GoodController {

//    @DubboReference(group = "example11",version = "1.0.0")
//    private GoodService goodService;

    @Autowired
    private GoodService goodService;

    @GetMapping(value = "getGoodName")
    public String getGoodName(){
        return goodService.getGoodName();
    }

}

4.注册实例

应用在启动后会注册到 Nacos 注册中心中。

🚀 4.4 用“NacosNamingService类 + @EnableDiscoveryClient”实现服务的注册/订阅

软件开发人员使用Spring Cloud Alibaba开发Dubbo服务和Spring Cloud服务,其中Spring Cloud 服务依赖 Dubbo 服务,这样就可以在业务中用 Spring Cloud Alibaba 整合 Dubbo 服务和Spring Cloud 服务。Spring Cloud Alibaba 支持如下两种通信协议:

  • Dubbo 通信协议:用 Dubbo 作为最底层的 RPC框架。
  • Spring Cloud 通信协议:用 Spring Cloud 作为最底层的 RPC 框架。

Spring Cloud Alibaba用“NacosNamingService类+ @EnableDiscoveryClient”实现服务注册/订阅,具体包含如下两个模块。

spring-cloud-starter-alibaba-nacos-discovery:封装了 Nacos Naming(注册中心),并通过 Nacos Client 调用与 Nacos 服务注册/订阅功能相关的 Open API。

spring-cloud-starter-dubbo:封装了Dubbo(RPC框架)的部分功能。

下面我们来分析服务注册/订阅的核心原理。

4.4.1 服务注册的原理

待补充

4.4.2.服务订阅的原理

待补充

🚀 4.4.3 【实例】通过服务幂等性设计验证服务的注册/订阅

所谓幂等性设计,即一次请求和多次请求某一个资源会得到同样的结果。用数学的语言来表达就是:f(x)=f(f(x))。比如,求绝对值的函数,abs(x)=abs(abs(x))。

为什么开发人员需要幂等性设计?在开发人员将系统解耦隔离后,服务间的调用可能会有3个状态:成功(Success)、失败(Failed)、超时(Timeout)。

“成功”和“失败”都是有明确原因的状态。但“超时”是没有原因的状态,可能是在网络传输过程中出现了丢包,也可能是请求没有到达应用服务,还有可能是“请求到达了,但没有正常返回结果”等。于是,调用方完全不知道下游系统是否存在网络问题、是否收到了请求、收到的请求是处理成功或者失败,还是在响应时遇到了网络的问题。

服务幂等性设计涉及的主要项目模块见表 4-2

idempotent 美[aɪ'dempətənt] n. 幂等

表 4-2 服务幕等性设计涉及的主要项目模块

模块名称模块功能描述
distributed-uuid-server分布式发号器服务,在分布式环境中,为服务产生跨实例的全局唯一性ID
idempotent-design-user-api公共 Dubbo 接口,对消费者暴露 SDK
idempotent-design-user-client服务消费者
idempotent-design-user-server服务提供者

1.幂等性设计的思路

本实例模拟电商系统中商品库存被扣除的业务场景--如果用户下单成功,则扣除对应商品的库存。

本实例通过“UUID + Redis”判重来实现商品扣库存的幂等性设计,底层基础框架采用的是Spring Cloud Alibaba,具体思路如下:

(1)在 idempotent-design-user-client 模块中封装 RESTfulAPI 作为消费者,来调用库存扣减服务 idempotent-design-user-server。

(2)开启消费者的 Dubbo 超时重试机制,并设置消费超时时间。

(3)通过动态开关控制是否开启幂等设计。

如果开启了幂等性设计,则消费者会通过分布式发号器服务生产一个全局唯一的ID,并传递给服务提供者。服务提供者解析出全局唯一的ID,并缓存这个ID,且设置缓存的过期时间,比如 3s。这样,在 3s 内即使消费者一直重复请求,也不会影响数据的一致性,这样就能确保接口的幂等性。

如果没有开启幂等性设计,则消费者直接调用扣库存服务。如果出现服务调用超时,则Dubbo 会自动重试,或者消费者主动重试,从而导致服务多次扣减库存。

这样就会出现错误的业务执行过程:业务只发起了一次扣库存的请求,但是服务提供者实际上执行了多次扣库存的数据库变更。

2.具体代码实现

(1)分布式发号器服务distributed-uuid-server。

分布式全局 ID 采用雪花算法,具体实现如下:

java
package com.alibaba.cloud.youxia.service.impl;

public class SnowFlake {

    /**
     * 起始的时间戳
     */
    private final static long START_STMP = 1480166465631L;

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;   //机器标识占用的位数
    private final static long DATACENTER_BIT = 5;//数据中心占用的位数

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private long datacenterId;  //数据中心
    private long machineId;     //机器标识
    private long sequence = 0L; //序列号
    private long lastStmp = -1L;//上一次时间戳

    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

    /**
     * 产生下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStmp == lastStmp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }

        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
            | datacenterId << DATACENTER_LEFT       //数据中心部分
            | machineId << MACHINE_LEFT             //机器标识部分
            | sequence;                             //序列号部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private long getNewstmp() {
        return System.currentTimeMillis();
    }
}

暴露分布式发号器服务的服务提供者的具体代码如下所示:

java
package com.alibaba.cloud.youxia.service.impl;

import com.alibaba.cloud.youxia.config.SnowflakeConfig;
import com.alibaba.cloud.youxia.config.SnowflakeInfo;
import com.alibaba.cloud.youxia.service.DistributedService;
import com.alibaba.cloud.youxia.util.NetUtils;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

@DubboService(version = "1.0.0",group = "distributed-uuid-server")
public class DistributedServiceImpl implements DistributedService {
    @Autowired
    private SnowflakeConfig snowflakeConfig;
    Map<String, SnowFlake> snowFlakeHandlerMap = new ConcurrentHashMap<>();

    @Override
    public long nextId(final long datacenterId, final long machineId) {
        final long sdatacenterId = datacenterId;
        final long smachineId = machineId;
        final String handler = sdatacenterId + "_" + smachineId;
        SnowFlake snowFlake;
        if (snowFlakeHandlerMap.containsKey(handler)) {
            snowFlake = snowFlakeHandlerMap.get(handler);
            return snowFlake.nextId();
        } else {
            snowFlake = new SnowFlake(datacenterId, machineId);
            snowFlakeHandlerMap.putIfAbsent(handler, snowFlake);
            snowFlake = snowFlakeHandlerMap.get(handler);
            return snowFlake.nextId();
        }
    }

    @Override
    public long nextId() {
        List<SnowflakeInfo> config = snowflakeConfig.getConfig();
        String localAddress = NetUtils.getLocalAddress();
        SnowflakeInfo snowflakeInfo = config.stream().filter(s -> Objects.equals(s.getIp(), localAddress)).findFirst()
                .orElse(null);
        long dataCenterId = Optional.ofNullable(snowflakeInfo).map(SnowflakeInfo::getDataCenterId).orElse(0L);
        long machineId = Optional.ofNullable(snowflakeInfo).map(SnowflakeInfo::getMachineId).orElse(0L);
        return nextId(dataCenterId, machineId);
    }
}

(2)消费者服务idempotent-design-user-client。

在服务 idempotent-design-user-client 中,通过 RESTfuI API调用库存服务扣减商品的库存,具体代码如下:

java
package com.alibaba.cloud.youxia.controller;

import com.alibaba.cloud.youxia.bo.Example2ProductBo;
import com.alibaba.cloud.youxia.config.NacosConfig;
import com.alibaba.cloud.youxia.dto.GoodDTO;
import com.alibaba.cloud.youxia.request.GoodServiceRequest;
import com.alibaba.cloud.youxia.response.DefaultResult;
import com.alibaba.cloud.youxia.service.DistributedService;
import com.alibaba.cloud.youxia.service.GoodService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/good")
public class GoodController {
    @DubboReference(version = "1.0.0", group = "idempotent-design-user-server", retries = 4,timeout = 3000)
    private GoodService goodService;

    @DubboReference(version = "1.0.0", group = "distributed-uuid-server")
    private DistributedService distributedService;

    @Autowired
    private NacosConfig nacosConfig;

    @PostMapping(value = "/updataGoodNum")
    public DefaultResult<GoodDTO> updateGoodNum(@RequestParam("goodId") String goodId) {
        long uuid = distributedService.nextId(7, 8);
        GoodServiceRequest<Example2ProductBo> request = new GoodServiceRequest<Example2ProductBo>();
        Example2ProductBo example2ProductBo = new Example2ProductBo();
        example2ProductBo.setGoodId(Long.valueOf(goodId));
        request.setRequestData(example2ProductBo);
        if (nacosConfig.isMideng()) {
            request.setUuid(uuid + "");
        }
        return goodService.updateGoodNum(request);
    }
}

(3)服务提供者idempotent-design-user-server

在服务 idempotent-design-user-server 中,对比开启幂等性设计和关闭幂等性设计时接口处理过程的不同之处,具体代码如下:

java
package com.alibaba.cloud.youxia.service.impl;

import com.alibaba.cloud.youxia.bo.Example2ProductBo;
import com.alibaba.cloud.youxia.dto.GoodDTO;
import com.alibaba.cloud.youxia.entity.Example2ProductEntity;
import com.alibaba.cloud.youxia.mapper.Example2ProductMapper;
import com.alibaba.cloud.youxia.request.GoodServiceRequest;
import com.alibaba.cloud.youxia.response.DefaultResult;
import com.alibaba.cloud.youxia.service.GoodService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.concurrent.TimeUnit;

@DubboService(version = "1.0.0",group = "idempotent-design-user-server")
public class GoodServiceImpl implements GoodService {

    @Autowired
    private Example2ProductMapper example2ProductMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public DefaultResult<GoodDTO> updateGoodNum(GoodServiceRequest goodServiceRequest) {
        DefaultResult<GoodDTO> result=new DefaultResult<>();
        GoodDTO returnItem=new GoodDTO();
        Example2ProductBo example2ProductBo=(Example2ProductBo)goodServiceRequest.getRequestData();
        //开启幂等性设计
        if(!StringUtils.isEmpty(goodServiceRequest.getUuid())){
            long uuid=Long.parseLong(goodServiceRequest.getUuid());
            if(null!=redisTemplate.opsForValue().get(uuid)){
                result.setData(new GoodDTO());
                result.setMessage("uuid:"+uuid+" 已经连续访问多次!");
                return result;
            }else{
                redisTemplate.opsForValue().set(uuid,true,5, TimeUnit.SECONDS);
            }
        }
        try {
            //设置执行延时时间2s
            Thread.sleep(2000);
        }catch (InterruptedException e){
            System.out.println(e.getMessage());
        }
        List<Example2ProductEntity> queryResult1= example2ProductMapper.queryGoodInfoByGoodId(example2ProductBo);
        if(!CollectionUtils.isEmpty(queryResult1)){
            Example2ProductEntity item=queryResult1.get(0);
            System.out.println("开始扣减库存,扣除之前的商品库存为:"+item.getNum()+" 商品ID为:"+item.getGoodId());
        }
        example2ProductMapper.updateGoodNum(example2ProductBo);
        List<Example2ProductEntity> queryResult2= example2ProductMapper.queryGoodInfoByGoodId(example2ProductBo);
        if(!CollectionUtils.isEmpty(queryResult2)){
            Example2ProductEntity item=queryResult2.get(0);
            System.out.println("开始扣减库存,扣除之后的商品库存为:"+item.getNum()+" 商品ID为:"+item.getGoodId());
        }
        returnItem.setGoodId(queryResult2.get(0).getGoodId());
        returnItem.setNum(queryResult2.get(0).getNum());
        result.setData(returnItem);
        result.setCode("200");
        result.setMessage("库存扣减成功!!!!!");
        return result;
    }
}

3.场景验证

要验证幂等性设计,则需要准备相关的数据,并对比开启幂等性设计和关闭幂等性设计对数据的影响。

(1)测试数据准备。目前商品库存为 100,向商品表example2_product 中插入一个测试商品商品 ID为 7878,具体代码如下:

sql
insert into example2_product value(3477374334,7878,100,'苹果笔记本电脑'):
commit;

(2)按顺序启动服务 distributed-uuid-server、idempotent-design-user-server 和 idempotent-design-user-client,幂等服务的注册列表如图4-8所示,3个服务完成注册。

(3)使用swagger(http://127.0.0.1:7889/swagger-ui.html)验证没有开启幂等性设计的库存扣减的场景,在扣减库存过程中,需要增加当前请求的耗时:用线程机制让当前请求对应的线程休眠2s,并模拟在请求调用过程中的网络超时故障。

在增加耗时后,idempotent-design-user-client会报超时异常,具体日志如下:

org.apache.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer. start time: 2025-02-23 11:54:36.313, end time: 2025-02-23 11:54:37.326, client elapsed: 1 ms, server elapsed: 1012 ms, timeout: 1000 ms, request: Request [id=5, version=2.0.2, twoway=true, event=false, broken=false, data=null], channel: /10.0.1.1:51667 -> /10.0.1.1:26756
	at org.apache.dubbo.remoting.exchange.support.DefaultFuture.doReceived(DefaultFuture.java:210) ~[dubbo-2.7.8.jar:2.7.8]
	at org.apache.dubbo.remoting.exchange.support.DefaultFuture.received(DefaultFuture.java:175) ~[dubbo-2.7.8.jar:2.7.8]

通过 SQL语句查询对应商品的库存值可以发现,库存值已经由 100 变为 95(如图 4-9 所示),这说明服务器端实际扣了5次。如果是正常扣减库存,则应该只扣减一次,库存值应该为99。

(4) 验证开启幂等性设计的库存扣减的场景。

还原测试商品的库存值为 100,并通过 Swagger 模拟用户调用。如图 4-10 所示,返回幕等性设计已经生效的提示信息。

通过 SQL语句查询对应商品,发现库存值为 99,如图 4-11所示。

idempotent-design-user-cient 在调用idempotent-design-user-server 时,配置了超时重试次数为 4,具体代码如下:

java
@DubboReference(version = "1.0.0", group = "idempotent-design-user-server", retries = 4,timeout = 1000)
private GoodService goodService;

如果超时,则理论上会执行5次库存扣减。在幂等性设计生效后,有效地拦截了4次库存扣减,保证了数据的一致性。

🚀 4.5 用“Ribbon + Nacos Client”实现服务发现的负载均衡

Spring Cloud Alibaba 可以用 Ribbon 和 Nacos Client 实现服务发现的负载均衡。开发人员只需要依赖它的 Jar 包,即可实现“开箱即用”。

🚀 4.5.1 为什么需要负载均衡

通过服务发现,可以发现一个能够提供服务的实例信息列表。但应用需要先采用策略从这个包含一组 IP 地址的服务列表中选出一个IP 地址,然后对其发起 RPC 请求。负载均衡就是用来解决这个问题的,通常包括客户端负载均衡和服务器端负载均衡。

负载均衡器(Load Balancer,LB)的作用是: 将用户的请求按照负载均衡算法分配到多个服务上,从而实现系统的高可用

🚀 4.5.2 【实例】用“Ribbon + Nacos Client”实现负载均衡

Ribbon 是一个负载均衡器。Ribbon 是基于客户端负载均衡算法的,所以需要开发人员主动开启负载均衡。

Spring Cloud Alibaba 采用 Ribbon 来实现负载均衡,从而提升服务订阅者调用的高可用性。

项目代码:Demo4Book/sca/chapterfour/ribbon-discovery-spring-cloud-alibaba-provider

项目代码:Demo4Book/sca/chapterfour/ribbon-discovery-spring-cloud-alibaba-consumer

1.添加负载均衡算法

在 Spring Cloud Alibaba 中,通过 NacosRule 类来定义负载均衡算法,具体代码如下所示:

文件(SCA jar里的):com.alibaba.cloud.nacos.ribbon.NacosRule

java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.alibaba.cloud.nacos.ribbon;

import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.DynamicServerListLoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;

public class NacosRule extends AbstractLoadBalancerRule {
    private static final Logger LOGGER = LoggerFactory.getLogger(NacosRule.class);
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
    @Autowired
    private NacosServiceManager nacosServiceManager;

    public NacosRule() {
    }

    public Server choose(Object key) {
        try {
            String clusterName = this.nacosDiscoveryProperties.getClusterName();
            String group = this.nacosDiscoveryProperties.getGroup();
            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer)this.getLoadBalancer();
            String name = loadBalancer.getName();
            NamingService namingService = this.nacosServiceManager.getNamingService(this.nacosDiscoveryProperties.getNacosProperties());
            List<Instance> instances = namingService.selectInstances(name, group, true);
            if (CollectionUtils.isEmpty(instances)) {
                LOGGER.warn("no instance in service {}", name);
                return null;
            } else {
                List<Instance> instancesToChoose = instances;
                if (StringUtils.isNotBlank(clusterName)) {
                    List<Instance> sameClusterInstances = (List)instances.stream().filter((instancex) -> {
                        return Objects.equals(clusterName, instancex.getClusterName());
                    }).collect(Collectors.toList());
                    if (!CollectionUtils.isEmpty(sameClusterInstances)) {
                        instancesToChoose = sameClusterInstances;
                    } else {
                        LOGGER.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", new Object[]{name, clusterName, instances});
                    }
                }

                Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);
                return new NacosServer(instance);
            }
        } catch (Exception var10) {
            Exception e = var10;
            LOGGER.warn("NacosRule error", e);
            return null;
        }
    }

    public void initWithNiwsConfig(IClientConfig iClientConfig) {
    }
}

Spring Cloud Alibaba 在服务发现的过程中,会根据服务实例的权重选出合适的服务实例。详细源码可以查阅 ExtendBalancer 类的 getHostByRandomWeight2()方法。

2.在应用中初始化 NacosRule

通过注解@Configuration 和@Bean 加载 NacosRule 类,具体代码如下所示:

文件:Demo4Book/sca/chapterfour/ribbon-discovery-spring-cloud-alibaba-consumer/src/main/java/com/alibaba/cloud/youxia/config/NacosRibbonRuleConfig.java

java
package com.alibaba.cloud.youxia.config;

import com.alibaba.cloud.nacos.ribbon.NacosRule;
import com.netflix.loadbalancer.IRule;
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 NacosRibbonRuleConfig {

    @Bean
    public IRule nacosRule(){
        return new NacosRule();
    }

    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

文件:Demo4Book/sca/chapterfour/ribbon-discovery-spring-cloud-alibaba-consumer/src/main/java/com/alibaba/cloud/youxia/config/NacosGlobalClientConfig.java

java
package com.alibaba.cloud.youxia.config;

import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Configuration;

@Configuration
@RibbonClient(name ="ribbon-discovery-spring-cloud-alibaba-provider",configuration =NacosRibbonRuleConfig.class)
public class NacosGlobalClientConfig {
}

3.定义消费者和服务提供者

服务提供者的具体代码如下所示:

java
package com.alibaba.cloud.youxia.controller;

import com.alibaba.cloud.youxia.service.RibbonDiscoveryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/provider")
public class ProviderController {

    @Autowired
    private RibbonDiscoveryService ribbonDiscoveryService;

    @GetMapping(value = "/getRibbonConfig")
    @ResponseBody
    public String testRestRibbon(){
        return ribbonDiscoveryService.getRibbonConfig();
    }
}
java
package com.alibaba.cloud.youxia.service.impl;

import com.alibaba.cloud.youxia.service.RibbonDiscoveryService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;

import java.net.InetAddress;

@DubboService(version = "1.0.0",group = "ribbon-provider")
public class RibbonDiscoveryServiceImpl implements RibbonDiscoveryService {

    @Autowired
    private Environment environment;

    @Override
    public String getRibbonConfig() {
        String port = environment.getProperty("local.server.port");
        String ip = "";
        try {
            ip = InetAddress.getLocalHost().toString();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        String result = "负载均衡成功!" + "当前机器节点IP地址为:" + ip + ":" + port;
        return result;
    }
}

本实例用一个 RESTfUI API接口作为服务消费者,调用具备负载均衡功能的服务提供者的接口,具体代码如下所示:

文件:Demo4Book/sca/chapterfour/ribbon-discovery-spring-cloud-alibaba-consumer/src/main/java/com/alibaba/cloud/youxia/controller/NacosRibbonController.java

java
package com.alibaba.cloud.youxia.controller;

import com.alibaba.cloud.youxia.service.RibbonDiscoveryService;
import com.alibaba.cloud.youxia.service.RibbonTestService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping(value = "/ribbon")
public class NacosRibbonController {

    @DubboReference(version = "1.0.0",group = "ribbon-provider",timeout = 8000)
    private RibbonDiscoveryService ribbonDiscoveryService;

    @DubboReference
    private RibbonTestService ribbonTestService;

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping(value = "/test")
    public String doRibbon(){
        return restTemplate.getForObject("http://ribbon-discovery-spring-cloud-alibaba-provider/provider/getRibbonConfig",String.class);
    }
}

4.通过 Nacos 的控制台配置服务提供者对应实例的权重

服务提供者总共有 3 个实例,分别是 192.168.0.123:8079、192.168.0.123.8069、192.168.0.123:8099。

IDEA一份代码启动多个实例:

3个实例的初始化权重都为1。当服务消费者访问服务提供者时,3个实例的访问流重是均衡的。

验证如下情况:

(1) 将192.168.0.123:8079的权重设置为0,按照负载均衡策略,这个实例应该没有流量。通过 Swagger 进行验证(本实例已经集成了Swagger,读者可以通过对应的地址进行访问)请求只能到达其他两个节点,只返回 192.168.0.123:8069和 192.168.0.123:8099。

(2) 将192.168.0.123:8079和192.168.0.123:8099的权重设置为0,按照负载均衡策略,只有 192.168.0.123:8069 能被 Swagger 访问到。

🚀 4.6 用CP模式和AP模式来保持注册中心的数据一致性

实现数据一致性的技术方案有很多,Nacos通过 Soft-Jraft 算法来实现注册中心数据的一致性。

🚀 4.6.1 了解CAP理论

CAP 是分布式系统中最基础的理论,即一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Avaiiabiity)和分区容错性(Partition tolerance)这三项中的两项。

🚀 数据一致性

在 CAP 理论中,数据一致性是对数据“写”的要求。数据在收到“写”请求后才会发生变化,而数据的“写”请求包括数据的增加、删除、修改这三种。

在分布式环境中,对于数据的“写”请求,需要确保多个节点的数据(例如数据库存储的主从节点)保持一致。数据一致性分为以下两种:

  • 强数据一致性: 数据在各个节点之间随时保持完全一致,即数据的“写”操作要么成功,要么失败。
  • 最终数据一致性: 数据在各个节点之间在时间阈值内同步成功,保持数据的最终一致性。

🚀 可用性

可用性在 CAP 理论里是对结果的要求。它要求系统内的节点无论是接收到“写”请求还是“读”请求,都能处理并返回响应结果。

可用性的两个前置条件:

  • 返回结果必须在合理的时间内,这个“合理的时间”是根据业务来定的;
  • 系统中能正常接收请求的所有节点都返回结果。

🚀 分区容错性

分布式存储系统有很多的节点,这些节点都是通过网络进行通信的。而网络是不可靠的,如果节点间的通信出现了问题,则称当前分布式存储系统出现了分区。分区并不一定是由网络故障引起的,也可能是由机器故障引起的。

分区容错性是指: 如果出现了分区问题,分布式存储系统还能继续运行;不能因为出现了分区问题,整个分布式节点都“罢工”了。

🚀 4.6.2 了解Nacos的CP模式和AP模式

Nacos 支持用 CP 模式和 AP 模式来确保其数据一致性。下面先介绍一下 CP 模式和 AP 模式。

🚀 AP 模式

AP 模式是基于 Nacos Server的内存来存储数据的,所以很难达到强一致性。

在 AP 模式下,Nacos 采用 Nacos Server 之间互相的数据同步,来实现数据在集群之间的同步和复制。大致有以下几种情况:

(1)如果集群中有5个节点,由于硬件故障某个节点无法使用,那客户端可以通过负载均衡将故障节点的服务注册请求重新路由到其他正常工作的4个节点中的一个,以保证 Nacos 集群的整体可用性。

(2)如果集群中有5个节点,有3个节点网络互通,其他2个节点网络出现了故障,形成了分区故障,则客户端被负载到 Nacos 集群中任意一个节点,可以正常完成服务注册/订阅,只是新注册的服务数据不能实时地同步到其他节点上。已经完成服务注册/订阅的应用,还可以正常通过自身的容错机制,保证在出现分区故障时 Nacos 集群的可用性。

(3)使用 HTTP 接口来同步变更服务信息,在排除网络故障之后,还是会有节点的资源达到阈值,并存在数据同步失败的风险。如果节点之间基于内存来维护服务信息,则会存在数据不一致的情况。可以通过重试机制来重试数据的同步,以实现一定值范围内的最终一致性。

🚀 CP 模式

在 CP 模式下,Nacos 采用 Raft 算法完成 Nacos Server 间数据的同步和复制操作。CP模式主要通过 Raft 算法来保持数据的强一致性。大致有以下几种情况。

(1) 如果集群中存在5个节点,在执行完成数据一致性算法之后,会产生一个 Leader 节点和4个Follower 节点。如果1个Follower 节点宕机,则Leader 节点会把到该节点的请求路由到其他3个正常的节点上。如果 Leader 节点宕机,则集群会重新选举,产生新的Leader 节点。在选举过程中,集群整体不可用。在选举过程中,如果发现当前集群中存活的节点数小于n/2+1(n代表集群中总的节点数),则选举不能正常进行,集群整体不可用。

CP 模式是牺牲一定的可用性来确保数据的强一致性。

(2) 集群中5个节点,出现了分区故障。1个分区包含1个Leader 节点和2个Follower点,另外1个分区只包含2个Follower 节点。只要Leader 节点不宕机,那它还是可以正常地处理请求。如果 Leader 节点所在的分区的节点总数小于“n/2+1”,则影响集群的重新选举(这种概率非常小)。总体上来看,还是可以确保分布式系统的容忍性的。

(3) 在CP 模式下,节点间的数据同步只能是从 Leader 节点到 Follower 节点。并且,数据同步采用持久化方式复制日志文件。在同步过程中,如果有1个节点最终同步失败,则数据变更整体失败,并回滚。

总体上来看,采用基于 Raft 算法后的CP 模型能够确保数据的强一致性。

4.6.3 了解Raft与Soft-Jraft

4.6.4 Nacos注册中心AP模式的数据一致性原理

4.6.5 Nacos注册中心CP模式的数据一致性原理

🚀 4.6.6【实例】用持久化的服务实例来验证注册中心的数据一致性

项目代码:Demo4Book/sca/chapterfour/persistence-discovery-spring-cloud-alibaba

Nacos 通过 Soft-Jraft 算法来持久化注册中心的数据,具体操作如下:

1.用“Spring Boot+ Spring Cloud Alibaba”初始化项目

(1)添加 Spring Boot和 Spring Cloud Alibaba 的相关 Jar 包。

(2)添加配置文件 bootstrap.yaml,具体代码如下所示:

文件:Demo4Book/sca/chapterfour/persistence-discovery-spring-cloud-alibaba/src/main/resources/bootstrap.yaml

yaml
dubbo:
  scan:
    base-packages: com.alibaba.cloud.youxia
  protocol:
    name: dubbo
    port: 26767
  registry:
    address: nacos://127.0.0.1:8848
spring:
  application:
    name: persistence-discovery-spring-cloud-alibaba
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: c7ba173f-29e5-4c58-ae78-b102be11c4f9
        group: persistence-discovery-spring-cloud-alibaba
        ephemeral: false
server:
  port: 7890

ephemeral,美[ɪˈfemərəl],短暂的

2.运行程序,观察效果

运行程序后,可以在 Nacos 控制台上看到持久化的实例已经注册成功,临时实例字段被设置为false,表示实例已被持久化到文件中了。

3.关闭程序并更改 Dubbo 端口号,验证注册中心数据的一致性

(1)关闭程序并更改 Dubbo 的端口号,具体代码如下所示:

yaml
server:
  Port: 7890
dubbo:
  Port: 26767

(2)重新运行程序,在 Nacos 控制台中会出现两个实例。

已经下线的实例(172.17.1.160:26734)的健康状态为 false,新上线的实例(172.17.1.160:26767)的健康状态为 true。Nacos 集群会永久地保存被设置为持久化类型的实例的元数据。

在 Nacos 的安装目录的文件路径“/data/naming/data/{命名空间 ID}”下,可以看到持久化的实例元数据。

4.7 用缓存和文件来存储Nacos的元数据

Nacos 的元数据是指 Nacos 服务注册与服务订阅的数据来源。Nacos 支持用缓存和文件来存储其元数据。

4.7.1 认识Nacos的元数据

Nacos 的元数据主要包含 Nacos 数据(如配置和服务)的描述信息,如服务版本、权重、灾策略、负载均衡策略、鉴权配置、各种自定义的标签(label)。从作用范围来看,元数据分为服务元数据、集群元数据及实例元数据,服务元数据、实例元数据及集群元数据之间的关系如图 4-18 所示。

4.7.2 用缓存存储Nacos的元数据

4.7.3 用文件存储Nacos的元数据

4.7.4 【实例】用Spring Cloud Alibaba整合Nacos和Dubbo的元数据

4.8 用Nacos Sync来实现应用服务的数据迁移

4.8.1 为什么要进行应用服务的数据迁移

4.8.2 如何完成应用服务的数据迁移

4.8.3 【实例】将Eureka注册中心中的应用服务数据迁移到Nacos注册中心中