【SpringCloud Alibaba】(三)使用 Nacos 实现服务的自动注册与发现

1. 服务治理

如果系统采用了微服务的架构模式,随着微服务数量的不断增多,服务之间的调用关系会变得纵横交错,以纯人工手动的方式来管理这些微服务以及微服务之间的调用关系是及其复杂的,也是极度不可取的。

所以,需要引入服务治理的功能。服务治理也是在微服务架构模式下的一种最核心和最基本的模块,主要用来实现各个微服务的自动注册与发现

引入服务治理后,微服务项目总体上可以分为三个大的模块:服务提供者、服务消费者和注册中心,三者的关系如下图所示:

在这里插入图片描述

  1. 服务提供者会将自身提供的服务注册到注册中心,并向注册中心发送心跳信息来证明自己还存活,其中,心跳信息中就会包含服务提供者自身提供的服务信息
  2. 注册中心会存储服务提供者上报的信息,并通过服务提供者发送的心跳来更新服务提供者最后的存活时间,如果超过一段时间没有收到服务提供者上报的心跳信息,则注册中心会认为服务提供者不可用,会将对应的服务提供者从服务列表中剔除
  3. 服务消费者会向注册中心订阅自身监听的服务,注册中心会保存服务消费者的信息,也会向服务消费者推送服务提供者的信息
  4. 服务消费者从注册中心获取到服务提供者的信息时,会直接调用服务提供者的接口来实现远程调用

这里需要注意的是:服务消费者一般会从注册中心中获取到所有服务提供者的信息,根据具体情况实现对具体服务提供者的实例进行访问

2. 注册中心

从上面的分析可以看出,微服务实现服务治理的关键就是引入了注册中心,它是微服务架构模式下一个非常重要的组件,主要实现了服务注册与发现,服务配置和服务的健康检测等功能

2.1 服务注册与发现

  1. 服务注册:注册中心提供保存服务提供者和服务消费者的相关信息
  2. 服务发现:也可以理解为服务订阅,服务调用者也就是服务消费者,向注册中心订阅服务提供者的信息,注册中心会向服务消费者推送服务提供者的信息

2.2 服务配置

  1. 配置订阅:服务的提供者和消费者都可以向注册中心订阅微服务相关的配置信息
  2. 配置下发:注册中心能够将微服务相关的配置信息主动推送给服务的提供者和消费者

2.3 服务健康检测

注册中心会定期检测存储的服务列表中服务提供者的健康状况,例如服务提供者超过一定的时间没有上报心跳信息,则注册中心会认为对应的服务提供者不可用,就会将服务提供者踢出服务列表

2.4 常见的注册中心

能够实现注册中心功能的组件有很多,但是常用的组件大概包含:Zookeeper、Eureka、Consul、Etcd、Nacos 等。这里,就给大家简单介绍下这些能够实现注册中心功能的框架或组件:

  1. Zookeeper:接触过分布式或者大数据开发的小伙伴应该都知道,Zookeeper 是 Apache Hadoop 的一个子项目,它是一个分布式服务治理框架,主要用来解决应用开发中遇到的一些数据管理问题,例如:分布式集群管理、元数据管理、分布式配置管理、状态同步和统一命名管理等。在高并发环境下,也可以通过 Zookeeper 实现分布式锁功能
  2. Eureka:Eureka 是 Netflix 开源的 SpringCloud 中支持服务注册与发现的组件,但是后来闭源了
  3. Consul:Consul 是 HashiCorp 公司推出的开源产品,用于实现分布式系统的服务发现、服务隔离、服务配置,这些功能中的每一个都可以根据需要单独使用,也可以同时使用所有功能
  4. Etcd:etcd 是一个高度一致的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。它可以优雅地处理网络分区期间的领导者选举,即使在领导者节点中也可以容忍机器故障
  5. Nacos:我们重点说下 Nacos。Nacos 是阿里巴巴开源的一款更易于构建云原生应用的支持动态服务发现、配置管理和服务管理的平台,其提供了一组简单易用的特性集,能够快速实现动态服务发现、服务配置、服务元数据及流量管理,主要如下所示:
    • 服务注册:Nacos Client 会通过发送 REST 请求的方式向 Nacos Server 注册自己的服务,提供自身的元数据,比如IP地址、端口等信 息。Nacos Server 接收到注册请求后,就会把这些元数据信息存储在一个双层的内存 Map 中
    • 服务心跳:在服务注册后,Nacos Client 会维护一个定时心跳来持续通知 Nacos Server,说明服务一直处于可用状态,防止被剔除。默认 5s 发送一次心跳
    • 服务健康检查:Nacos Server 会开启一个定时任务用来检查注册服务实例的健康情况,对于超过 15s 没有收到客户端心跳的实例会将它的 healthy 属性置为 false (客户端服务发现时不会发现),如果某个实例超过 30 秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
    • 服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给 Nacos Server,获取上面注册的服务清 单,并且缓存在 Nacos Client 本地,同时会在 Nacos Client 本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地存
    • 服务同步:Nacos Server 集群之间会互相同步服务实例,用来保证服务信息的一致性

这里,我们选用的注册中心就是阿里巴巴开源的 Nacos。

3. 搭建 Nacos 环境

下载 Nacos 地址:下载 Nacos 地址

下载后,安装教程:Windows下Nacos的安装与使用

我这里下载的安装包为:nacos-server-2.1.0.zip

注意:nacos 默认是以集群的方式启动的。如果以单机方式启动有两种方法:①命令行加参数 ② 修改配置文件

3.1 启动 Nacos

方式一:命令行方式

解压 Nacos 安装包,并在命令行进入到 Nacos 的 bin 目录下执行如下命令以单机的方式启动 Nacos

startup.cmd -m standalone

方式二:修改配置文件

nacos/bin 目录下,打开 startup.cmd文件,修改第 26 行:

set MODE="cluster"

改为:

set MODE="standalone"

然后双击 startup.cmd,运行项目。启动成功后,访问:

http://192.168.10.88:8848/nacos

账号密码默认都是:nacos

界面如下:

在这里插入图片描述

我们进入到 Nacos 的服务管理-服务列表菜单下,如下所示:

在这里插入图片描述
可以看到,在 Nacos 的服务管理-服务列表菜单下还没有任何服务,接下来,我们就对项目的代码进行改造

3.2 集成 Nacos 注册中心

引入 Nacos 注册中心时,我们需要对项目的代码进行一定的改造,以便利用 Nacos 实现服务的注册与发现功能。这里需要三个步骤:

  1. pom.xml 引入 Nacos 依赖
  2. application.yml 配置 nacos
  3. 主启动类添加注解 @EnableDiscoveryClient

接下来按照这三个步骤改造微服务。

3.2.1 改造用户微服务

1、在用户微服务的 pom.xml 文件中添加 nacos 的服务注册与发现依赖,如下所示:

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

2、在用户微服务的 resources 目录下的 application.yml 文件中添 Nacos 注册中心的服务地址配置,如下所示

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

3、在用户微服务的启动类上标注 @EnableDiscoveryClient 注解,如下
所示:

@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan(value = {
    
    "com.zzc.user.mapper"})
@EnableDiscoveryClient
public class ShopUserApplication {
    
    

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

}

此时,就完成了对用户微服务的代码改造

4、启动用户微服务,并刷新 Nacos 页面,如下所示:

在这里插入图片描述
可以看到,用户微服务已经成功注册到 Nacos 中

3.2.2 改造其它微服务

我们可以用同样的方式来改造商品微服务和订单微服务的代码,改造好之后,分别启动商品微服务和订单微服务,并再次刷新 Nacos 的页面,如下所示:
在这里插入图片描述
可以看到,用户微服务、商品微服务和订单微服务都已成功注册到 Nacos

3.3 实现服务发现

按照整个项目的执行流程:用户执行下单操作时,订单微服务会调用用户微服务的接口获取用户的基本信息,会调用商品微服务的接口获取商品的基本信息。在订单微服务中校验用户的合法性和校验商品库存是否充足,如果用户合法并且商品库存充足,就会向订单数据表中记录订单信息并调用商品微服务的接口来扣减商品的库存

用户微服务和商品微服务作为服务的提供者,而订单微服务作为服务的消费者,如果要实现服务的发现功能,我们还需要对订单微服务的代码进行改造。将订单微服务中硬编码的用户微服务和商品微服务的 IP 地址和端口号修改成从 Nacos 中获取

创建动态服务地址方法

OrderServiceImpl 类中创建一个从 Nacos 中通过服务名称获取 IP 和端口号的方法 getServiceUrl(),并在 getServiceUrl() 方法中将 IP 和端口号拼接成 IP:PORT 的形式,如下所示:

@Autowired
private DiscoveryClient discoveryClient;

// 用户的微服务名称
private String userServer = "server-user";
// 商品的微服务名称
private String productServer = "server-product";

private String getServiceUrl(String serviceName){
    
    
    ServiceInstance serviceInstance = discoveryClient.getInstances(serviceName).get(0);
    return serviceInstance.getHost() + ":" + serviceInstance.getPort();
}

具体的实现方式:调用 DiscoveryClient 对象的 getInstances() 方法,并传入服务的名称,从 Nacos 注册中心中获取一个 ServiceInstance 类型的 List 集合,从 List 集合中获取第 1 个元素,也就是从 List 集合中获取到一个 ServiceInstance 对象,从 ServiceInstance 对象中获取到 IP 地址和端口号,并将其拼接成 IP:PORT 的形式

userServer 的值需要与用户微服务下的 application.yml 文件中的如下配置的值相同

spring:
  application:
    name: server-user

productServer 也同理

最后,OrderServiceImpl 类代码为:

@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    
    

    @Autowired
    private OrderItemMapper orderItemMapper;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    private String userServer = "server-user";
    private String productServer = "server-product";

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveOrder(OrderParamVo orderParamVo) {
    
    
        if (orderParamVo.isEmpty()){
    
    
            throw new RuntimeException("参数异常: " + JSONObject.toJSONString(orderParamVo));
        }
        //从Nacos服务中获取用户服务与商品服务的地址
        String userUrl = this.getServiceUrl(userServer);
        String productUrl = this.getServiceUrl(productServer);

        User user = restTemplate.getForObject("http://"+ userUrl + "/user/get/" + orderParamVo.getUserId(), User.class);
        if (user == null){
    
    
            throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParamVo));
        }
        Product product = restTemplate.getForObject("http://" + productUrl + "/product/get/" + orderParamVo.getProductId(), Product.class);
        if (product == null){
    
    
            throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParamVo));
        }
        if (product.getProStock() < orderParamVo.getCount()){
    
    
            throw new RuntimeException("商品库存不足: " + JSONObject.toJSONString(orderParamVo));
        }
        Order order = new Order();
        order.setAddress(user.getAddress());
        order.setPhone(user.getPhone());
        order.setUserId(user.getId());
        order.setUsername(user.getUsername());
        order.setTotalPrice(product.getProPrice().multiply(BigDecimal.valueOf(orderParamVo.getCount())));
        baseMapper.insert(order);
        OrderItem orderItem = new OrderItem();
        orderItem.setNumber(orderParamVo.getCount());
        orderItem.setOrderId(order.getId());
        orderItem.setProId(product.getId());
        orderItem.setProName(product.getProName());
        orderItem.setProPrice(product.getProPrice());
        orderItemMapper.insert(orderItem);
        Result<Integer> result = restTemplate.getForObject("http://" + productUrl + "/product/update_count/" + orderParamVo.getProductId() + "/" + orderParamVo.getCount(), Result.class);
        if (result.getCode() != HttpCode.SUCCESS){
    
    
            throw new RuntimeException("库存扣减失败");
        }
        log.info("库存扣减成功");
    }

    private String getServiceUrl(String serviceName){
    
    
        ServiceInstance serviceInstance = discoveryClient.getInstances(serviceName).get(0);
        return serviceInstance.getHost() + ":" + serviceInstance.getPort();
    }

}

可以看到,订单微服务调用商品微服务的扣减商品库存接口时,不再是硬编码商品微服务的 IP 地址和端口号了

至此,整个项目就改造完成了。

代码地址

代码已经上传至码云,码云地址

其中,数据库文件位于 db 文件夹下。

猜你喜欢

转载自blog.csdn.net/sco5282/article/details/131878471