分布式方案 - SpringCloud微服务工程思想

327 阅读10分钟

就是自己记录记录,也可能不准确。 准备在7天内完成这个板块的学习,根据篇幅可能只拆成3部分 + 1个实战,加油。

前言

带着这些问题来学习:

  • 单体系统和分布式系统?
  • 为什么要有分布式的工程方案?
  • 微服务和分布式有什么关系?
  • SprignBoot 和 SpringCloud的关系?
  • 该怎么学习微服务?

单体系统的优点与缺点

  • 优点:简单,部署方便,一个JAR包搞定。
  • 缺点:业务扩展性不好,业务与业务之间一般通过注入的方式,耦合性太强,随着业务的增加维护性差

根本:业务量大的工程,可以使用分布式系统来解决扩展性维护性的问题。

分布式系统

将整个系统拆分成多个能够独立运行的服务,在物理上是隔离的,服务之间通过网络进行通信。

优点缺点:

  • 服务与服务之间是独立的,维护和扩展性好,能实现集群部署。
  • 业务拆分,每个业务可以独立的部署,但是怎么拆分是一个问题,拆分的复杂度上升了。
  • 工程部署,原本一个服务器部署即可,现在需要多个服务器,手动部署无疑是困难的。
  • 负载均衡,多个集群之中如何选择某个服务,需要考虑。
  • 网络通信,基于 http 的接口访问,如何保证通信的可靠性。

为了解决扩展性维护性和一些因为业务大导致的单系统出现的缺点,提出的分布式系统,但是能感受到分布式系统同时也具备着一定的困难,因为有困难所以有框架。

微服务框架

微服务是分布式的一种落地,将工程拆分后形成的一个个独立的服务,我们可以简单理解成微服务。

为了管理这些服务,SringClod无疑是Java技术中首当其冲的框架。

SpringCloud是一个非常大的框架,它基于SpringBoot实现了很多框架的整合,而这里说的很多其他框架就有:

  • 网络通信:RestTemplate,Feign
  • 服务注册及发现:Nacos、Eureka
  • 分布式缓存数据库:Redis、Es...
  • 部署及管理:Docker、K8S
  • 消息消费:RocketMQ

所以如何去学习微服务,其实应该多做一些项目积累(可能还没不够也没关系),或者多阅读一些文章了解单体工程的缺陷,在大脑中构思出分布式工程的蓝图。根据分布式的特点以及痛点,针对相关的技术点去学习。

上面出现了那些英文字母,其实就是分布式的技术栈,所以 技术栈的学习 + 项目经验 + 原理分析 + 多思考 = 学会了分布式中的微服务。

0. 工程拆分

微服务的工程拆分:

  1. 每个服务都是独立的,拥有自己的数据库,只负责自己那部分的数据内容。
  2. 每个服务都能被独立部署,享有一个 ip + 端口,其他服务可以通过 ip + 端口的方式调用该服务的业务。

如何拆分?见我文章,思路是一样的,只是微服务拆出来的是一个单独能执行的工程:Maven的聚合工程搭建示意 - 掘金 (juejin.cn)

  • 父工程定义版本号以依赖版本管理
  • 子工程根据自己的业务导入相应的依赖,无需再指定版本号

1. 服务调用

服务调用:指 “微服务” 向外暴露的接口,其他微服务可以通过http的方式去请求接口,拿到对应服务提供的数据。

前面通过工程拆分,工程之间是独立的了,那么工程之间如何进行相互调用,可以通过以下两种http的方式:

1.1 RestTemplate

Spring家族中的一个类:org.soringframewoork.web.client.RestTemplate

使用方式:

  1. 在配置类中注入该类到IOC容器中
@Bean
public RestTemplate restTemplate(){
    return new RestTemplate();
}
  1. 在 service 中需要调用其他微服务接口的地方,加入restTemplate,完成调用,方法如下:
  • restTemplate.getForObject(url, User.class);
    • url:微服务暴露的地址
    • User.class:请求url返回的json反序列成什么类对象
// 1 注入restTemplate
@Autowired
private RestTemplate restTemplate;

public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    
    // 2.查询用户
    String url = "http://localhost:8081/user/" + order.getUserId();
    // url是服务接口的地址,返回的数据可以反序列为类对象
    User user = restTemplate.getForObject(url, User.class);
    
    // 3.封装user信息
    order.setUser(user);
    // 4.返回
    return order;
}

目前缺点:url是以硬编码的方式出现在代码中,后续通过注册中心改进。

1.2 Feign

2. 生产者与消费者

上面的服务之间通过 http 框架产生了调用被调用关系,通过调用关系可以将服务赋予一个名称:

  • 服务提供者:暴露接口给其它微服务调用
  • 服务消费者:调用其他微服务提供的接口
  • 一个服务可以同时是服务提供者,也可以是服务消费者

我们也知道服务调用时出现了:

  • 硬编码,这是服务器消费者如何获取服务提供者的地址信息的问题;
  • 分布式是集群的方式,如果A服务有多个,其他服务想调用A服务,如何选择具体是哪个A;
  • 如果A服务宕机了,那么又如何保证业务的正常执行。

解决方案呼之欲出:注册中心

3. 注册中心

以 Eureka 注册中心为例子,它是这样解决上面提出的三个问题的。

  1. 消费者如何获取服务提供的信息
    • 服务提供者向 注册中心 注册自己的信息
    • 服务消费者向 注册中心 拉取相应服务的信息
  2. 如果有多个服务的选择方式
    • 使用负载均衡的算法,从服务器列表中挑选一个
  3. 服务提供者的健壮性保证
    • 服务器提供者需要每30S向注册中心发送心跳请求
    • 注册中心会根据心跳情况更新服务器列表

所以架构中就会出现两个角色:

  • 服务端:像Eureka、Nacos的服务器
  • 客户端:我们写的模块代码

我们现在来学习一下服务端这个角色:

3.1 Eureka

3.1.1 搭建Eureka

创建一个新的module,在pom中加入依赖,这个依赖是服务端的意思

<!--eureka服务端-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

启动类上加上@EnableEurekaServer注解:

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

application.yml的配置信息:

server:
  port: 10086 #服务端口
spring:
  application:
    name: eurekaserver #euraka的服务名称,后面其他服务注册也需要带上名字
eureka:
  client:
    service-url: #euraka的地址信息,如果变成集群就用逗号隔开=
      defaultZone: http://127.0.0.1:10086/eureka

3.1.2 将服务注册到Eureka中

同理,其他的服务也需要导入一个依赖,这个依赖是客户端的意思:

<!--erueka客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

application.yml加入配置:

spring:
  application:
    name: userservice #注册到euraka的服务名称
eureka:
  client:
    service-url: #euraka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka

3.1.3 服务发现及远程调用

服务拉取是基于服务名称获取服务列表,将之前使用 RestTemplate 的服务调用改写一下:

// 2.查询用户
String url = "http://localhost:8081/user/" + order.getUserId();

改成,其实就是将http://localhost:8081替换成了服务名称

String url = "http://userservice/user/" + order.getUserId();

如果涉及到多个服务userservice调用,启动负载均衡即可,只需要在注册RestTemplate上加上@LoadBalanced注解即可:

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

(补充)负载均衡 Ribbon

上面的@LoadBalanced实现了负载均衡,底层是用过 Ribbon 实现的。

首先会被LoadBalancerInterceptor这个拦截器拦住 客户端发出的http请求。

有两种方式调整负载均衡:

  1. (全局配置方案)在OrderService服务中,通过 IRule 调整规则,例如调整成随机的负载均衡,之后通过服务调用请求的规则都是下面定义的内容:
@Bean
public IRule randomRule(){
    return new RandomRule();
}
  1. (针对某个服务名称配置负载均衡策略)通过配置文件调整负载均衡,com.netflix.loadbalancer.RandomRule和上面的工作是一样的。

请求userservice这个服务时,使用的规则是随机。

userservice: # 根据实际服务修改
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

(补充)饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会长,饥饿加载则会在项目启动时就创建,配置方式如下,在服务消费者的加上如下代码:

ribbon:
  eager-load:
    enabled: true #开启饥饿加载
    clients: #这是一个List,所以下面用集合的方式写
      - userservice  #指定饥饿加载的服务名称

3.2 Nacos

Nacos是阿里巴巴的产品,相比Eureka功能更加丰富,也是国内比较受欢迎的云原生管理平台。地址:home (nacos.io)

入门见这个资料:Nacos 快速开始

整体流程:

  1. 下载,启动Nacos服务
  2. 在程序中导包,写相应的配置

3.2.1 下载

Windows & Linux :Releases · alibaba/nacos (github.com)

下载后,解压即可。

3.2.2 配置

在 conf 文件中是配置内容,它是基于SpringBoot工程的,所以配置内容的书写方式和SpringBoot配置文件是一样的,

3.2.3 启动

注:Nacos的运行需要以至少2C4g60g*3的机器配置下运行。

Linux/Unix/Mac

启动命令(standalone代表着单机模式运行,非集群模式):

sh startup.sh -m standalone

如果您使用的是ubuntu系统,或者运行脚本报错提示[[符号找不到,可尝试如下运行:

bash startup.sh -m standalone

Windows

启动命令(standalone代表着单机模式运行,非集群模式):

startup.cmd -m standalone

3.2.4 在代码中配置

  1. 加入依赖:
<!--nacos的管理依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.5.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
  1. 注释掉其他注册中心的Client依赖,添加 Nacos 的客户端依赖
<!--<dependency>-->
<!--    <groupId>org.springframework.cloud</groupId>-->
<!--    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!--</dependency>-->

<!-- nacos客户端依赖包 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 注释掉其他服务注册的配置,添加 Nacos 的服务注册:

server-addr: localhost:8848根据实际情况,配置成 Nacos 服务器的地址。

#eureka:
#  client:
#    service-url: #euraka的地址信息
#      defaultZone: http://127.0.0.1:10086/eureka

spring:
  application:
    name: orderservice #euraka的服务名称
  cloud:
    nacos:
      server-addr: localhost:8848

3.2.5 服务分级存储 - 集群

在 yml 添加spring.cloud.nacos.discovery.集群名

服务 - 集群 - 实例

Nacos 支持将服务的多个实例,按照某种规则(例如地域)划分成集群,一个集群中有多个实例。

集群出现的意义:服务尽可能选择本地集群的服务,当本地集群不可访问时,再访问其它集群。简单理解就是同个圈子内的服务优先考虑。

配置集群的方式,在 yml 配置文件中添加 discover 内容如下:

spring:
  application:
    name: orderservice
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 定义一下集群名

(补充)负载均衡 NacosRule

上面配置完实例的集群后,还没有结束,需要通过负载均衡的策略才能去控制同个圈子内的服务优先考虑

和Eureka配置负载均衡的方式一致,在 yml 中配置,请求userservice这个服务时,使用的规则是优先同个集群:

userservice: # 根据实际服务修改
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule

当同个集群中找不到相应的服务后,才会去考虑其他集群的。