Spring Cloud 初级篇

前言

学完SpringCloud零基础篇,还未引入SpringCloud的技术知识,构建了一个微服务架构:Order订单模块、Payment支付模块,它们通过RestTemplate实现了模块之间的相互调用;接着,在该基础上将按下图SpringCloud技术知识,一步步往上加,让它越来越庞大、复杂,但是,也越来越强大。

1.Eureka服务注册与发现

服务之间直接调用不就行了吗?为什么要引入服务注册中心?
有一句古话:“避免中间商赚差价”。
微服务a调微服务b,服务很少,a可以直接调b,问题是怕量变引起质变;比如说,一个病人直接去私人医院,一对一的医治专家为你服务,不用中间横着一道门诊挂号;但是,现在病人越来越多,今天这个微服务还能不能提供,这个专家还有没有剩号和逾号,今天到底有多少个病人通过这个专家挂号,我们需要监控、权鉴流量的管控,类似于在医院有一个门诊前台一样,多一个注册中心;通过注册中心,专家知道今天要为多少病人服务,今天有多少人挂了号,我们可以同时进行调度和协调。

1.1.Eureka基础知识

1.1.1.什么是服务治理

Spring Cloud 封装了Netflix公司开发的Eureka模块来实现服务治理
在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务与服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。

1.1.2.什么是服务注册

什么是服务注册与发现

Eureka采用了CS的设计架构,Eureka Server作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心。当服务启动的时候,会把当前自己服务器的信息,比如:服务地址、通讯地址等以别名方式注册到注册中心上。另一方(消费者/服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用。RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何rpc远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))

下左图是Eureka系统架构,右图是Dubbo的架构,请对比:

1.1.3.Eureka两组件

Eureka包含两个组件:Eureka Server和Eureka Client

Eureka Server提供服务注册服务
各个微服务节点通过配置启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。

Eureka Client通过注册中心进行访问
是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)

1.2.单机Eureka构建步骤

1.2.1.IDEA生成Eureka Server端服务注册中心,类似物业公司

a. 建Module
cloud-eureka-server7001

b. 改POM
1). 1.X和2.X的对比说明

以前的老版本(2018)

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

现在的新版本(当前使用2021.7)

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

2). 导入pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2021</artifactId>
        <groupId>com.neo.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-eureka-server7001</artifactId>

    <dependencies>
        <!-- eureka-server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.neo.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- boot web actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- 一般通用配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

c. 写YML
新建application.yml

server:
  port: 7001

eureka:
  instance:
    hostname: localhost # eureka服务端的实例名称
  client:
    # false表示不向注册中心注册自己
    register-with-eureka: false
    # false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url: 
      # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

d. 主启动
新建主启动类EurekaMain7001

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

e. 测试
启动访问http://localhost:7001
结果页面:

No application available 没有服务被发现,因为没有注册服务进来当然不可能有服务被发现。

1.2.2.Eureka Client端cloud-provider-payment8001将注册进Eureka Server成为服务提供者provider,类似尚硅谷学校对外提供授课服务

修改支付微服务cloud-provider-payment8001
a. 改POM
1). 1.X和2.X的对比说明

以前的老版本,别再使用

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

现在的新版本,当前使用

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2). pom.xml引入eureka-client依赖

<!-- eureka-client -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

b. 写YML
application.yml添加eureka-client配置

eureka:
  client:
    # 表示是否将自己注册进Eureka Server,默认为true
    register-with-eureka: true
    # 是否从Eureka Server抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url: 
      defaultZone: http://localhost:7001/eureka

c. 主启动
主启动类PaymentMain8001添加注解@EnableEurekaClient

d. 测试
1). 先要启动Eureka Server,再启动Eureka Client
2). 访问http://localhost:7001

3). 微服务注册名配置说明

e. 自我保护机制

1.2.3.Eureka Client端cloud-consumer-order80将注册进Eureka Server成为服务消费者consumer,类似来尚硅谷上课消费的各位同学

修改订单微服务cloud-consumer-order80
a. 改POM
pom.xml引入eureka-client依赖

<!-- eureka-client -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

b. 改YML
application.yml添加eureka-client配置

spring:
  application:
    name: cloud-order-service

eureka:
  client:
    # 表示是否将自己注册进Eureka Server,默认为true
    register-with-eureka: true
    # 是否从Eureka Server抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url: 
      defaultZone: http://localhost:7001/eureka

c. 主启动
主启动类OrderMain80添加注解@EnableEurekaClient

d. 测试
1). 先要启动Eureka Server:EurekaMain7001服务
2). 再要启动服务提供者Eureka Client:PaymentMain8001服务,后启动服务消费者Eureka Client:OrderMain80服务
3). 访问Eureka服务器http://localhost:7001

4). 查询订单信息
访问http://localhost/consumer/payment/get/4,查询通过,说明单机Eureka构建成功

1.2.4.bug

Failed to bind properties under 'eureka.client.service-url' to java.util.Map<java.lang.String, java.lang.String>

1.3.集群Eureka构建步骤

单机Eureka没有集群的高可用,一定会带来一个严重的问题:单点故障,有必要一步步地搭建Eureka的集群

1.3.1.Eureka集群原理说明

1.先启动eureka注册中心
2.启动服务提供者payment支付服务
3.支付服务启动后会把自身信息(比如服务地址)以别名方式注册进eureka
4.消费者order服务在需要调用接口时,使用服务别名去注册中心获取实际的RPC远程调用地址
5.消费者获得调用地址后,底层实际时利用HttpClient技术实现远程调用
6.消费者获得服务地址后会缓存在本地jvm内存中,默认每间隔30秒更新一次服务调用地址

问题:微服务RPC远程服务调用最核心的是什么?
高可用,试想一下你的注册中心只有一个only one,它出故障了那就呵呵了,会导致整个微服务环境不可用,所以解决办法:搭建Eureka注册中心集群,实现负载均衡+故障容错

Eureka集群工作原理:互相注册,相互守望
对外暴露是一个整体,内部是两台或多台服务构成

1.3.2.Eureka Server集群环境构建步骤

a. 参考cloud-eureka-server7001

b. 新建cloud-eureka-server7002

c. 改POM
复制cloud-eureka-server7001的pom.xml依赖粘贴

<dependencies>
    <!-- eureka-server -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.neo.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- boot web actuator -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- 一般通用配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

d. 修改映射配置
1). 找到C:\Windows\System32\drivers\etc\hosts

2). 修改映射配置添加进hosts文件

127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com

e. 写YML(以前单机)
互相注册,相互守望,即:7001注册7002,7002注册7001
1). 修改7001配置文件

server:
  port: 7001

eureka:
  instance:
    hostname: eureka7001.com # eureka服务端的实例名称
  client:
    # false表示不向注册中心注册自己
    register-with-eureka: false
    # false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://eureka7002.com:7002/eureka/

2). 修改7002配置文件

server:
  port: 7002

eureka:
  instance:
    hostname: eureka7002.com # eureka服务端的实例名称
  client:
    # false表示不向注册中心注册自己
    register-with-eureka: false
    # false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://eureka7001.com:7001/eureka/

f. 主启动
新建cloud-eureka-server7002主启动类

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

e. 测试
分别启动7001、7002服务
分别访问http://eureka7001.com:7001、http://eureka7002.com:7002,出现Eureka图标,并且DS Replicas相互指向,说明Eureka集群搭建成功。

1.3.3.将支付服务8001微服务发布到上面2台Eureka集群配置中

a. 直接修改YML,支付服务8001注册进7001、7002两台Eureka集群
注释掉defaultZone: http://localhost:7001/eureka,添加:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002:7002/eureka

1.3.4.将订单服务80微服务发布到上面2台Eureka集群配置中

a. 同样,直接修改YML

1.3.5.测试01

a.先要启动Eureka Server,7001/7002服务
b.再要启动服务提供者provider,8001
c.后要启动消费者,80
d.访问Eureka集群7001,7002
1).访问地址:http://eureka7001.com:7001,订单服务80和支付服务8001都成功注册进Eureka集群的7001

2).访问地址:http://eureka7002.com:7002,同样,订单服务80和支付服务8001也成功入驻Eureka集群的7002,而且Eureka集群的7001和7002相互指向,互相守望

e.访问http://localhost/consumer/payment/get/4
成功查询到支付信息

1.3.6.支付服务提供者8001集群环境构建

a. 参考cloud-provider-payment8001
b. 新建cloud-provider-payment8002

c. 改POM
直接复制cloud-provider-payment8001的pom.xml文件

d. 写YML
复制cloud-provider-payment8001的application.yml文件,修改server.port端口8001为8002

e. 主启动
f. 业务类
直接复制cloud-provider-payment8001的业务类粘贴

g. 修改8001/8002的Controller
现在Eureka注册中心是集群,微服务提供者8001、8002是集群,服务消费者80找的是Eureka注册中心上的微服务服务提供者;换言之,现在的8001、8002对外暴露的是同一个服务名称cloud-payment-service,但是调用方得知道具体调8001还是调8002,所以这里的微服务提供者的负载均衡就得发挥作用;
那么这里从8001开始改动Controller,而8002可以直接复制粘贴。
1). 修改Controller,注入配置文件服务端口

@Value("${server.port}")
private String serverPort;

2). 启动现在的Eureka微服务集群
启动:Eureka注册中心7001、7002 -> 服务提供者8001、8002 -> 服务消费者80

访问地址:http://eureka7001.com:7001或http://eureka7002.com:7002,,同一个订单服务名称下的两台机器8001、8002都成功注册进了Eureka注册中心

单独访问支付微服务8001、8002,正常访问ok

然后通过订单微服务80访问能正常访问,但问题是:查询结果都来自支付微服务集群的8001机器,没有8002机器的数据

原因是原程序订单微服务80的Controller调用地址写死了:

1.3.7.负载均衡

a. ❌bug: 订单服务访问地址不能写死
通过在Eureka上注册过的微服务名称调用

//public static final String PAYMENT_URL = "http://localhost:8001";
public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

然后热部署后重新访问http://localhost/consumer/payment/get/4,会报错nested exception is java.net.UnknownHostException: CLOUD-PAYMENT-SERVICE,因为微服务名称CLOUD-PAYMENT-SERVICE有多台机器,不知道具体访问哪台机器,接下来需要配置负载均衡,才能成功访问该微服务名称的集群

b. 使用@LoadBalanced注解赋予RestTemplate负载均衡的能力

c. ApplicationContextConfig
提前说一下Ribbon的负载均衡功能

package com.neo.springcloud.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 ApplicationContextConfig {

    @Bean
    @LoadBalanced // 使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

1.3.8.测试02

热部署后重新访问http://localhost/consumer/payment/get/4,8001/8002端口交替出现,实现了8001、8002的默认轮询的负载均衡:

Ribbon和Eureka整合后Consumer可以直接调用服务而不用再关心地址和端口号,且该服务还有负载均衡功能了🙂。

1.4.actuator微服务信息完善

前提:pom.xml已导入spring-boot-starter-web、spring-boot-starter-actuator依赖

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

1.4.1.主机名称:服务名称修改

a. 当前问题:含有主机名称

b. 修改cloud-provider-payment8001的YML
修改部分:eureka结点下添加

instance:
    instance-id: payment8001

完整内容:

server:
  port: 8001

spring:
  application:
    name: cloud-payment-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource  # 当前数据源操作类型
    driver-class-name: org.gjt.mm.mysql.Driver  # mysql驱动包
    url: jdbc:mysql://localhost:3306/db2021?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root

eureka:
  client:
    # 表示是否将自己注册进Eureka Server,默认为true
    register-with-eureka: true
    # 是否从Eureka Server抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      #defaultZone: http://localhost:7001/eureka
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002:7002/eureka  # 集群版
  instance:
    instance-id: payment8001

mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: com.neo.springcloud.entities  # 所有Entity别名类所在包

c. 修改之后

1.4.2.访问信息有IP信息提示

a. 当前问题:没有IP提示

b. 修改cloud-provider-payment8001的YML
修改部分:eureka.instance结点下添加

prefer-ip-address: true

完整内容:

server:
  port: 8001

spring:
  application:
    name: cloud-payment-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource  # 当前数据源操作类型
    driver-class-name: org.gjt.mm.mysql.Driver  # mysql驱动包
    url: jdbc:mysql://localhost:3306/db2021?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root

eureka:
  client:
    # 表示是否将自己注册进Eureka Server,默认为true
    register-with-eureka: true
    # 是否从Eureka Server抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      #defaultZone: http://localhost:7001/eureka
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002:7002/eureka  # 集群版
  instance:
    instance-id: payment8001
    prefer-ip-address: true # 访问路径可以显示IP地址

mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: com.neo.springcloud.entities  # 所有Entity别名类所在包

c. 修改之后

1.5.服务发现Discovery

目前已经完成了7001、7002、8001、8002、80五个微服务的调用和实现,那么,不排除微服务自身要对外提供一种功能,就是:我的IP是多少?我的服务名称是多少?我的端口号是多少?说白了即:盘点Eureka服务上这么多注册的机器,你的对外暴露的信息是什么?

a. 对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息
b. 修改cloud-provider-payment8001的Controller
注入DiscoveryClient对象,读取微服务名,根据微服务名获取实例信息(机器信息),如:服务名称、主机、端口号、IP地址
修改内容:

@Resource
private DiscoveryClient discoveryClient;

@GetMapping(value = "/payment/discovery")
public Object discovery() {
    List<String> services = discoveryClient.getServices();
    for (String element : services) {
        log.info("******element:" + element);
    }
    List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
    for (ServiceInstance instance : instances) {
        log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
    }
    return this.discoveryClient;
}

c. 8001主启动类,添加@EnableDiscoveryClient注解

d. 自测
1). 先要启动EurekaServer
2). 再启动8001主启动类,需要稍等一会儿
3). 访问http://localhost:8001/payment/discovery
返回了cloud-payment-service、cloud-order-service服务名称,说明Eureka上注册了这两个微服务

8001后台日志打印结果,说明有cloud-payment-service、cloud-order-service两个微服务,CLOUD-PAYMENT-SERVICE微服务下有两台实例机器8001、8002。

1.6.Eureka自我保护

1.6.1.故障现象

a. 概述
保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。

如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

1.6.2.导致原因

一句话:某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存
属于CAP里面的AP分支

1). 为什么会产生Eureka自我保护机制?
为了防止Eurea Client可以正常运行,但是与Eureka Server网络不通情况下,Eureka Server不会立刻将Eureka Client服务剔除

2). 什么是自我保护模式?
默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka将会注销该实例(默认90秒)。但当网络分区故障发生(延时、卡顿、拥挤)时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题——当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。

在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。
它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着
综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何可能健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。

1.6.3.怎么禁止自我保护

a. 注册中心eurekaServer端7001
1). 出厂默认,自我保护机制是开启的

eureka.server.enable-self-preservation=true

2). 使用eureka.server.enable-self-preservation=false 可以禁用自我保护模式

3). 关闭效果

4). 在eurekaServer端7001处设置关闭自我保护机制

b. 生产者客户端eurekaClient端8001
1). 默认

eureka.instance.lease-renewal-interval-in-seconds=30,单位为秒(默认是30秒)
eureka.instance.lease-expiration-duration-in-seconds=90,单位为秒(默认是90秒)

2). 配置

eureka:
  instance:
    # Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
    lease-renewal-interval-in-seconds: 1
    # Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
    lease-expiration-duration-in-seconds: 2

3). 测试
1.7001和8001都配置完成,先启动7001再启动8001

2.微服务8001能成功入驻eurekaServer7001

3.关闭微服务8001,查看eurekaServer7001上入驻的8001是还存在、还是说等一会或立刻被删掉了?
关闭8001微服务后,该服务立刻从7001上注销了

2.Zookeeper服务注册与发现

a. Eureka停止更新了你怎么办?
https://github.com/Netflix/eureka/wiki

Eureka 2.0 (Discontinued)
The existing open source work on eureka 2.0 is discontinued. The code base and artifacts that were released as part of the existing repository of work on the 2.x branch is considered use at your own risk.
Eureka 1.x is a core part of Netflix's service discovery system and is still an active project.

b. SpringCloud整合Zookeeper代替Eureka

2.1.注册中心Zookeeper

前提:在CentOS8上已经安装Zookeeper并联通,关闭防火墙或者配置好开放端口并开启防火墙,CentOS8并且能与主机网络互通
配置防火墙

# 查看防火墙开发端口
firewall-cmd --zone=public --list-ports
# 开发Zookeeper端口2181以便远程访问
firewall-cmd --zone=public --add-port=2181/tcp --permanent
# 重启防火墙
firewall-cmd --reload

1.zookeeper是一个分布式协调工具,可以实现注册中心功能
2.关闭Linux服务器防火墙后启动zookeeper服务器
3.zookeeper服务器取代Eureka服务器,zk作为服务注册中心

2.2.服务提供者

a. 新建cloud-provider-payment8004

b. POM
添加zookeeper客户端依赖

<dependencies>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.neo.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- SpringBoot整合zookeeper客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

c. YML

# 8004表示注册到zookeeper服务器的支付服务提供者端口号
server:
  port: 8004

spring:
  application:
    name: cloud-provider-payment
  cloud:
    zookeeper:
      connect-string: 192.168.200.128:2181

d. 主启动类

@SpringBootApplication
@EnableDiscoveryClient // 该注解用于向使用consul或zookeeper作为注册中心时注册服务
public class PaymentMain8004 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8004.class, args);
    }
}

e. Controller

@RestController
@Slf4j
public class PaymentController {

    @Value("${server.port}")
    private String serverPort;

    @RequestMapping(value = "/payment/zk")
    public String paymentzk() {
        return "springcloud with zookeeper:" + serverPort + "\t" + UUID.randomUUID().toString();
    }
}

f. 启动8004注册进zookeeper
a. 解决zookeeper版本jar包冲突问题

b. 排出zk冲突后的新POM修改部分

<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    <!-- 先排除自带的zookeeper3.5.3 -->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 添加zookeeper3.7.0版本 -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.7.0</version>
</dependency>

g. 验证测试

访问http://localhost:8004/payment/zk

h. 验证测试2

获得json串后用FeHelper前端助手工具查看试试

i. 思考
服务节点是临时节点还是持久节点

关闭服务8004后,一开始并没有从zookeeper中注销,在一定的时间范围内zookeeper给服务8004发心跳包,但是慢慢的发着发着每人回我了,突然发现zookeeper不客气了,瞬间把你干掉了。
从上述操作中得到的结论:服务节点是临时节点

重启服务8004后,会在zookeeper中注册一个新的流水号,但还是同一个服务名,在这里zookeeper比eureka稍微心狠一点点,不能说冷酷到底吧,但是也有一点硬汉风格。你一定时间内有就给我留下,没有就给清了,它比eureka更加干脆利落,可没有eureka那么含情脉脉。因此,我们可以知道它的服务节点是临时性的。

2.3.服务消费者

a. 新建cloud-consumerzk-order80

b. POM
复制8004的POM文件

<dependencies>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.neo.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- SpringBoot整合zookeeper客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

c. YML
复制8004的YML文件修改

server:
  port: 80

spring:
  application:
    name: cloud-consumer-order
  cloud:
    # 注册到zookeeper地址
    zookeeper:
      connect-string: 192.168.200.128:2181

d. 主启动

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

e. 业务类
1). 配置Bean

@Configuration
public class ApplicationContextConfig {

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

2). Controller

@RestController
@Slf4j
public class OrderZKController {

    public static final String INVOKE_URL = "http://cloud-provider-payment";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping(value = "/consumer/payment/zk")
    public String paymentInfo() {
        String result = restTemplate.getForObject(INVOKE_URL + "/payment/zk", String.class);
        return result;
    }
}

f. 验证测试

g. 访问测试地址
http://localhost//consumer/payment/zk

3.Consul服务注册与发现

3.1.Consul简介

3.1.1.是什么

https://www.consul.io/docs/intro

Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用Go语言开发
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网络,总之Consul提供了一种完整的服务网络解决方案。
它具有很多优点。包括:基于raft协议,比较简洁;支持健康检查,同时支持 HTTP 和 DNS 协议 支持跨数据中心的 WAN 集群 提供图形界面 跨平台,支持Linux、Mac、Windows

3.1.2.能干嘛

Spring Cloud Consul 具有如下特性:

a. 服务发现
提供HTTP和DNS两种发现方式。

b. 健康监测
支持多种方式,HTTP、TCP、Docker、Shell脚本定制化

c. KV存储
Key、Value的存储方式

d. 多数据中心
Consul支持多数据中心

f. 可视化Web界面

3.1.3.去哪下

https://www.consul.io/downloads.html

3.1.4.怎么玩

https://www.springcloud.cc/spring-cloud-consul.html

3.2.安装并运行Consul

3.2.1.官网安装说明

https://learn.hashicorp.com/consul/getting-started/install.html

3.2.2.下载完成后只有一个consul.exe文件,硬盘路径下双击运行,查看版本号信息

3.2.3.使用开发模式启动

a. consul agent -dev

b. 通过以下地址可以访问Consul的首页:http://localhost:8500

c. 结果页面

3.3.服务提供者

3.3.1.新建Module支付服务provider8006

cloud-providerconsul-payment8006

3.3.2.POM

添加consul-discovery依赖

<dependencies>
    <!-- SpringCloud consul-server -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    </dependency>
    <!-- SpringBoot 整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- 日常通用jar包配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3.3.3.YML

# consul服务端口号
server:
  port: 8006

spring:
  application:
    name: consul-provider-payment
  # consul注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #hostname: 127.0.0.1
        service-name: ${spring.application.name}

3.3.4.主启动类

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

3.3.5.业务类Controller

@RestController
@Slf4j
public class PaymentController {

    @Value("${server.port}")
    private String serverPort;

    @RequestMapping(value = "/payment/consul")
    public String paymentConsul() {
        return "springcloud with consul:" + serverPort + "\t" + UUID.randomUUID().toString();
    }
}

3.3.6.验证测试

1). 服务注册通过

2). 服务访问通过
http://localhost:8006/payment/consul

3.4.服务消费者

3.4.1.新建Module消费服务order80

cloud-consumerconsul-order80

3.4.2.POM

复制8006的pom.xml依赖

3.4.3.YML

复制8006的application.yml,修改端口和服务名

# consul服务端口号
server:
  port: 80

spring:
  application:
    name: consul-consumer-payment
  # consul注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #hostname: 127.0.0.1
        service-name: ${spring.application.name}

3.4.4.主启动类

@SpringBootApplication
@EnableDiscoveryClient // 该注解用于consul或者zookeeper作为注册中心时注册服务
public class OrderConsulMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderConsulMain80.class, args);
    }
}

3.4.5.配置Bean

复制80的ApplicationContextConfig配置Bean

@Configuration
public class ApplicationContextConfig {

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

3.4.6.Controller

@RestController
@Slf4j
public class OrderConsulController {

    public static final String INVOKE_URL = "http://consul-provider-payment";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping(value = "/consumer/payment/consul")
    public String paymentInfo() {
        String result = restTemplate.getForObject(INVOKE_URL + "/payment/consul", String.class);
        return result;
    }
}

3.4.7.验证测试

1). 服务注册通过

2). 服务访问通过
http://localhost/consumer/payment/consul

3.5.三个注册中心异同点

组件名 语言 CAP 服务健康检查 对外暴露接口 Spring Cloud集成
Eureka Java AP 可配支持 HTTP 已集成
Consul Go CP 支持 HTTP/DNS 已集成
Zookeeper Java CP 支持 客户端 已集成

3.5.1.CAP

C:Consistency(强一致性)
A:Availability(可用性)
P:Partition tolerance(分区容错性)
CAP理论关注粒度是数据,而不是整体系统设计的策略

3.5.2.经典CAP图

最多只能同时较好的满足两个。
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求;
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三大类:

CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

a. AP(Eureka)
AP架构
当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。
结论:违背了一致性C的要求,只满足可用性和分区容错,即AP

b. CP(Zookeeper/Consul)
CP架构
当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性
结论:违背了可用性A的要求,只满足一致性和分区容错,即CP

4.Ribbon负载均衡服务调用

4.1.概述

a. 是什么

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项,如连接超时、重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。

b. 官网资料
1).https://github.com/Netflix/ribbon/wiki/Getting-Started
2).Ribbon目前也进入维护模式

未来替换方案:

新组件功能将以其它替代平代替的方式实现

CURRENT REPLACEMENT
Hystrix rResilience4j
Hystrix Dashboard / Turbine Micrometer + Monitoring System
Ribbon Spring Cloud Loadbalancer
Zuul 1 Spring Cloud Gateway
Archaius 1 Spring Boot external config + Spring Cloud Config

c. 能干嘛
1). LB(负载均衡)

LB负载均衡(Load Balance)是什么
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。
常用的负载均衡有软件Nginx,LVS,硬件F5等。
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡的区别
Nginx是服务器负载均衡(集中式NB),客户端所有请求都会交给Nginx,然后由Nginx实现转发请求。即负载均衡是由服务端实现的。
Ribbon本地负载均衡(进程内NB),在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程调用技术。

1.集中式LB
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;

2.进程内LB
将LB逻辑集成到消费方,消费方从服务注册中心获知由哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB,它只是一个类库,集成与消费方进程,消费方通过它来获取到服务提供方的地址。

2). 前面讲解过了80通过轮询负载访问8001、8002

3).一句话(什么叫Ribbon):负载均衡 + RestTemplate调用

4.2.Ribbon负载均衡演示

4.2.1.架构说明

Ribbon在工作时分成两步
第一步先选择EurekaServer,它优先选择在同一个区域内负载较少的server.
第二步再根据用户指定的策略,再从server取到的服务注册列表中选择一个地址。
其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。

总结:Ribbon其实就是一个软负载均衡的客户端组件,它可用和其它所需请求的客户端结合使用,和eureka结合只是其中的一个实例。

4.2.2.POM

之前写样例时候没有引入spring-cloud-starter-ribbon也可以使用ribbon

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

猜测spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon引用
证明如下:可以看到spring-cloud-starter-netflix-eureka-client确实引入了Ribbon

4.2.3.RestTemplate的使用

a. 官网
https://docs.spring.io/spring-framework/docs/5.2.15.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html

b. getForObject方法/getForEntity方法
返回对象为响应体中数据转化成的对象,基本上可以理解为Json

返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等

c. postForObject/postForEntity

4.3.Ribbon核心组件IRule

4.3.1.IRule:根据特定算法从服务列表中选取一个要访问的服务

a. com.netflix.loadbalancer.RoundRobinRule
轮询

b. com.netflix.loadbalancer.RandomRule
随机

c. com.netflix.loadbalancer.RetryRule
先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务

d. WeightedResponseTimeRule
对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择

e. BestAvailableRule
会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务

f. AvailabilityFilteringRule
先过滤故障实例,再选择并发较小的实例

g. ZoneAvoidanceRule
默认规则,复合判断server所在区域的性能和server的可用性选择服务器

4.3.2.如何替换

a. 修改cloud-consumer-order80

b. 注意配置细节

官方文档明确给出了警告:
这个自定义配置类不能放在@ComponentScan所扫描的当前包以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。

c. 新建package
com.neo.myrule

d. 上面包下新建MySelfRule规则类

@Configuration
public class MySelfRule {

    @Bean
    public IRule myRule() {
        return new RandomRule(); // 定义为随机
    }
}

f. 主启动类添加@RibbonClient

@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)

g. 测试
访问http://localhost/consumer/payment/get/4,8001、8002随机出现

4.4.Ribbon负载均衡算法

4.4.1.原理

负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启后rest接口计算从1开始。
List instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
如:List [0] instances = 127.0.0.1:8002
List [1] instances = 127.0.0.1:8001

8001 + 8002 组合成为集群,它们共计2台机器,集群总数为2,按照轮询算法原理:
当总请求树为1时:1 % 2 = 1 对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求树为2时:2 % 2 = 0 对应下标位置为0,则获得服务地址为127.0.0.1:8002
当总请求树为3时:3 % 2 = 1 对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求树为4时:4 % 2 = 0 对应下标位置为0,则获得服务地址为127.0.0.1:8002
如此类推...

4.4.2.源码

解析默认的RoundRobinRule(负载均衡轮询算法)源码

private AtomicInteger nextServerCyclicCounter;
public RoundRobinRule() {
    this.nextServerCyclicCounter = new AtomicInteger(0);
}
// 选择哪台服务器
public Server choose(ILoadBalancer lb, Object key) {
    if (lb == null) {
        log.warn("no load balancer");
        return null;
    }

    Server server = null;
    int count = 0;
    while (server == null && count++ < 10) {
        List<Server> reachableServers = lb.getReachableServers(); // 健康的服务器数
        List<Server> allServers = lb.getAllServers(); // 服务器集群总数,如8001、8002两台
        int upCount = reachableServers.size();
        int serverCount = allServers.size();

        if ((upCount == 0) || (serverCount == 0)) {
            log.warn("No up servers available from load balancer: " + lb);
            return null;
        }

        int nextServerIndex = incrementAndGetModulo(serverCount);
        server = allServers.get(nextServerIndex); // 获取指定下标的服务器

        if (server == null) {
            /* Transient. */
            Thread.yield();
            continue;
        }

        if (server.isAlive() && (server.isReadyToServe())) {
            return (server);
        }

        // Next.
        server = null;
    }

    if (count >= 10) {
        log.warn("No available alive servers after 10 tries from load balancer: "
                + lb);
    }
    return server;
}

/**
 * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
 *
 * @param modulo The modulo to bound the value of the counter.
 * @return The next value.
 */
private int incrementAndGetModulo(int modulo) {
    for (;;) {
        int current = nextServerCyclicCounter.get();
        int next = (current + 1) % modulo;
        // JUC:CAS和自旋锁实现高并发下i++操作
        if (nextServerCyclicCounter.compareAndSet(current, next))
            return next;
    }
}
/**
 * Atomically sets the value to the given updated value
 * if the current value {@code ==} the expected value.
 *
 * <p><a href="package-summary.html#weakCompareAndSet">May fail
 * spuriously and does not provide ordering guarantees</a>, so is
 * only rarely an appropriate alternative to {@code compareAndSet}.
 *
 * @param expect the expected value
 * @param update the new value
 * @return {@code true} if successful
 */
public final boolean weakCompareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

4.4.3.手写

原理 + JUC(CAS + 自旋锁的复习)
自己试着写一个本地负载均衡器:
a. 7001/7002集群启动
b. 8001/8002微服务改造
controller

    @GetMapping(value = "/payment/lb")
    public String getPaymentLB() {
        return serverPort;
    }

c. 80订单微服务改造
1.ApplicationContextBean去掉注解@LoadBalanced

2.LoadBalancer接口

public interface LoadBalancer {
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

3.MyLB

@Component
public class MyLB implements LoadBalancer {

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    public final int getAndIncrement() {
        int current;
        int next;
        do {
            current = this.atomicInteger.get();
            next = current >= 2147483647 ? 0 : current + 1;
        } while (!this.atomicInteger.compareAndSet(current, next));
        System.out.println("*****第几次访问,次数next:" + next);
        return next;
    }

    // 负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,
    // 每次服务重启后rest接口计算从1开始。
    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}

4.OrderController

    @Resource
    private LoadBalancer loadBalancer;

    @Resource
    private DiscoveryClient discoveryClient;

    @GetMapping(value = "/consumer/payment/lb")
    public String getPaymentLB() {
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        if (instances == null || instances.size() <= 0) {
            return null;
        }
        ServiceInstance serviceInstance = loadBalancer.instances(instances);
        URI uri = serviceInstance.getUri();
        return restTemplate.getForObject(uri + "/payment/lb", String.class);
    }

5.测试
http://localhost/consumer/payment/lb

5.OpenFeign服务接口调用

5.1.概述

a. OpenFeign是什么

官网解释:
https://cloud.spring.io/spring-cloud-openfeign/reference/html

Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。
它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡

Feign是一个声明式的Web服务客户端,让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可
GitHub:https://github.com/spring-cloud/spring-cloud-openfeign

b. 能干嘛

Feign能干什么
Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的形式来配置它(以前是Dao接口上标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。

Feign集成了Ribbon
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用

c. Feign和OpenFeign两者区别

Feign OpenFeign
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务 OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其它服务。
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-feign</artifactId></dependency> <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>

5.2.OpenFeign使用步骤

服务调用由原来的Ribbon+RestTemplate,变为接口+注解风格的OpenFeign

5.2.1.接口+注解

a. 微服务调用接口 + @FeignClient

5.2.2.新建cloud-consumer-feign-order80

a. Feign在消费端使用

5.2.3.POM

<dependencies>
    <!-- openfeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- eureka-client -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.neo.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

5.2.4.YML

server:
  port: 80

spring:
  application:
    name: cloud-order-service

eureka:
  client:
    register-with-eureka: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002:7002/eureka

5.2.5.主启动

开启:@EnableFeignClients

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

5.2.6.业务类

a. 业务逻辑接口+@FeignClient配置调用provider服务

b. 新建PaymentFeignService接口并新增注解@FeignClient
使用:@FeignClient

@Component
@FeignClient("CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {

    @GetMapping(value = "/payment/get/{id}")
    CommonResult getPaymentById(@PathVariable("id") Long id);
}

c. 控制层Controller

@RestController
@Slf4j
public class OrderFeignController {

    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping(value = "/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymenById(@PathVariable("id") Long id) {
        return paymentFeignService.getPaymentById(id);
    }
}

5.2.7.测试

a. 先启动2个eureka集群7001、7002
b. 再启动2个微服务8001、8002
c. 启动OpenFeign
d. http://localhost/consumer/payment/get/4
8001、8002均衡出现

e. Feign自带负载均衡配置项

5.2.8.小总结

5.3.OpenFeign超时控制

5.3.1.超时设置,故意设置超时演示出错情况

a. 服务提供方8001故意写暂停程序

    @GetMapping(value = "/payment/feign/timeout")
    public String paymentFeignTimeout() {
        // 暂停几秒钟线程
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return serverPort;
    }

b. 服务消费方80添加超时方法PaymentFeignService

@Component
@FeignClient("CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {

    @GetMapping(value = "/payment/get/{id}")
    CommonResult getPaymentById(@PathVariable("id") Long id);

    @GetMapping(value = "/payment/feign/timeout")
    String paymentFeignTimeout();
}

c. 服务消费方80添加超时方法OrderFeignController

    @GetMapping(value = "/consumer/payment/feign/timeout")
    public String paymentFeignTimeout() {
        // openfeign-ribbon, 客户端一般默认等待1秒钟
        return paymentFeignService.paymentFeignTimeout();
    }

d. 测试
1). http://localhost/consumer/payment/feign/timeout

2). 错误页面

5.3.2.OpenFeign默认等待1秒钟,超过后报错

5.3.3.是什么

默认Feign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
yml文件中开启配置

OpenFeign默认支持Ribbon

5.3.4.YML文件里需要开启OpenFeign客户端超时控制

# 设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
  # 指的是建立连接所用的时间,适用于网络状态正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
  # 指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000

5.4.OpenFeign日志打印功能

5.4.1.是什么

Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。
说白了就是对Feign接口的调用情况进行监控和输出

5.4.2.日志级别

NONE:默认的,不显示任何日志;
BASIC:仅记录请求方法、URL、响应状态码及执行时间;
HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。

5.4.3.配置日志bean

@Configuration
public class FeignConfig {

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

5.4.4.YML文件里需要开启日志的Feign客户端

logging:
  level: 
    # feign日志以什么级别监控哪个接口
    com.neo.springcloud.service.PaymentFeignService: debug

5.4.5.后台日志查看

posted @ 2021-07-24 00:32  冰枫丶  阅读(230)  评论(0编辑  收藏  举报