松哥 spring cloud

文章目录

第 01 章 微服务的由来


介绍

2009年,Netflix 重新定了它的应用程序员的开发模型,这个算是微服务的首次探索。

20014年,《Microservices》,这篇文章以一个更加通俗易懂的方式,为大家定义了微服务。

为什么要用微服务?
互联网应用产品的两大特点:

  1. 用户需求快
  2. 用户群体大

在这样的情况下,我们需要构建一个能够灵活扩展,同时能够快速应对外部环境变化的一个应用,使用传统的开发方式,显然无法满足,这个时候,微服务就应运而生了。


什么是微服务

简单的来说,微服务就是将一个单一应用程序拆分为一组小型服务的方法,拆分完成之后,每一个服务都运行在独立的进程中,服务与服务之间采用轻量级的通信机制来进行沟通。(spring cloud 基于 http 的 Restful API)

每一个服务,都是围绕具体的业务进行构建,例如一个电商系统,订单服务,支付系统,物流系统,会员服务等等,这些拆分后的应用都是独立的应用,都可独立的部署到生产环境中。就是在采用微服务之后,我们的项目不在拘泥于一种语言。可以根据业务的上下文选择合适的语言和构建工具进行构建。

微服务可以理解为是 soa 的一个传承,一个本质的却别是一个真正的分布式,去中心化的。微服务的拆分比 soa 更彻底。


微服务的优势

  1. 复杂度可控
  2. 独立部署
  3. 技术选型灵活
  4. 较好的容错性
  5. 较强的可拓展性

使用 spring cloud 的优势

spring cloud 可以理解为微服务这种思想在 Java 领域的具体实现。spring cloud 在发展之初,就借鉴了微服务的思想,同时结合 spring boot,spring cloud 提供了组件的一键式启动和部署能力,极大的简化了微服务架构的落地。

spring cloud 这种框架,从设计之初,就充分的考虑了分部式架构演化所需要的功能,列如服务注册,配置中心,消息总线以及负载均衡等。这些功能都是可以插拔的形式提供出来的。这样,在分布式系统不断的演化过程当中,我们的 spring cloud 也可以非常方便的进化。


spring cloud 体系

spring cloud 包含的组件

  • Spring Cloud Netflix,这个组件,在 Spring Cloud 成立之初,立下了汗马功劳。但是, 2018 年的断更,也是 Netflix 掉链子了。
  • Spring Cloud Config,分布式配置中心,利用Git/Svn 来集中管理项目的配置文件
  • Spring Cloud Bus,消息总线,可以构建消息驱动的微服务,也可以用来做一些状态管理等
  • Spring Cloud Consul,服务注册发现
  • Spring Cloud Stream,基于 Redis、RabbitMQ、Kafka 实现的消息微服务
  • Spring Cloud OpenFeign,提供 OpenFeign 集成到 Spring Boot 应用中的方式,主要解决微服务之间的调用问题
  • Spring Cloud Gateway,Spring Cloud 官方推出的网关服务
  • Spring Cloud Cloudfoundry,利用 Cloudfoundry 集成我们的应用程序
  • Spring Cloud Security,在 Zuul 代理中,为 OAuth2 客户端认证提供支持
  • Spring Cloud AWS ,快速集成亚马逊云服务
  • Spring Cloud Contract,一个消费者驱动的、面向 Java 的契约框架
  • Spring Cloud Zookeeper,基于 Apache Zookeeper 的服务注册和发现
  • Spring Cloud Data Flow,在一个结构化的平台上,组成数据微服务
  • Spring Cloud Kubernetes,Spring Cloud 提供的针对 Kubernetes 的支持
  • Spring Cloud Function
  • Spring Cloud Task,短生命周期的微服务

spring cloud 和 spring boot 的版本关系
在这里插入图片描述

在这里插入图片描述

spring cloud 介绍


什么是 spring cloud

spring cloud 是一系列框架的集合。spring cloud 内部包含了许多框架,这些框架相互协作,共同构建分布式系统。利用这些组件可以非常方便的构建一个分布式系统。
官网:https://spring.io/projects/spring-cloud

核心特性

  1. 服务注册与发现
  2. 负载均衡
  3. 服务之间的调用
  4. 容错,服务降级,断路器
  5. 消息总线
  6. 分布式配置中心
  7. 链路器

版本名称

不同于其他的框架,spring cloud 版本名称是通过 A(Angel)、B(Brixton)、C(Camden)、D(Dalston)、E(Edgware)、F(Finchley)。。这样来命名的,这些名字使用了伦敦地铁站来进行命名。目前最新版是 H(Hoxton) 版

spring cloud 中,除了大的版本之外,还有一些小版本,小版本的命名方式如下:

  • M,M版是milestone的缩写,所以我们会看到一-些版本叫M1、M2
  • RC,RC是Release Candidate, 表示该项目处于候选状态,这是正式发版之前的一个状态
    我们会看到RC1、RC2
  • SR, SR是Service Release,表示项目正式发布的稳定版,其实相当于GA (Generally
    Available) 版。所以,我们会看到SR1、SR2
  • SNAPSHOT,这个表示快照版

在这里插入图片描述

第 02 章 服务注册中心


01. 注册中心介绍_知识星球-Java达摩院

eurekaspring cloud 的注册中心,类似于 dubbo 中的 zookeeper,那么到底什么是注册中心那,我们为什需要注册中心。

在这里插入图片描述

在单体服务中所有的业务都集中在一个项目中,当用户从浏览器中发起请求的时候,直接有前端发起请求给后端,后端调用业务逻辑,给前端请求做出响应,完成一次调用。整个调用过程是一条直线,不需要服务之间中转,所以没有必要引入注册中心。

但是,随着公司项目的规模越来越大,我们会将系统进行拆分,例如一个电商项目,可以拆分为,订单模块、物流模块、支付模块、CMS 模块等等。这样,当用户发起请求的时候,就需要各个模块之间进行协作,这样不可避免的要进行模块之间的调用。此时,我们的系统架构就发生了变化。

在这里插入图片描述

如上所示,模块之间的调用,变得越来越复杂,而且模块之间还存在强耦合。例如 A 调用 B,那么就需要在 A 中写上 B 的地址,这样就意味着 B 的部署的位置要固定,同时,如果以后 B 要进行集群化部署,A 也需要修改。

为了解决如上所述问题,那么注册中心的出现。

 

02. Eureka 介绍

eurekanetflix 公司提供的一款服务注册中心,eureka 基于 REST 来实现服务的注册和发现,曾经,eurekaSpring Cloud 中最重要的核心组件之一。Spring Cloud 中封装了 eureka,在 eureka 的基础上,优化了一些配置,然后提供了可视化的页面,可以方便的查看服务的注册情况以及服务注册中心集群的运行情况。

eureka 由两部分:服务端和客户端,服务端就是注册中心,用来接收其他服务的注册,客户端则是一个 java客户端,用来注册,并可以实现负载均衡的功能。

在这里插入图片描述
从图中 ,我们可以看出,eureka 中,有三个角色:

  • Eureka Server:注册中心
  • Eureka Provider:服务的提供者
  • Eureka Consumer:服务的消费者

03. Eureka 基本搭建

eureka 本身是使用 java 开发的,Spring Cloud 使用 Spring Boot 技术对 eureka 进行了封装,所以,在 Spring Cloud 中使用 eureka 非常方便,只需要引入 spring-cloud-starter-netflix-eureka-server 这个依赖即可,然后就像启动一个普通的 Spring Boot项目一样启动 eureka 即可。

创建一个普通的 Spring Boot 项目,创建的时候,添加Eureka 依赖:

在这里插入图片描述
项目创建成功之后,在项目启动类上添加注解,标记该项目是一个 Eureka Server

package org.javaboy.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

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

}

@EnableEurekaServer 注解表示开启 eureka 的功能。
接下来,在 application.properties 中添加基本配置信息:
在这里插入图片描述
启动项目之后,浏览器舒输入 http://localhost:1111/ 就可以查看 eureka 后台管理页面:

系统状态,例如系统的启动时间等
在这里插入图片描述
集群环境下的副本,也就是当前服务从哪里同步数据
在这里插入图片描述

当前注册上来的服务
在这里插入图片描述
系统运行环境,如内存,cpu
在这里插入图片描述
当前服务的基本信息,例如 ip 地址,状态等
在这里插入图片描述


04. Eureka 集群搭建

使用注册中心之后,所有的服务都要通过服务注册中心来进行信息交换。服务注册中心的稳定性就非常重要,一旦服务注册中心掉线,会影响整个系统的稳定性。所以,在实际开发中,Eureka 一般都是以集群的方式出现的。

Eureka 集群,实际上就是启动了多个Eureka 实例,多个 Eureka 实例之间,互相注册,互相同步数据,共同组成了一个 Eureka 集群。

搭建 Eureka 集群,首先我们要一点准备的工作,修改电脑中的 hosts 文件:
在这里插入图片描述

再上一章的基础上,我们 resource 文件夹下,在添加两个配置文件,分别为:application-a.properties 以及 application-b.properties:
在这里插入图片描述
application-a.properties 内容如下:

# 设置一个端口号
server.port=1111
# 给当前服务取一个名字
spring.application.name=eureka

eureka.instance.hostname=eurekaA

# 默认情况下,Eureka Server 也是一个普通的微服务,所以当他还是一个注册中心的时候,他会有两层身份
# 1. 是注册中心
# 2. 普通服务,即当前这个服务会自己将自己注册到上面来
# 这个设置为 false 表示当前项目不要注册到注册中心上来
eureka.client.register-with-eureka=true

# 表示是否从 Eureka Server 上获取注册信息
eureka.client.fetch-registry=true

# A 服务要注册到 B 上面去,多个使用用逗号分割
eureka.client.service-url.defaultZone=http://eurekaB:1112/eureka

application-b.properties 内容如下:

# 设置一个端口号
server.port=1112
# 给当前服务取一个名字
spring.application.name=eureka
eureka.instance.hostname=eurekaB
# 默认情况下,Eureka Server 也是一个普通的微服务,所以当他还是一个注册中心的时候,他会有两层身份
# 1. 是注册中心
# 2. 普通服务,即当前这个服务会自己将自己注册到上面来
# 这个设置为 false 表示当前项目不要注册到注册中心上来
eureka.client.register-with-eureka=true
# 表示是否从 Eureka Server 上获取注册信息a
eureka.client.fetch-registry=true
# A 服务要注册到 B 上面去
eureka.client.service-url.defaultZone=http://eurekaA:1111/eureka

配置完成之后,对应的项目打成 jar 包:
在这里插入图片描述

在打包的过程中可能出错,按照如下的博客可以解决问题:https://blog.csdn.net/weixin_43567035/article/details/109706572

打包之后,在命令行中启动两个 Eureka 实例,两个启动命令如下:

java -jar eureka-0.0.1-SNAPSHOT.jar --spring.profiles.active=a
java -jar eureka-0.0.1-SNAPSHOT.jar --spring.profiles.active=b

启动成功之后,就可以看见两个服务之间相互注册,共同给组成一个集群。


06. Eureka 工作细节

Eureka Server 本身可以分为两大部分,Eureka ServerEureka Client


Eureka Server
Eureka Server 主要对外提供了三个功能:

  1. 服务注册,所有的服务都要注册到 Eureka Server 上面来。
  2. 提供注册表,注册表就是所有注册上来服务的一个列表,Eureka Client 在调用服务时,需要获取这个注册表,一般来说,这个注册表会缓存下来,如果缓存失效,则直接获取新的注册表。
  3. 同步状态,Eureka Client 通过注册,心跳机制,和 Eureka Server 同步当前客户端状态。

Eureka Client

Eureka Client 主要用用来简化每一个服务和 Eureka Server 之间的交互。Eureka Client 会自动拉去,更新以及缓存Eureka Server 中信息,这样,即使 Eureka Serverf 所有节点都宕机,Eureka Client依然能够获取到想要调用的服务地址(但是地址可能不会准确)。

服务注册:

服务的提供者将自己注册到服务注册中心(Eureka Server),需要注意,所谓的服务的提供者,只是一个业务上的划分,本质上就是一个 Eureka Client 。当 Eureka Client 向 Eureka Server 注册时,他需要提供自身的一些元数据信息,例如 IP 地址、端口、名称、运行状态。

服务续约

Eureka Client 注册到 Eureka Server 上之后,事情没有结束,刚刚开始而已。注册成功之后,默认情况下,Eureka Client 每隔 30 秒就要向 Eureka Server 发送一条心跳消息,来告诉 Eureka Server 我还在运行。如果 Eureka Server 连续 90 秒都没有收到 Eureka Client 的续约消息(连续三次没发送),它会认为 Eureka Client 已经掉线了,会将掉线的 Eureka Client 从当前的服务注册列表中删除。

服务续约,有两个相关的属性(一般不建议修改):

eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
  • eureka.instance.lease-renewal-interval-in-seconds 表示服务续约时间,默认是 30 秒
  • eureka.instance.lease-expiration-duration-in-seconds 表示失效时间,默认是 90 秒

服务下线

当 Eureka Client 下线时,他会主动发送一条消息,告诉 Eureka Server,我下线了。

获取注册表信息

Eureka Client 从 Eureka Server 上获取服务的注册信息,并将其缓存在本地。本地客户端,在需要调用远程服务时,会从该信息中查找远程服务所对应的 IP 地址,端口等信息。Eureka Client 上缓存的服务注册信息会定期更新(30 秒),如果 Eureka Server 返回的注册表信息与本地缓存的注册表信息不同的话,Eureka Client 会自动处理。

这里,也涉及到两个属性,一个是是否允许获取注册表信息:

eureka.client.fetch-registry=true

Eureka Client 上缓存的服务注册信息,定期更新的时间间隔,默认 30 秒:

eureka.client.registry-fetch-interval-seconds=30

07. Eureka 集群原理

我们来看一张官方的 Eureka 集群架构图:
在这里插入图片描述

在这个集群架构中,Eureka Server 之间通过 Replicate 进行数据同步,不同的 Eureka Server 之间不区分主从节点,所有节点都是平等的。节点之间,通过置顶 serverUrl 来互相注册,形成一个集群,进而提高节点的可用性。

Eureka Server 集群中,如果有一个节点宕机,Eureka Client 会自动切换到新的 Eureka Server 上。每一个 Eureka Server 节点,都会互相同步数据。Eureka Server 的链接方式,可以是单线的,就是 A --> B -->C,此时,A 的数据也会和 C 之间互相同步。但是一般不建议这种写法,在我们配置 serviceUrl 时,可以指定多个注册地址,即 A既可以注册到 B 上,也可以注册到 C 上。

Eureka 分区:

  1. region:地理上的不同区域
  2. zone:具体的机房

在这里插入图片描述

第 03 章 服务注册与消费


01. 服务注册

服务注册就是把一个微服务注册到 Eureka Server 上,这样,当其他服务需要调用该服务时,只需要从 Eureka Server 上查询该服务的信息即可。

这里我们创建一个 provider,作为我们服务的提供者,创建项目的时候, 选择 Eureka Client 的依赖,这样,当服务创建成功之后,我们简单配置一下,就可以被注册到 Eureka Server 上了:
在这里插入图片描述

项目创建成功之后,我们只需要在 application.properties 中配置一下项目的注册地址即可。注册地址的配置,和 Eureka Server 集群的配置很像。配置如下:

server.port=1113
spring.application.name=provider
eureka.client.service-url.defaultZone=http://localhost:1111/eureka

如上三行配置,分别表示当前服务的端口号,名称以及服务的地址。
接下来,启动 Eureka Server,待服务注册中心启动成功之后,在启动 provider
两者启动成功之后,浏览器输入 http://localhost:1111,就可以查看到 provider 的注册信息。

在这里插入图片描述


02. 服务消费

首先在 provider 中提供一个接口,然后创建一个新的 counsumer 项目,消费这个接口。
provider 中,提供一个 hello 接口如下:

@RestController
public class HelloController {
    Integer port;
    @GetMapping("/hello")
    public String hello(){
        return "hello javaboy";
    }
}

接下来创建 consumer 项目,consumer 项目中,去消费 provider 提供的接口。consumer 要能够获取到 provider 这个接口的地址,它就需要去 Eureka Server 中查询,如果直接在 consumer 中写死 provider 地址,意味着这两个服务之间的耦合度就太高了,我们要降低耦合度。首先我们来看一个写死的调用。

创建一个 consumer 项目,添加 webeureka client 依赖:
在这里插入图片描述

创建完成之后,我们首先需要在 application.properties 中配置注册信息:

spring.application.name=consumer
server.port=1115
eureka.client.service-url.defaultZone=http://localhost:1111/eureka

配置完成之后,假设我们现在想在 consumer 中调用 provier 提供的服务,我们可以直接写死,就是说,这个调用过程中不会涉及到 Eureka Server

  @GetMapping("hello1")
    public String hello1(){
        HttpURLConnection connection=null;
        try {
            URL url=new URL("http://localhost:1113/hello");
            connection= (HttpURLConnection) url.openConnection();
            if (connection.getResponseCode()==200){
                BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                String s = reader.readLine();
                reader.close();
                return s;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "error";
    }

这是一段利用了 HttpUrlConnection 来发起的请求,请求中 provider 的地址写死了,意味着 providerconsumer 高度绑定在一起,这个不符合微服务的思想。

要改造它,我们可以借助 Eureka Client 提供的 DiscoveryClient 工具,利用这个工具,我们可以根据服务名从 Eureka Server 上查询到一个服务的详细信息,改造后的代码如下:

 @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("hello2")
    public String hello2(){
        List<ServiceInstance> list = discoveryClient.getInstances("provider");
        ServiceInstance provider = list.get(0);
        String host = provider.getHost();
        int port = provider.getPort();
        StringBuffer buffer=new StringBuffer();
        buffer.append("http://").append(host+":")
                .append(port).append("/hello");

        HttpURLConnection connection=null;

        try {
            URL url=new URL(buffer.toString());
            connection= (HttpURLConnection) url.openConnection();
            if (connection.getResponseCode()==200){
                BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                String s = reader.readLine();
                reader.close();
                return s;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "error";
    }

注意,DiscoveryClient 查询到的服务列表是一个集合,因为服务在部署的过程中,可能是集群化部署,集合中的每一项就是一个实例。

这里我们可以稍微展示一下集群化的部署:

首先修改 provider 中的 hello 接口:

@RestController
public class HelloController {
    @Value("${server.port}")
    Integer port;
    @GetMapping("/hello")
    public String hello(){
        return "hello javaboy"+port;
    }
}

因为,我们会多启动一个 provider 实例,多个 provider 实例的端口不同,为了区分调用的时候是哪一个 provider 提供的服务,我们要在接口返回值中返回端口。

修改完成之后,对 provider 进行打包,打包成功之后,我们启动两个实例:

java -jar provider-0.0.1-SNAPSHOT.jar --server.port=1113
java -jar provider-0.0.1-SNAPSHOT.jar --server.port=1116

启动完成之后,检查 Eureka Server 上,这两个实例是否注册上来。

注册成功之后,在 consumer 中,再去调用 providerDiscoveryClient 集合中,获取到的就不是一个实例,而是两个,这里我们可以手动的去实现一下负载均衡:

 int count =0;
    @GetMapping("hello3")
    public String hello3(){
        List<ServiceInstance> list = discoveryClient.getInstances("provider");
        ServiceInstance provider = list.get((count++)%list.size());
        String host = provider.getHost();
        int port = provider.getPort();
        StringBuffer buffer=new StringBuffer();
        buffer.append("http://").append(host+":")
                .append(port).append("/hello");

        HttpURLConnection connection=null;

        try {
            URL url=new URL(buffer.toString());
            connection= (HttpURLConnection) url.openConnection();
            if (connection.getResponseCode()==200){
                BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                String s = reader.readLine();
                reader.close();
                return s;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "error";
    }

从集合中获取数据,通过一个小小的举动,就可以实现线性负载均衡。


03. 服务消费升级改造

从两个方面进行改造:

  1. http 调用
  2. 负载均衡

http 调用,我们使用 Spring 提供了 RestTemplate 实现。
首先,在当前服务中,提供一个 RestTemplate 实例:

package org.javaboy.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class ConsumerApplication {

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

	@Bean
	RestTemplate restTemplateOne(){
		return new RestTemplate();
	}
}

然后再调用的时,不在使用 HttpUrlConnection,而是直接使用 RestTemplate

    @Autowired
    @Qualifier("restTemplateOne")
    private RestTemplate restTemplateOne;

    @GetMapping("hello2")
    public String hello2() {
        List<ServiceInstance> list = discoveryClient.getInstances("provider");
        ServiceInstance provider = list.get(0);
        String host = provider.getHost();
        int port = provider.getPort();
        StringBuffer buffer = new StringBuffer();
        buffer.append("http://").append(host + ":")
                .append(port).append("/hello");
        HttpURLConnection connection = null;
        String s = restTemplateOne.getForObject(buffer.toString(), String.class);
        return s;
    }

用了 RestTemplate,一行代码就实现了 http 调用。

接下来,使用 Ribbon 来实现负载均衡。

首先,我们给 RestTemplate 实例添加一个 @LoadBanlanced 注解,开启负载均衡:

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

此时的 RestTemplate 就具备了负载均衡功能。
此时的代码调用如下:

  @Autowired
    @Qualifier("restTemplate")
    private RestTemplate restTemplate;

    @GetMapping("hello4")
    public String hello4() {
        String s = restTemplate.getForObject("http://provider/hello", String.class);
        return s;
    }

java 中关于 http 请求的工具实际上非常多,自带的 httpUrlConnection,古老的 HttpClient,后起之秀 OkHttp 等,除了这些之外,还有一个非常好用的工具 –RestTemplate,这是 Spring 中就开始提供的 Http 请求工具。


04. RestTemplate

get 请求

这里我们先在 provider 中定义一个 hello2 接口:

  @GetMapping("hello2")
    public String hello2(String name){
        return "hello"+name;
    }

接下来我们在 consumer 去访问这个接口,这个接口是一个 GET 请求,所以,访问方式,就是调用 RestTemplate 中的 Get 请求。

可以看到,在 RestTemplate 中,关于 GET 请求,一共有两大类方法如下:
在这里插入图片描述

这两大类方法实际上是重载的,唯一不同的是,就是返回值类型不相同。

getForObject 返回的是一个对象,这个对象就是服务端返回的一个具体的值。getForEntity 返回的是一个 ResponseEntity,这个 ResponseEntity 中除了服务端返回的具体数据外,还保留了 http 响应头的数据:

 @Autowired
    @Qualifier("restTemplate")
    private RestTemplate restTemplate;

    @GetMapping("hello4")
    public void  hello4() {
        String s = restTemplate.getForObject("http://provider/hello2?name={1}", String.class,"javaboy");
        System.out.println("s = " + s);
        ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://provider/hello2?name={1}", String.class,"javaboy");

        String body = responseEntity.getBody();
        System.out.println("body = " + body);
        HttpStatus statusCode = responseEntity.getStatusCode();
        System.out.println("statusCode = " + statusCode);
        int codeValue = responseEntity.getStatusCodeValue();
       System.out.println("codeValue = " + codeValue);
        System.out.println("================== header =============================");
        HttpHeaders headers = responseEntity.getHeaders();
        Set<String> strings = headers.keySet();
        strings.forEach(System.out::println);

    }

这里我们可以看出来,getForObject 直接拿到了服务的返回值,getForEntity 不仅仅拿到服务的返回值,还拿到了 http 的状态码,然后,启动 Eureka Serverprovider以及 consumer,访问 consumer 中的 hello4 接口,既可以看到请求接口。

看清楚两者之间的区别之后,接下来看下两个各自重载的方法,getForObjectgetForEntity 分别有三个重载方法,两者的三个重载方法基本都是一致的。所以,我们主要看其中一种。三个重载方法,其实代表了三种不同的传参方式。

    @GetMapping("hello5")
    public void  hello5() throws UnsupportedEncodingException {
        String s = restTemplate.getForObject("http://provider/hello2?name={1}", String.class,"张三");
        System.out.println("s = " + s);
        Map<String,Object> map=new HashMap<>();
        map.put("name","zhangsan");
        s = restTemplate.getForObject("http://provider/hello2?name={name}", String.class,map);
        System.out.println("s = " + s);
        String url="http://provider/hello2?name="+ URLEncoder.encode("张三","UTF-8");
        URI uri=URI.create(url);
        s = restTemplate.getForObject(uri, String.class);
        System.out.println("s = " + s);
    }

如上就是三种不同的传参方式。



post

首先在 provider 中,提供两个 post 接口,同时,因为 post 请求可能需要传递 json,所以,这里我们创建一个普通的 maven 项目作为 commons 模块,然后这个 commons 模块被 providerconsumer 共同引用,这样便可以方便我们使用 json传递了。

commons 模块创建成功之后,首先在 commons 模块中添加 user 对象。然后该模块分别被 providerconsumer 引用。

然后,我们在 provider 中,提供两个 post 接口:

   @PostMapping("/user1")
    public User addUser(User user) {
        return user;
    }


    @PostMapping("/user2")
    public User addUser2(@RequestBody User user) {
        return user;
    }

这里定义了两个添加 user 的方法,两个方法表示了两种不同的传参方式。第一中方式是 key-value 形式来传参,第二种方法是以 json 形式来传参。

定义完成之后,接下来,我们在 consumer 中调用这两个 post 接口。

在这里插入图片描述

这里我们可以看到,post 请求和 get 非常像,只是多出来了三个方法,就是 postForLocation,另外,两个 postForObjectpostForEntity 和前面的 get 使用方式基本一致。所以我们主要来看 postForObject

    @GetMapping("hello6")
    public void hello6() {
        MultiValueMap<String, Object> map = new LinkedMultiValueMap();
        map.add("username", "李四");
        map.add("password", "123");
        map.add("id", 1);
        User user = restTemplate.postForObject("http://provider/user1", map, User.class);
        System.out.println("user = " + user);

        user.setId(98);
        user.setUsername("王五");
        User user2 = restTemplate.postForObject("http://provider/user2", user, User.class);
        System.out.println("user2 = " + user2);
    }

post 请求到底是 key-value 形式的还是 json 形式的,主要看第二个参数,如果第二个参数是 MultiValueMap,则参数是以 key-valeu 形式来传递的。如果是一个普通的对象,则参数是以 json 形式来传递的。

最后再看一下 postForLocation。有时候,当我们执行完一个 post 请求之后,立马需要进行重定向,一个非常常见的场景就是注册,注册是一个 post 请求,注册完成之后,立马重定向到登录页面上去,对于这种场景,我们就可以使用 postForLocation

首先我们在 provider 上提供了一个用户注册接口:

package org.javaboy.provider.controller;

import org.javaboy.commons.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

/**
 * @author: yueLQ
 * @date: 2021-06-02 20:24
 */
@Controller
public class RegisterController {

    @PostMapping("/reg")
    public String register(User user){
     return "redirect:http://provider/loginPage?username="+user.getUsername();
    }

    @GetMapping("loginPage")
    @ResponseBody
    public String loginPage(String username){
       return "loginPage:"+username;
    }
}

注意,这里的 post 接口,响应一定是 302,否则 postForLocation 无效。

** 注意:重定向的地址,一定要写成绝对路径,不要写相对路径,否则 consumer 中调用是会出现问题。**

   @GetMapping("hello7")
    public void hello7() throws UnsupportedEncodingException {
        MultiValueMap<String, Object> map = new LinkedMultiValueMap();
        String encode = URLEncoder.encode("李四", "UTF-8");
        map.add("username", encode);
        map.add("password", "123");
        map.add("id", 1);
        URI uri = restTemplate.postForLocation("http://provider/reg", map,User.class);
        System.out.println("uri = " + uri);
        String forObject = restTemplate.getForObject(uri, String.class);
        System.out.println("forObject = " + forObject);
    }

这就是 postForLocation,调用该方法返回一个 uri,这个 uri 就是重定向的地址(里面也包含了重定向的参数),拿到 uri 之后,就可以直接发送新的请求了。


put

put 方法比较简单,重载的方法也比较少。
我们首先在 provider 中提供一个 put 接口:

 @PutMapping("user1")
    public void updateUser(User user){
        System.out.println("user = " + user);
    }

    @PutMapping("user2")
    public void updateUser2(@RequestBody User user){
        System.out.println("user = " + user);
    }

注意 put 接口请求和 post 很像,也接受两种类型的参数,key-value 形式以及 json 形式。

consumer 中,我们调用该方法:

  @GetMapping("hello8")
    public void hello8(){
        MultiValueMap<String, Object> map = new LinkedMultiValueMap();

        map.add("username", "zhangsan");
        map.add("password", "123");
        map.add("id", 1);
        restTemplate.put("http://provider/user1",map);

        User user = new User();
        user.setId(10);
        user.setUsername("lisi");
        user.setPassword("123");
        restTemplate.put("http://provider/user2",user);
    }

写法基本上和 post 请求相似,也是两种方式,可以传递不同的参数。


delete

delete 也比较容易,我们有两种方式传递参数,key-value 形式或者pathVariable(参数放在路径中),首先我们在 provider 中定义两个 delete 方法:

  @DeleteMapping("user1")
    public void deleteUser(Integer id){
        System.out.println("id = " + id);
    }

    @DeleteMapping("user2/{id}")
    public void deleteUser2(@PathVariable Integer id){
        System.out.println("id = " + id);
    }

然后再 consumer 中调用这两个接口:

  @GetMapping("hello9")
    public void hello9(){
        restTemplate.delete("http://provider/user1?id={1}",99);
        restTemplate.delete("http://provider/user2/{1}",100);
    }

delete 中所传递的参数也支持 map,这一块实际上是和 get 请求是一样的。


05. 客户端负载均衡

客户端负载均衡就是相对服务端负载均衡而言的。

服务端负载均衡,就是传统的 nginx 的方式,用 nginx 做负载均衡,我们称之为服务端的负载均衡。
在这里插入图片描述

这种负载均衡,我们称之为服务端的负载均衡,它的一个特点是,就是调用的客户端并不知道具体哪一个 Server 提供服务,他也不关心,反正请求发送给 nginxnginx 再将请求发送给 tomcat,客户端只需记着 nginx 的地址即可。

客户端负载均衡的另外一种场景:
在这里插入图片描述

客户端的负载均衡,就是调用的客户端本身是知道 server 的详细信息的。当需要调用 server 上的接口时候,客户端会从自身维护的 server列表中,根据提前配置好的负载均衡策略,自己挑选一个 server 来调用,此时,客户端知道他所调用的是哪一个 server

RestTemplate 中,要想使用负载均衡,只需要给 RestTemplate实例上添加一个 @LoadBalanced 注解即可,此时,RestTemplate 就会自动具备负载均衡的功能,这个负载均衡就是客户端的负载均衡。


09.负载均衡原理

spring cloud 中,实现负载均衡非常容易,只需要添加 @LoadBalanced 注解即可。只要添加了该注解,一个原本普普通通做 Rest 请求工具 RestTemplate 就会自动具备负载均衡功能,这个是如何实现的那?

整体上来说,这个功能实现就三个核心点:

  1. Eureka Client 本地缓存的服务信息中,选择一个可以调用的服务
  2. 根据 1 中所选的服务,重构请求 URL 地址。
  3. 1、2 步骤功能嵌入到 RestTemplate 中。

在这里插入图片描述

第 05 章 Hystrix


01.Hystrix 简单介绍

hystrix 叫做断路器或者熔断器,微服务系统中,整个系统的出错的概率非常高,因为在微服务的系统中,涉及到的模块太多了,每个模块出错,都可能导致整个服务出错,当所有模块都稳定运行,整个服务才算是稳定运行。

我们希望当整个系统中,某个模块无法正常工作的时候,能够通过我们提前的配置一些东西,来使整个系统正常运行,即单个模块出问题,不影响整个系统。


02.Hystrix 简单应用

首先创建一个新的 Spring Boot 模块,然后添加依赖:

在这里插入图片描述
项目创建成功之后,添加如下配置,将 hystrix 注册到 Eureka

spring.application.name=hystrix
server.port=3000
eureka.client.service-url.defaultZone=http:localhost:1111/eureka

然后再启动类上添加如下注解,开启断路器,同时提供 RestTemplate 实例:

package org.javaboy.hystrix;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

//@SpringBootApplication
//@EnableCircuitBreaker
// 启动类上的注解也可以使用 @SpringCloudApplication 代替
@SpringCloudApplication
public class HystrixApplication {

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

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

这样 hystrix 的配置就完成了。

接下来提供 hystrix 接口

server 层接口:

package org.javaboy.hystrix.service;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.security.PublicKey;

/**
 * @author: yueLQ
 * @date: 2021-06-03 21:00
 */
@Service
public class HelloService {

    @Autowired
    RestTemplate restTemplate;

    /**
     * 在这个方法中,我们将发起一个远程调用,去调用 provider 中提供的 hello 接口
     * 但是有可能调用失败
     *
     * 我们在这个方法中添加 @HystrixCommand 注解,配置 fallbackMethod  属性,这个属性表示调用该方法调用失败的时候临时替代方法
     * @return
     */
    @HystrixCommand(fallbackMethod = "error")
    public String hello(){
        return restTemplate.getForObject("http://provider/hello",String.class);
    }

    /**
     * 注意,这个方法的名字,要和 fallbackMethod 中的方法的名字一致
     * 方法返回值也要和对应的方法一致
     * @return
     */
    public String error(){
     return "error";
    }
}

controller层接口:

package org.javaboy.hystrix.controller;

import org.javaboy.hystrix.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: yueLQ
 * @date: 2021-06-03 21:02
 */
@RestController
public class HelloController {
    @Autowired
    HelloService helloService;

    @GetMapping("hello")
    public String hello(){
     return helloService.hello();
    }
}


03. Hystrix 请求命令

请求命令就是以继承的方式代替前面注解的方式。

我们来自定义一个 HelloCommand

package org.javaboy.hystrix.command;

import com.netflix.hystrix.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * @author: yueLQ
 * @date: 2021-06-04 16:27
 */
public class HelloCommand extends HystrixCommand<String> {

    private RestTemplate restTemplate;


    public HelloCommand(Setter setter, RestTemplate restTemplate) {
        super(setter);
        this.restTemplate = restTemplate;
    }

    /**
     * Implement this method with code to be executed when {@link #execute()} or {@link #queue()} are invoked.
     *
     * @return R response type
     * @throws Exception if command execution fails
     */
    @Override
    protected String run() throws Exception {
        return restTemplate.getForObject("http://provider/hello",String.class);
    }
}

调用方法:

    @GetMapping("hello2")
    public void hello2(){
        HelloCommand command = new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("javaboy")), restTemplate);
        String execute = command.execute(); // 直接执行

        // 先入队列在执行
        HelloCommand command2 = new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("javaboy")), restTemplate);
        Future<String> queue = command2.queue();
        try {
            queue.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

注意:

  1. 一个实例只能执行一次
  2. 可以直接执行,也可以先入队列,后执行。

通过注解实现异步调用

首先在 HelloServer 定义如下方法,方法的返回值为 Future<String>

  @HystrixCommand(fallbackMethod = "error")
    public Future<String> hello2(){
        return new AsyncResult<String>() {
            @Override
            public String invoke() {
                return restTemplate.getForObject("http://provider/hello",String.class);
            }
        };
    }

然后调用该方法:

   @GetMapping("hello3")
    public void hello3(){
        Future<String> stringFuture = helloService.hello2();
        try {
            String s = stringFuture.get();
            System.out.println("s = " + s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

在这里插入图片描述

通过继承的方式 hystrix,如何是实现服务的容错或降级,重写继承类中的 getFallback 方法即可:

package org.javaboy.hystrix.command;

import com.netflix.hystrix.HystrixCommand;
import org.springframework.web.client.RestTemplate;

/**
 * @author: yueLQ
 * @date: 2021-06-04 16:27
 */
public class HelloCommand extends HystrixCommand<String> {

    private RestTemplate restTemplate;


    public HelloCommand(Setter setter, RestTemplate restTemplate) {
        super(setter);
        this.restTemplate = restTemplate;
    }

    /**
     * Implement this method with code to be executed when {@link #execute()} or {@link #queue()} are invoked.
     *
     * @return R response type
     * @throws Exception if command execution fails
     */
    @Override
    protected String run() throws Exception {
        return restTemplate.getForObject("http://provider/hello",String.class);
    }


    /**
     * 请求失败的方法的回调
     * @return
     */
    @Override
    protected String getFallback() {
        return "error-extends";
    }
}

04. Hystrix 异常处理

当发起服务调用的时候,如果不是 provider 的原因导致请求的调用失败,而是 consumer 中本身代码出现问题导致的请求失败,即 consumer 中抛出了异常,这个时候,也会自行的进行服务的降级,只不过这个时候降级,我们需要知道哪里出现了问题。

如下示例代码,如果 hello 方法中,执行时抛出了异常,那么一样会进行服务的降级,进入error方法中,在 error 方法中,我们可以获取到异常的详细信息:

 /**
     * 在这个方法中,我们将发起一个远程调用,去调用 provider 中提供的 hello 接口
     * 但是有可能调用失败
     *
     * 我们在这个方法中添加 @HystrixCommand 注解,配置 fallbackMethod  属性,这个属性表示调用该方法调用失败的时候临时替代方法
     * @return
     */
    @HystrixCommand(fallbackMethod = "error")
    public String hello(){
        int i =  1/ 0;

        return restTemplate.getForObject("http://provider/hello",String.class);
    }


    /**
     * 注意,这个方法的名字,要和 fallbackMethod 中的方法的名字一致
     * 方法返回值也要和对应的方法一致
     * @return
     */
    public String error(Throwable e){
        e.printStackTrace();
     return "error";
    }

上面是注解的使用方式,可以通过继承的方式使用:

package org.javaboy.hystrix.command;

import com.netflix.hystrix.HystrixCommand;
import org.springframework.web.client.RestTemplate;

/**
 * @author: yueLQ
 * @date: 2021-06-04 16:27
 */
public class HelloCommand extends HystrixCommand<String> {

    private RestTemplate restTemplate;


    public HelloCommand(Setter setter, RestTemplate restTemplate) {
        super(setter);
        this.restTemplate = restTemplate;
    }

    /**
     * Implement this method with code to be executed when {@link #execute()} or {@link #queue()} are invoked.
     *
     * @return R response type
     * @throws Exception if command execution fails
     */
    @Override
    protected String run() throws Exception {
        int a=1/0;
        return restTemplate.getForObject("http://provider/hello",String.class);
    }


    /**
     * 请求失败的方法的回调
     * @return
     */
    @Override
    protected String getFallback() {
        return "error-extends"+getExecutionException().getMessage();
    }
}

如果使用继承的方式来做 hystrix,在 getFallback 方法中,我们可以通过 getExecutionException 方法来获取异常信息。

另一种可能性。如果抛出了异常,我们希望异常直接抛出,不要服务降级,那么只需要配置忽略某个异常即可。

 @HystrixCommand(fallbackMethod = "error",ignoreExceptions = ArithmeticException.class)
    public String hello(){
        int i =  1/ 0;

        return restTemplate.getForObject("http://provider/hello",String.class);
    }


这个配置表示当 hello 方法,抛出 ArithmeticException 异常的时候,不要进行服务的降级,直接将错误抛出。


05. Hystrix 请求缓存

请求缓存就是 consumer 中调用同一个接口,如果参数相同,则可以使用之前缓存下来的数据。

首先修改 provider 中的 hello2 接口,一会用来检测缓存配置是否生效:

    @GetMapping("hello2")
    public String hello2(String name)
    {
        System.out.println(new Date()+">>>>"+name);
        return "hello" + name;
    }

然后再 hystrix 的请求方法中,添加如下注解:

    @HystrixCommand(fallbackMethod = "error2")
    @CacheResult // 这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 kye 是方法的参数,缓存的 value 就是方法的返回值
    public String hello3(String name){
        return restTemplate.getForObject("http://provider/hello2?name={1}",String.class,name);
    }

    public String error2(String name,Throwable error){
        return "error:javaboy";
    }

这样的配置完成之后,缓存不一定会生效,一般来说,我们使用缓存,都有一个缓存生命周期的概念。这里也一样,我们需要初始化 HystrixRequestContext,初始化完成,缓存开始生效。

 @GetMapping("hello4")
    public void  hello4(){
        HystrixRequestContext hystrixRequestContext = HystrixRequestContext.initializeContext();
        String s = helloService.hello3("javaboy");
        s= helloService.hello3("javaboy");
        hystrixRequestContext.close();
    }

hystrixRequestContext.close(); 之前,缓存是有效的,close 之后,缓存失效。也就是说,访问一次 hello4 接口,provider 只会被调用一次(第二次使用的缓存),如果再次调用 herllo4 接口,之前的数据是失效的。

默认情况下,缓存的 key 就是所调用的方法的参数,如果参数有多个,就是多个参数组合起来作为缓存的 key

例如如下方法:

    @HystrixCommand(fallbackMethod = "error2")
    @CacheResult // 这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 kye 是方法的参数,缓存的 value 就是方法的返回值
    public String hello3(String name,Integer age){
        return restTemplate.getForObject("http://provider/hello2?name={1}",String.class,name);
    }

此时缓存的 key 就是 name+age,但是,如果有多个参数,但是又想以其中的一个作为缓存的 key,那么我们可以通过 CacheKey 注解来解决:

    @HystrixCommand(fallbackMethod = "error2")
    @CacheResult // 这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 kye 是方法的参数,缓存的 value 就是方法的返回值
    public String hello3(@CacheKey String name, Integer age){
        return restTemplate.getForObject("http://provider/hello2?name={1}",String.class,name);
    }

上面这个配置,虽然有两个参数,但是缓存的时候是以 name 为准。也就是说,两次请求中,只要 name 一样,即使 age 不一样,第二次请求也可以使用第一次请求的缓存结果。

另外,还有一个注解叫做 @CacheRemove()。在做数据缓存的时候。如果有一个数据删除的方法,我们一般除了删除了数据库中的数据,还希望能够顺带删除缓存中的数据,这个时候需要 @CacheRemove() 就派上用场了。

@CacheRemove() 在使用时,必须指定 commandKey 属性,commandKey 其实就是缓存方法的名字,指定 commandKey,@CacheRemove 才能找到缓存数据在哪里,进而才能成功的删除掉数据。

例如如下方法中定义了缓存和删除缓存:

    @HystrixCommand(fallbackMethod = "error2")
    @CacheResult // 这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 kye 是方法的参数,缓存的 value 就是方法的返回值
    public String hello3(String name){
        return restTemplate.getForObject("http://provider/hello2?name={1}",String.class,name);
    }

    @HystrixCommand
    @CacheRemove(commandKey = "hello3")
    public String deleteUserByName(String name){
        return null;
    }

再去调用:

@GetMapping("hello4")
    public void  hello4(){
        HystrixRequestContext hystrixRequestContext = HystrixRequestContext.initializeContext();
        // 第一次请求完,数据已经缓存下来了
        String s = helloService.hello3("javaboy");
        // 删除数据,同时缓存中的数据也会被删除
        helloService.deleteUserByName("javaboy");
        // 第二次请求,虽然参数还是 javaboy,但是缓存的数据已经没有了,所以这一次,还是会收到请求
        s= helloService.hello3("javaboy");
        hystrixRequestContext.close();
    }

如果是继承方式使用 Hystrix,只需要重写 getCacheKey 方法即可:

package org.javaboy.hystrix.command;

import com.netflix.hystrix.HystrixCommand;
import org.springframework.web.client.RestTemplate;

/**
 * @author: yueLQ
 * @date: 2021-06-04 16:27
 */
public class HelloCommand extends HystrixCommand<String> {

    private RestTemplate restTemplate;

    private String  name;

    public HelloCommand(String name,Setter setter, RestTemplate restTemplate) {
        super(setter);
        this.restTemplate = restTemplate;
        this.name=name;
    }

    /**
     * Implement this method with code to be executed when {@link #execute()} or {@link #queue()} are invoked.
     *
     * @return R response type
     * @throws Exception if command execution fails
     */
    @Override
    protected String run() throws Exception {
//        int a=1/0;
        return restTemplate.getForObject("http://provider/hello2?name={1}",String.class,name);
    }


    @Override
    protected String getCacheKey() {
        return name;
    }
    
    
    /**
     * 请求失败的方法的回调
     * @return
     */
    @Override
    protected String getFallback() {
        return "error-extends"+getExecutionException().getMessage();
    }
}

调用的时候一定要记得初始化 HystrixRequestContext

    @GetMapping("hello2")
    public void hello2(){
        HystrixRequestContext hystrixRequestContext = HystrixRequestContext.initializeContext();
        HelloCommand command = new HelloCommand("javaboy",HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("javaboy")), restTemplate);
        String execute = command.execute(); // 直接执行
        System.out.println("execute = " + execute);
        // 先入队列在执行
        HelloCommand command2 = new HelloCommand("javaboy",HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("javaboy")), restTemplate);
        Future<String> queue = command2.queue();
        try {
            String s = queue.get();
            System.out.println("s = " + s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        hystrixRequestContext.close();
    }


06. Hystrix 请求合并

如果 consumer 中,频繁调用 provider 中的同一个接口,在调用请求的时候,只是参数不一样,那么这样的情况下,我们就可以将多个请求合并成为一个,这样可以有效提高请发送的效率。

首先我们在 provider 中提供一个请求合并的接口:

package org.javaboy.provider.controller;

import org.javaboy.commons.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
 * @author: yueLQ
 * @date: 2021-06-09 20:35
 */
@RestController
public class UserController {

//    public User getUserById(Integer id){
//        User user = new User();
//        user.setId(1);
//        user.setUsername("王保单");
//        user.setPassword("10086");
//        return user;
//    }

    /**
     *  假设 consumer 传过来的多个 id 格式是 1,2,3,4.。。
     * @param ids
     * @return
     */

    @GetMapping("/user/{ids}")
    public List<User> getUserByIds(@PathVariable String ids){
        System.out.println("ids = " + ids);

        String[] split = ids.split(",");
        List<User> list = new ArrayList<>();
        for (String s : split) {
            User user = new User();
            user.setId(Integer.parseInt(s));
            user.setUsername("javaboy:"+s);
            user.setPassword(">>>"+s);
            list.add(user);
        }
        return list;
    }
}

这个接口既可以处理合并后的请求,也可以处理单个请求(单个请求的话,list 集合中就只有一项数据即可。)

然后再 hystrix 中,定义 UserService

package org.javaboy.hystrix.service;

import org.apache.commons.lang.StringUtils;
import org.javaboy.commons.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.List;

/**
 * @author: yueLQ
 * @date: 2021-06-09 20:50
 */
@Service
public class UserService {

    @Autowired
    RestTemplate restTemplate;

    public List<User> getUsersByIds(List<Integer> ids){
        User[] users = restTemplate.getForObject("http://provider/user/{1}", User[].class, StringUtils.join(ids,","));
        return Arrays.asList(users);
    }
}

接下来定义 UserBatchCommand,相当于我们之前的 HelloCommand

package org.javaboy.hystrix.command;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import org.javaboy.commons.User;
import org.javaboy.hystrix.service.UserService;

import java.util.List;

/**
 * @author: yueLQ
 * @date: 2021-06-09 21:00
 */
public class UserBatchCommand extends HystrixCommand<List<User>> {

    private List<Integer> ids;

    private UserService userService;

    /**
     * Construct a {@link HystrixCommand} with defined {@link Setter} that allows injecting property and strategy overrides and other optional arguments.
     * <p>
     * NOTE: The {@link HystrixCommandKey} is used to associate a {@link HystrixCommand} with {@link HystrixCircuitBreaker}, {@link HystrixCommandMetrics} and other objects.
     * <p>
     * Do not create multiple {@link HystrixCommand} implementations with the same {@link HystrixCommandKey} but different injected default properties as the first instantiated will win.
     * <p>
     * Properties passed in via {@link Setter#andCommandPropertiesDefaults} or {@link Setter#andThreadPoolPropertiesDefaults} are cached for the given {@link HystrixCommandKey} for the life of the JVM
     * or until {@link Hystrix#reset()} is called. Dynamic properties allow runtime changes. Read more on the <a href="https://github.com/Netflix/Hystrix/wiki/Configuration">Hystrix Wiki</a>.
     *
     * @param setter Fluent interface for constructor arguments
     */
    public UserBatchCommand(List<Integer> ids, UserService userService) {
        super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("batchCommand")).andCommandKey(HystrixCommandKey.Factory.asKey("batchKey")));
        this.ids = ids;
        this.userService = userService;
    }

    /**
     * Implement this method with code to be executed when {@link #execute()} or {@link #queue()} are invoked.
     *
     * @return R response type
     * @throws Exception if command execution fails
     */
    @Override
    protected List<User> run() throws Exception {
        return userService.getUsersByIds(ids);
    }
}

最后定义最关键的请求合并方法:

package org.javaboy.hystrix.command;

import com.netflix.hystrix.HystrixCollapser;
import com.netflix.hystrix.HystrixCollapserKey;
import com.netflix.hystrix.HystrixCollapserProperties;
import com.netflix.hystrix.HystrixCommand;
import org.javaboy.commons.User;
import org.javaboy.hystrix.service.UserService;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @author: yueLQ
 * @date: 2021-06-09 21:09
 * <p>
 * HystrixCollapser 中:
 * 第一个泛型:批处理返回来的泛型
 * 第二个泛型:数据响应的泛型
 * 第三个泛型:参数的类型
 */

public class UserCollapseCommand extends HystrixCollapser<List<User>, User, Integer> {

    private UserService userService;

    private Integer id;

    public UserCollapseCommand(UserService userService, Integer id) {
        super(HystrixCollapser.Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("UserCollapseCommand")).andCollapserPropertiesDefaults(HystrixCollapserProperties.Setter().withTimerDelayInMilliseconds(200)));
        this.userService = userService;
        this.id = id;
    }

    /**
     * 请求参数
     */
    @Override
    public Integer getRequestArgument() {
        return id;
    }

    /**
     * 请求合并的方法
     */
    @Override
    protected HystrixCommand<List<User>> createCommand(Collection<CollapsedRequest<User, Integer>> collapsedRequests) {
        List<Integer> ids = new ArrayList<>(collapsedRequests.size());

        for (CollapsedRequest<User, Integer> collapsedRequest : collapsedRequests) {
            ids.add(collapsedRequest.getArgument());
        }
        return new UserBatchCommand(ids, userService);
    }

    /**
     * 请求结果分发
     *
     * @param batchResponse
     * @param collapsedRequests
     */
    @Override
    protected void mapResponseToRequests(List<User> batchResponse, Collection<CollapsedRequest<User, Integer>> collapsedRequests) {

        int count = 0;
        for (CollapsedRequest<User, Integer> collapsedRequest : collapsedRequests) {
            collapsedRequest.setResponse(batchResponse.get(count++));
        }
    }
}

最后测试调用:

package org.javaboy.hystrix.controller;

import com.netflix.hystrix.Hystrix;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixRequestCache;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import org.checkerframework.checker.units.qual.A;
import org.javaboy.commons.User;
import org.javaboy.hystrix.command.HelloCommand;
import org.javaboy.hystrix.command.UserCollapseCommand;
import org.javaboy.hystrix.service.HelloService;
import org.javaboy.hystrix.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * @author: yueLQ
 * @date: 2021-06-03 21:02
 */
@RestController
public class HelloController {
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    HelloService helloService;

    @GetMapping("hello")
    public String hello(){
     return helloService.hello();
    }

    @GetMapping("hello2")
    public void hello2(){
        HystrixRequestContext hystrixRequestContext = HystrixRequestContext.initializeContext();
        HelloCommand command = new HelloCommand("javaboy",HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("javaboy")), restTemplate);
        String execute = command.execute(); // 直接执行
        System.out.println("execute = " + execute);
        // 先入队列在执行
        HelloCommand command2 = new HelloCommand("javaboy",HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("javaboy")), restTemplate);
        Future<String> queue = command2.queue();
        try {
            String s = queue.get();
            System.out.println("s = " + s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        hystrixRequestContext.close();
    }


    @GetMapping("hello3")
    public void hello3(){
        Future<String> stringFuture = helloService.hello2();
        try {
            String s = stringFuture.get();
            System.out.println("s = " + s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }


    @GetMapping("hello4")
    public void  hello4(){
        HystrixRequestContext hystrixRequestContext = HystrixRequestContext.initializeContext();
        // 第一次请求完,数据已经缓存下来了
        String s = helloService.hello3("javaboy");
        // 删除数据,同时缓存中的数据也会被删除
        helloService.deleteUserByName("javaboy");
        // 第二次请求,虽然参数还是 javaboy,但是缓存的数据已经没有了,所以这一次,还是会收到请求
        s= helloService.hello3("javaboy");
        hystrixRequestContext.close();
    }


    @Autowired
    private UserService userService;
    @GetMapping("hello5")
    public void hello5() throws ExecutionException, InterruptedException {
        HystrixRequestContext hystrixRequestContext = HystrixRequestContext.initializeContext();

        UserCollapseCommand cmd1 = new UserCollapseCommand(userService, 99);
        UserCollapseCommand cmd2 = new UserCollapseCommand(userService, 100);
        UserCollapseCommand cmd3 = new UserCollapseCommand(userService, 101);


        Future<User> queue1 = cmd1.queue();
        Future<User> queue2 = cmd2.queue();
        Future<User> queue3 = cmd3.queue();


        User user1 = queue1.get();
        System.out.println("user1 = " + user1);
        User user2 = queue2.get();
        System.out.println("user2 = " + user2);
        User user3 = queue3.get();
        System.out.println("user3 = " + user3);

        TimeUnit.SECONDS.sleep(2);

        UserCollapseCommand cmd4 = new UserCollapseCommand(userService, 102);
        Future<User> queue4 = cmd4.queue();
        User user4 = queue4.get();
        System.out.println("user4 = " + user4);

        hystrixRequestContext.close();
    }
}

在这里插入图片描述
在这里插入图片描述

 


 
通过注解来实现请求合并:

package org.javaboy.hystrix.service;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.apache.commons.lang.StringUtils;
import org.javaboy.commons.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;

/**
 * @author: yueLQ
 * @date: 2021-06-09 20:50
 */
@Service
public class UserService {

    @Autowired
    RestTemplate restTemplate;

    @HystrixCollapser(batchMethod = "getUsersByIds",collapserProperties = {@HystrixProperty(name ="timerDelayInMilliseconds",value = "200")})
    public Future<User> getUserById(Integer id){
        return null;
    }


    @HystrixCommand
    public List<User> getUsersByIds(List<Integer> ids){
        User[] users = restTemplate.getForObject("http://provider/user/{1}", User[].class, StringUtils.join(ids,","));
        return Arrays.asList(users);
    }
}

这里的核心是 @HystrixCollapser 注解。这个注解中,指定了批处理的方法即可。

测试代码如下:

  @GetMapping("hello6")
    public void hello6() throws ExecutionException, InterruptedException {
        HystrixRequestContext hystrixRequestContext = HystrixRequestContext.initializeContext();
        Future<User> queue1 = userService.getUserById(99);
        Future<User> queue2 = userService.getUserById(100);
        Future<User> queue3 = userService.getUserById(101);

        User user1 = queue1.get();
        System.out.println("user1 = " + user1);
        User user2 = queue2.get();
        System.out.println("user2 = " + user2);
        User user3 = queue3.get();
        System.out.println("user3 = " + user3);

        TimeUnit.SECONDS.sleep(2);
        Future<User> queue4 =userService.getUserById(102);
        User user4 = queue4.get();
        System.out.println("user4 = " + user4);
        hystrixRequestContext.close();
    }

在这里插入图片描述

第 04 章 Consul

spring cloud 中,大部分组件都有备选方案,例如注册中心,除了常见 eureka 之外,像 zookeeper 我们也可以直接使用在 Spring Cloud 中,还有另外还有一个比较重要的方案,就是 consul

consulHashiCorp 公司退出来的开源产品。主要提供:服务发现,服务隔离,服务配置等功能。

相比于 eurekazookeeper·,consul 配置更加一站式,因为它内置了很多微服务常见的需求:服务发现和注册,分布式一致性协议发现,健康检查,键值对存储,多数据源中心等。我们不在需要借助,第三方组件来实现这些功能了。


4.1 安装

不同于 Eurekaconsul 使用 go 语言开发,所以,使用 consul,我们需要安装软件。

linux 中,首先执行如下命令下载 consul:

 wget https://releases.hashicorp.com/consul/1.6.2/consul_1.6.2_linux_amd64.zip

然后解压下载文件:

unzip consul_1.6.2_linux_amd64.zip

解压完成之后,我们在当前目录下就可以看到 consul 文件,然后执行如下命令,启动 consul

./consul agent -dev -ui -node=consul-dev -client=172.17.0.2
  • ui: 显示 ui 界面
  • node:节点的名称
  • client:给 consul 一个地址,这个地址是内网的地址。

在这里插入图片描述
启动成功之后,在物理机中,我们可以直接访问 consul 后台管理页面,注意这个访问一定要保定 8500 端口是可用的,或者直接关闭防火墙。

在这里插入图片描述


4.2 单节点的注册

简单的来看一个消费案例。

首先我们来创建一个服务提供者。就是一个普通的 spring boot 项目,添加如下依赖:

在这里插入图片描述

项目添加成功之后添加如下配置:

spring.application.name=consul-provider
server.port=2000
# consul 配置
# 主机地址
spring.cloud.consul.host=81.68.158.166
# consul 服务器端口
spring.cloud.consul.port=8500
# 注册到 consul 中的名字
spring.cloud.consul.discovery.service-name=consul-provider

# 打开默认心跳
spring.cloud.consul.discovery.heartbeat.enabled=true

在项目的启动类上开启服务发现的功能:

package org.javaboy.consulprovider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class ConsulProviderApplication {

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

最后添加一个测试接口:

package org.javaboy.consulprovider.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: yueLQ
 * @date: 2021-06-10 21:43
 */
@RestController
public class HelloController {

    @GetMapping("hello")
    public String hello(){
        return "consul-provider";
    }
}

接下来就是启动项目了,项目启动成功之后,访问 consul 后台管理页面,如果看到如下信息,则表示已经启动注册成功了。

在这里插入图片描述

启动的时候可能会出现小红叉,如下博客有解决方案:https://blog.csdn.net/ourstronger/article/details/108641931


4.3 集群注册

为了区分集群中哪一个 provider 提供服务,我们可以修改一下 consul 中的接口:

@RestController
public class HelloController {

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

    @GetMapping("hello")
    public String hello(){
        return "consul-provider>>>"+port;
    }
}

修改完成之后,对项目进行打包。打包成功之后,命令行执行如下两行命令,启动两个 provider 实例:

java -jar consul-provider-0.0.1-SNAPSHOT.jar
java -jar consul-provider-0.0.1-SNAPSHOT.jar --server.port=2001

启动成功之后,在去 consul 后台管理页面,就可看到有两个实例了:
在这里插入图片描述


4.5 Consul 消费

首先创建一个消费者实例,创建方式和 provider 一致。

创建成功之后,添加如下配置:

spring.application.name=consul-consumer
server.port=2002
spring.cloud.consul.host=81.68.158.166
# consul 服务器端口
spring.cloud.consul.port=8500
# 注册到 consul 中的名字
spring.cloud.consul.discovery.service-name=consul-consumer
# 打开默认心跳
spring.cloud.consul.discovery.heartbeat.enabled=true

开启服务发现,并添加 RestTemplate

package org.javaboy.consulconsumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
public class ConsulConsumerApplication {

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

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

最后提供一个服务调用的方法:

package org.javaboy.consulconsumer.controller;

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

/**
 * @author: yueLQ
 * @date: 2021-06-10 22:52
 */
@RestController
public class HelloController {

    @Autowired
    LoadBalancerClient client;

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/hello")
    public void hello(){
        ServiceInstance choose = client.choose("consul-provider");
        System.out.println("服务地址:"+choose.getUri());
        System.out.println("服务名称:"+choose.getServiceId());
        String forObject = restTemplate.getForObject(choose.getUri()+"/hello", String.class);
        System.out.println("forObject = " + forObject);
    }
}

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

这里我们通过 loadBalanceClient 实例,可以获取调用 ServiceInstance。获取到调用地址之后,在用 RestTemplate 去调用。

然后,启动项目,浏览器输入 http://localhost:2002/hello,查看请求结果,这个请求自带负载均衡功能。

在这里插入图片描述

第 06 章 OpenFeign

前面的无论是基本调用,还是 hystrix,我们实际上都是通过手动调用 RestTemplate 来实现远程调用的。使用 RestTemplate 存在一个问题:繁琐,每一个请求,参数不同,请求地址不同,返回的数据类型也不同,其他的都一样,所以我们希望能够对请求进行简化。

我们希望对请求进行简化,简化的方案就是 OpenFeign

一开始这个组件不叫这个名字,开始的名字叫做 FeignNetflix Feign,但是 Netflix 中的组件现在已经停止了开源工作。 OpenFeignSpring Cloud 团队在 Netflix Feign 的基础上开发出来的声明式服务调用的组件。关于 Open Feign 组件的 Issuehttps://github.com/OpenFeign/feign/issues/373

6.1 使用

继续使用之前 provider

消费者,新建一个 Spring Boot 模块,创建的时候了,选择 Open Feign 依赖,如下:

在这里插入图片描述

创建成功之后,在 application.properties 中进行配置,使项目注册到 eureka 上:

spring.application.name=openFeign
server.port=4000
# 注册到 eureka 上面去
eureka.client.service-url.defaultZone=http://localhost:1111/eureka

接下来在启动类上添加注解,开启 Feign 的支持:

package org.javaboy.openfeign;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OpenfeignApplication {

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

}

接下来,定义 HelloService 接口,去使用 OpenFeign

package org.javaboy.openfeign.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @author: yueLQ
 * @date: 2021-06-11 19:16
 */
@FeignClient("provider")
public interface HelloService {

    /**
     *  这里的方法名称无所谓
     * @return
     */
    @GetMapping("/hello")
    String hello();
}

最后调用 HelloController 中,调用 HelloService 进行测试:

package org.javaboy.openfeign.controller;

import org.javaboy.openfeign.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: yueLQ
 * @date: 2021-06-11 19:22
 */
@RestController
public class HelloController {
    @Autowired
    private HelloService helloService;
    @GetMapping("hello")
    public String hello(){
        return helloService.hello();
    }
}

接下来启动 OpenFeign 项目,进行测试。测试结果如下:
在这里插入图片描述


6.2 参数传递

和普通参数传递的区别:

  1. 参数一定要绑定参数的名称。
  2. 如果是通过 header 来传递参数,一定要记得中文转码。

测试服务接口,继续使用 provider 提供的接口,代码如下:

package org.javaboy.provider.controller;

import com.ctc.wstx.shaded.msv_core.grammar.DataExp;
import org.javaboy.commons.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.yaml.snakeyaml.util.UriEncoder;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Date;

/**
 * @author: yueLQ
 * @date: 2021-06-01 21:12
 */
@RestController
public class HelloController {
    @Value("${server.port}")
    Integer port;

    @GetMapping("/hello")
    public String hello()
    {
        return "hello javaboy" + port;
    }


    @GetMapping("hello2")
    public String hello2(String name)
    {
        System.out.println(new Date()+">>>>"+name);
        return "hello" + name;
    }

    @PostMapping("/user1")
    public User addUser(User user) {
        return user;
    }


    @PostMapping("/user2")
    public User addUser2(@RequestBody User user) {
        return user;
    }


    @PutMapping("user1")
    public void updateUser(User user){
        System.out.println("user = " + user);
    }

    @PutMapping("user2")
    public void updateUser2(@RequestBody User user){
        System.out.println("user = " + user);
    }


    @DeleteMapping("user1")
    public void deleteUser(Integer id){
        System.out.println("id = " + id);
    }

    @DeleteMapping("user2/{id}")
    public void deleteUser2(@PathVariable Integer id){
        System.out.println("id = " + id);
    }

  // 这里是使用  header 传递参数的
    @GetMapping("user3")
    public void getUserByName(@RequestHeader String name) throws UnsupportedEncodingException {
        System.out.println(URLDecoder.decode(name,"UTF-8"));
    }

}

这里我们使用 Open Feign 中添加接口即可。

package org.javaboy.openfeign.service;

import org.javaboy.commons.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 * @author: yueLQ
 * @date: 2021-06-11 19:16
 */
@FeignClient("provider")
public interface HelloService {

    /**
     *  这里的方法名称无所谓
     * @return
     */
    @GetMapping("/hello")
    String hello();

    @GetMapping("hello2")
    String hello2(@RequestParam("name") String name);

    @PostMapping("user2")
    User addUser(@RequestBody User user);

    @PutMapping("user1")
    void updateUser(@RequestParam("username") String name,@RequestParam("password") String password,@RequestParam("id") Integer id);


    @DeleteMapping("user2/{id}")
    void deleteUserById(@PathVariable("id") Integer id);

    @GetMapping("user3")
    void getUserByName(@RequestHeader("name") String name);
}

注意:但凡是以 key-value 形式的参数,一定记得标记参数的名称

HelloController 中调用 HelloService

package org.javaboy.openfeign.controller;

import org.javaboy.commons.User;
import org.javaboy.openfeign.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

/**
 * @author: yueLQ
 * @date: 2021-06-11 19:22
 */
@RestController
public class HelloController {
    @Autowired
    private HelloService helloService;
    @GetMapping("hello")
    public String hello() throws UnsupportedEncodingException {
        String hello2 = helloService.hello2("光头小小强007");
        System.out.println("hello2 = " + hello2);

        User user = new User();
        user.setId(1);
        user.setUsername("我是八戒");
        user.setPassword("6666");
        User user1 = helloService.addUser(user);
        System.out.println("user1 = " + user1);
        helloService.updateUser("悟空","8888",1);
        helloService.deleteUserById(1);
        helloService.getUserByName(URLEncoder.encode("王者荣耀","UTF-8"));
        return helloService.hello();
    }

}

注意:
如果放在 header 中的是中文参数,一定要编码之后传递,不然会出现乱码。


6.3 继承特性

provideropenfeign 中,公共的部分抽取出来,一起使用。

我们创建一个新的 Module,叫做 hello-api,注意该模块会被其他模块所依赖,所以这个模块是一个 maven 项目,但是由于该模块要用到 Spring MVC 的东西,因此创建成功之后,给这个模块添加一个 web 依赖,导入 Spring MVC 需要的一套东西。

项目创建成功之后,首先添加依赖:

        <dependency>
            <groupId>org.javaboy</groupId>
            <artifactId>commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

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

然后再该模块中,定义公共的接口,就是 provideropenFeign 中公共的部分:

package org.javaboy.api.service;

import org.javaboy.commons.User;
import org.springframework.web.bind.annotation.*;

import java.io.UnsupportedEncodingException;

/**
 * @author: yueLQ
 * @date: 2021-06-11 20:57
 */

public interface IUserService {

    @GetMapping("/hello")
    String hello();

    @GetMapping("hello2")
    String hello2(@RequestParam("name") String name);

    @PostMapping("user2")
    User addUser2(@RequestBody User user);

    @DeleteMapping("user2/{id}")
    void deleteUser2(@PathVariable("id") Integer id);

    @GetMapping("user3")
    void getUserByName(@RequestHeader("name") String name) throws UnsupportedEncodingException;
}

定义完成之后,接下来,在 provideropenFeign 中,分别引用该模块:

	<dependency>
		<groupId>org.javaboy</groupId>
		<artifactId>hello-api</artifactId>
		<version>1.0-SNAPSHOT</version>
	</dependency>

添加成功之后,在 provider 中实现该接口:

package org.javaboy.provider.controller;

import com.ctc.wstx.shaded.msv_core.grammar.DataExp;
import org.javaboy.api.service.IUserService;
import org.javaboy.commons.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.yaml.snakeyaml.util.UriEncoder;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Date;

/**
 * @author: yueLQ
 * @date: 2021-06-01 21:12
 */
@RestController
public class HelloController implements IUserService {
    @Value("${server.port}")
    Integer port;
    
    @Override
    public String hello()
    {
        return "hello javaboy" + port;
    }

    @Override
    public String hello2(String name)
    {
        System.out.println(new Date()+">>>>"+name);
        return "hello" + name;
    }

    @Override
    public User addUser2(@RequestBody User user) {
        return user;
    }

    @Override
    public void deleteUser2(@PathVariable Integer id){
        System.out.println("id = " + id);
    }

    @Override
    public void getUserByName(@RequestHeader String name) throws UnsupportedEncodingException {
        System.out.println(URLDecoder.decode(name,"UTF-8"));
    }
}

openFeign ,定义接口继承自公共接口:

package org.javaboy.openfeign.service;

import org.javaboy.api.service.IUserService;
import org.javaboy.commons.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 * @author: yueLQ
 * @date: 2021-06-11 19:16
 */
@FeignClient("provider")
public interface HelloService extends IUserService {
}

接下来,测试代码不变。
关于继承的特性:

  1. 使用继承特性,代码简洁明了不容易出错。服务端和消费端的代码统一,一改具改,不易出错。这是优点也是缺点,这样会提高服务端和消费端的耦合度。
  2. 上面一章所讲到的参数传递,在使用继承之后,依然不变,参数该怎样传递还是怎样传递。

6.4 日志

openFeign 中,我们可以通过配置日志,来查看整个请求的调用过程。日志级别一共分为四种:

  1. none,不开启日志,默认就是这个。
  2. basic,记录请求的方法,url,响应状态码,执行时间。
  3. headers,在 basic 的基础上,加载请求和响应头。
  4. full,在 headers 的基础上,在增加 body 以及请求数据源。

如上的四种配置,可以通过 Bean 来配置:

package org.javaboy.openfeign;

import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;


@SpringBootApplication
@EnableFeignClients
public class OpenfeignApplication {

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

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

最后,在 application.properties 中开启日志的级别:

logging.level.org.javaboy.openfeign.service.HelloService=debug

6.5 数据压缩
# 开启请求数据压缩
feign.compression.request.enabled=true
# 开启响应数据压缩
feign.compression.response.enabled=true
# 压缩的数据类型
feign.compression.request.mime-types=text/xml,application/json
# 压缩的数据下限,2048 表示当前传输要大于 2048 时,才会进行数据压缩
feign.compression.request.min-request-size=2048

6.6 服务降级

hystrix 中的容错,服务降级功能,在 openFeign 中一样可以使用。(注意:spring boot 2.5 版本以上不在使用 hystrix 依赖,openFeign 中也没有,这里要使用低版本的例如:2.2.2.RELEASE

首先定义服务降级的方法:

package org.javaboy.openfeign.fallback;

import org.javaboy.commons.User;
import org.javaboy.openfeign.service.HelloService;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.UnsupportedEncodingException;

/**
 * @author: yueLQ
 * @date: 2021-06-11 22:40
 */
@Component
@RequestMapping("javaboy") // 防止请求地址重复
public class HelloServiceFallBack implements HelloService {
    @Override
    public String hello() {
        return "error";
    }

    @Override
    public String hello2(String name) {
        return "error";
    }

    @Override
    public User addUser2(User user) {
        return null;
    }

    @Override
    public void deleteUser2(Integer id) {

    }

    @Override
    public void getUserByName(String name) throws UnsupportedEncodingException {

    }
}

然后再 HelloService 中配置这个服务降级类:

package org.javaboy.openfeign.service;

import org.javaboy.api.service.IUserService;
import org.javaboy.commons.User;
import org.javaboy.openfeign.fallback.HelloServiceFallBack;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 * @author: yueLQ
 * @date: 2021-06-11 19:16
 */
@FeignClient(value="provider",fallback = HelloServiceFallBack.class)
public interface HelloService extends IUserService {
}

最后在 application.properties 中来开启 hystrix

feign.hystrix.enabled=true

也可以通过自定义 FallBackFactory 来实现请求降级:

package org.javaboy.openfeign.fallback;

import feign.hystrix.FallbackFactory;
import org.javaboy.commons.User;
import org.javaboy.openfeign.service.HelloService;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;

/**
 * @author: yueLQ
 * @date: 2021-06-11 23:49
 */
@Component
public class HelloServiceFallBackFactory implements FallbackFactory<HelloService> {

    @Override
    public HelloService create(Throwable cause) {
        return new HelloService() {
            @Override
            public String hello() {
                return "error---error";
            }

            @Override
            public String hello2(String name) {
                return null;
            }

            @Override
            public User addUser2(User user) {
                return null;
            }

            @Override
            public void deleteUser2(Integer id) {

            }

            @Override
            public void getUserByName(String name) throws UnsupportedEncodingException {

            }
        };
    }
}

HelloService 中进行配置:

package org.javaboy.openfeign.service;

import org.javaboy.api.service.IUserService;
import org.javaboy.commons.User;
import org.javaboy.openfeign.fallback.HelloServiceFallBack;
import org.javaboy.openfeign.fallback.HelloServiceFallBackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 * @author: yueLQ
 * @date: 2021-06-11 19:16
 */
//@FeignClient(value="provider",fallback = HelloServiceFallBack.class)
@FeignClient(value="provider",fallbackFactory = HelloServiceFallBackFactory.class)
public interface HelloService extends IUserService {
}

在这里插入图片描述

第 07 章 Resilience4j

resilience4jspring cloud greenwich 版本推荐的容错解决方案,相比较于 hystrixresilience4j 专门为 java 8 以及函数式编程而设计的。

resilience4j 主要提供了如下功能:

  1. 断路器
  2. 限流
  3. 基于信号量的隔离
  4. 缓存
  5. 限时
  6. 请求重试

7.1 断路器使用演示

首先创建一个普通的 maven 项目进行简单的测试,添加 junit依赖:

      <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

resilience4j 提供了很多功能,不同的功能对应不同的依赖,可以按照需求添加,使用断路器则需要添加断路器的依赖:

      <dependency>
            <groupId>io.github.resilience4j</groupId>
            <artifactId>resilience4j-circuitbreaker</artifactId>
            <version>0.13.2</version>
        </dependency>

一个正常执行的案例:

    @Test
    public void test1() {
        // 首先我们需要获取一个 CircuitBreakerRegistry 实例
        // 可以调用 ofDefaults 方法获取一个 CircuitBreakerRegistry 实例,也可以自定义属性
        CircuitBreakerRegistry registry = CircuitBreakerRegistry.ofDefaults();

        // 自定义的配置
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
                // 故障率阈值百分比,超过这个阈值断路器就会打开
                .failureRateThreshold(50)
                // 断路器保持打开的时间,在达到设置的时间之后,断路器会进入到半开的状态(half open)
                .waitDurationInOpenState(Duration.ofMillis(1000))
                // 当断路器处于 half open 的状态下,环形缓冲区的大小
                .ringBufferSizeInHalfOpenState(2)
                // 当断路器处于关闭的转态的时候,环形缓冲区的大小
                .ringBufferSizeInClosedState(2)
                .build();
        CircuitBreakerRegistry registry1 = CircuitBreakerRegistry.of(config);
        // 获取断路器,参数是传入一个名字随机
        CircuitBreaker breaker = registry1.circuitBreaker("javaboy");
        CircuitBreaker breaker1 = registry1.circuitBreaker("javaboy2", config);

        CheckedFunction0<String> supplier = CircuitBreaker.decorateCheckedSupplier(breaker, () -> "hello resilience4j");
        Try<String> result = Try.of(supplier).map(v -> v + "hello world");
        // 判断是否调用成功
        System.out.println(result.isSuccess());
        System.out.println(result.get());
    }

一个出现异常的断路器:

 @Test
    public void test2() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
                // 故障率阈值百分比,超过这个阈值断路器就会打开
                .failureRateThreshold(50)
                // 断路器保持打开的时间,在达到设置的时间之后,断路器会进入到半开的状态(half open)
                .waitDurationInOpenState(Duration.ofMillis(1000))
                // 当断路器处于关闭的转态的时候,环形缓冲区的大小
                .ringBufferSizeInClosedState(2)
                .build();
        CircuitBreakerRegistry registry1 = CircuitBreakerRegistry.of(config);
        // 获取断路器,参数是传入一个名字随机
        CircuitBreaker breaker = registry1.circuitBreaker("javaboy");
        // 获取断路器的一个状态
        System.out.println(breaker.getState());
        breaker.onError(0,new RuntimeException());
        System.out.println(breaker.getState());
        breaker.onError(0,new RuntimeException());
        System.out.println(breaker.getState());
        CheckedFunction0<String> supplier = CircuitBreaker.decorateCheckedSupplier(breaker, () -> "hello resilience4j");
        Try<String> result = Try.of(supplier).map(v -> v + "hello world");
        // 判断是否调用成功
        System.out.println(result.isSuccess());
        System.out.println(result.get());
    }

注意:
由于 ringBufferSizeInClosedState 的值为 2,表示当前有两条数据时候才会去统计故障率。所以,下面的动手故障测试,至少需要调用两次 onError 断路器才会打开。


7.2 限流

RateLimiter 本身和前面的断路器很像。
首先添加依赖:

   <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-ratelimiter</artifactId>
        <version>0.13.2</version>
    </dependency>

限流测试:

 @Test
    public void test3(){
        RateLimiterConfig limiterConfig = RateLimiterConfig.custom()
                // 阈值刷新的时间
                .limitRefreshPeriod(Duration.ofMillis(1000))
                // 阈值刷新的频次
                .limitForPeriod(2)
                // 限流之后的冷却时间
                .timeoutDuration(Duration.ofMillis(1000))
                .build();

        RateLimiter rateLimiter = RateLimiter.of("javaboy", limiterConfig);
        // 拿到相当于执行的任务
        CheckedRunnable checkedRunnable = RateLimiter.decorateCheckedRunnable(rateLimiter, () -> {
            System.out.println(new Date());
        });
        // 执行
        Try.run(checkedRunnable)
                // 继续尝试执行
                .andThenTry(checkedRunnable)
                .andThenTry(checkedRunnable)
                .andThenTry(checkedRunnable)
                // 如果失败了怎么办,打印异常
                .onFailure(t-> System.out.println(t.getMessage()));
    }

7.3 请求重试

首先第一步添加依赖:

 <dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-retry</artifactId>
    <version>0.13.2</version>
 </dependency>

案例:

 @Test
    public void test4(){
        RetryConfig config = RetryConfig.custom()
                // 最大重试次数
                .maxAttempts(5)
                // 重试的间隔时间
                .waitDuration(Duration.ofMillis(1000))
                // 抛出那些异常来可以重试,重试异常
                .retryExceptions(RuntimeException.class)
                .build();

        Retry retry = Retry.of("javaboy", config);
        // 进行重试
        Retry.decorateRunnable(retry, new Runnable(){
            int count=0;
            // 开启重试功能之后,run 方法执行时,如果抛出异常,会自动触发重试功能
            @Override
            public void run() {
                if (count++<3){
                    throw new RuntimeException();
                }
            }
        }).run();

    }

7.4 微服务中的重试

Retry

首先常见一个 spring boot 项目,创建的时候添加 eureka-client 的依赖,是他能够注册到 eureka 上(这里我们的 spring boot 版本是 :2.2.4.RELEASEspring cloud 版本是 Hoxton.SR1,版本一定要同一否则部分功能体现不出来):
在这里插入图片描述

创建成功之后,添加 resilience4j 的依赖:

<dependency>
			<groupId>io.github.resilience4j</groupId>
			<artifactId>resilience4j-spring-boot2</artifactId>
			<version>1.2.0</version>
			<exclusions>
				<exclusion>
					<groupId>io.github.resilience4j</groupId>
					<artifactId>resilience4j-circuitbreaker</artifactId>
				</exclusion>
				<exclusion>
					<groupId>io.github.resilience4j</groupId>
					<artifactId>resilience4j-ratelimiter</artifactId>
				</exclusion>
				<exclusion>
					<groupId>io.github.resilience4j</groupId>
					<artifactId>resilience4j-bulkhead</artifactId>
				</exclusion>
				<exclusion>
					<groupId>io.github.resilience4j</groupId>
					<artifactId>resilience4j-timelimiter</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

resilience4j-spring-boot2 中包含了 resilience4j 的所有功能,但是没有配置的功能无法使用,需要将其在依赖中剔除。
接下来 application.yml 中配置 retry

resilience4j:
  retry:
    retry-aspect-order: 399    # 表示 retry 的优先级,默认情况下 retry 优于限流和断路器
    backends: # 可以定义使用那种策略
      retryA: # 自定义策略的名字
        maxRetryAttempts: 5  # 重试的次数
        waitDuration: 500 # 重试的等待时间、
        exponentialBackoffMultiplier: 1.1 # 间隔乘数
        retryExceptions:
          - java.lang.RuntimeException

spring:
  application:
    name: resilience4j
server:
  port: 5000
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1111/eureka

创建 RestTemplate 实例:

package org.javaboy.resilience4j2.controller;

import org.javaboy.resilience4j2.serfvice.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: yueLQ
 * @date: 2021-06-12 22:34
 */
@RestController
public class HelloController {
    @Autowired
    private HelloService helloService;

    @GetMapping("hello")
    public String hello(){
        return helloService.hello();
    }
}

创建 HelloService

package org.javaboy.resilience4j2.serfvice;

import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * @author: yueLQ
 * @date: 2021-06-12 22:34
 */
@Service
@Retry(name="retryA") // 表示要使用的重试的策略
public class HelloService {
    @Autowired
    private RestTemplate restTemplate;

    public String hello(){
        return restTemplate.getForObject("http://localhost:1113/hello",String.class);
    }
}

创建 HelloController 测试:

package org.javaboy.resilience4j2.controller;

import org.javaboy.resilience4j2.serfvice.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: yueLQ
 * @date: 2021-06-12 22:34
 */
@RestController
public class HelloController {
    @Autowired
    private HelloService helloService;

    @GetMapping("hello")
    public String hello(){
        return helloService.hello();
    }
}

测试结果,在 providerhello 接口被重试调用了五次:
在这里插入图片描述


circuitBreaker

首先从依赖中删除排除 CircuitBreaker
然后,从 application.yml 中进行配置。

  circuitbreaker: # 添加实例
    instances:
      cbA:
        ringBufferSizeInClosedState: 2 # 状态下的环形缓冲区的大小
        ringBufferSizeInHalfOpenState: 2
        waitInterval: 5000
        recordExceptions:
          - org.springframework.web.client.HttpServerErrorException
    circuit-breaker-aspect-order: 398 # 配置优先级

配置完成之后,用 @CircuitBreakder 注解标记相关方法:

package org.javaboy.resilience4j2.serfvice;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * @author: yueLQ
 * @date: 2021-06-12 22:34
 */
@Service
//@Retry(name="retryA") // 表示要使用的重试的策略

public class HelloService {
    @Autowired
    private RestTemplate restTemplate;

    @CircuitBreaker(name = "cbA", fallbackMethod = "error")
    public String hello() {
        return restTemplate.getForObject("http://localhost:1113/hello", String.class);
    }

    public String error(Throwable e) {
        return "error" + e.getMessage();
    }
}

@CircuitBreaker 注解中的 name 属性指定 circuitbreaker 配置,fallbackMethod 属性用来指定服务降级方法,需要注意的是,服务降级的方法中,要添加异常参数。


RateLimiter

RateLimiter 作为限流工具,主要在服务端使用,用来保护服务端接口。
首先在 provider 中添加 RateLimiter 依赖:

	<dependency>
			<groupId>io.github.resilience4j</groupId>
			<artifactId>resilience4j-spring-boot2</artifactId>
			<version>1.2.0</version>
			<exclusions>
				<exclusion>
					<groupId>io.github.resilience4j</groupId>
					<artifactId>resilience4j-circuitbreaker</artifactId>
				</exclusion>
				<exclusion>
					<groupId>io.github.resilience4j</groupId>
					<artifactId>resilience4j-bulkhead</artifactId>
				</exclusion>
				<exclusion>
					<groupId>io.github.resilience4j</groupId>
					<artifactId>resilience4j-timelimiter</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

接下来,在 porviderapplication.properties 配置文件中,去配置 RateLimiter

# 在一个周期内处理多少请求
resilience4j.ratelimiter.limiters.rlA.limit-for-period=1
# 一秒钟处理一次
resilience4j.ratelimiter.limiters.rlA.limit-refresh-period=1s
# 冷却时间
resilience4j.ratelimiter.limiters.rlA.timeout-duration=1s

为了查看请求效果,在 providerHelloController 中打印每一个请求的时间:

    @Override
    @RateLimiter(name = "rlA")
    public String hello()
    {
        String s =  "hello javaboy" + port;
        System.out.println(new Date());
//        int i = 1/0;
        return s;
    }

这里通过 @RateLimiter 注解来标记该接口限流。

配置完成后,重启 provider

然后,在客户端模拟多个请求,查看限流的效果。

    public String hello() {
        for (int i = 0; i < 5; i++) {
            restTemplate.getForObject("http://localhost:1113/hello", String.class);
        }
        return "success";
    }

在这里插入图片描述

第 08 章 服务监控

微服务由于服务数量众多,所以出故障率很大,这种时候不能太单纯依靠人肉运维。

早期的 spring cloud 中,微服务主要使用Hystix Dashboard,集群数据库监控使用 turbine

Greenwich 版本中,官方建议监控工具使用 Micrometer

Micrometer

  1. 提供了度量指标,例如 timerscounters
  2. 一揽子开箱即用的解决方案,例如缓存,类加载器,垃圾收集器等。

新建一个 Spring Boot 项目,添加 Actuator 依赖。项目创建成功之后,添加如下配置,开启所有的端点:

management.endpoints.web.exposure.include=*

在这里插入图片描述

我们需要一个可视化的工具来展示这些 json 数据。这里主要介绍 prometheus


prometheus

官网:https://prometheus.io/download/

安装

# 下载
wget https://github.com/prometheus/prometheus/releases/download/v2.27.1/prometheus-2.27.1.linux-amd64.tar.gz   
# 解压
tar -zxvf prometheus-2.27.1.linux-amd64.tar.gz

解压完成之后,配置一下数据路径和要监控的服务地址:

cd prometheus-2.27.1.linux-amd64/
vim prometheus.yml 

修改 prometheus.yml 配置文件之后,主要修改两个地方,一个是数据接口,另一个是服务地址:
在这里插入图片描述

接下来,将 prometheus 整合到 Spring Boot 项目中。

首先添加依赖:

		<dependency>
			<groupId>io.micrometer</groupId>
			<artifactId>micrometer-registry-prometheus</artifactId>
		</dependency>

然后在 application.properties 中配置,添加 prometheus 配置:

management.endpoints.web.exposure.include=*
# 开启 prometheus
management.endpoint.prometheus.enabled=true
management.metrics.export.prometheus.enabled=true
management.endpoint.metrics.enabled=true

接下来,启动 prometheus
启动命令如下:

./prometheus --config.file=prometheus.yml

在这里插入图片描述

启动成功之后,浏览器输入 http://81.68.158.166:9090/graph 查看 prometheus 数据信息。


grafana

官网地址:https://grafana.com/grafana/download?platform=linux
在这里插入图片描述

安装好之后启动命令:

 systemctl start grafana-server.service

默认的用户名和密码是 admin,首次登录后会让我们修改密码。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

第 09 章 Zuul

zuulGateway

由于每一个微服务的地址都有可能发生变化,无法直接对外公布这些服务地址,基于安全以及高内聚低耦合等设计,我们有必要将内部系统和外部系统做一个分割。

一个专门用来处理外部请求的组件,就是服务网关。

  • 权限问题统一处理
  • 数据剪裁和聚合
  • 简化客户端的调用
  • 可以针对不同的客户端提供不同的网关支持。

spring cloud 中,网关主要有两种实现方案:

  • zuul
  • spring cloud gateway

9.1 简介

zuulnetflix 公司提供的网关服务。

zuul 的功能:

  • 权限控制,可以做认证或者授权。
  • 监控
  • 动态路由
  • 负载均衡
  • 静态资源处理

zuul 中的基本功能基本上都是基于过滤器来实现的,它的过滤有几种不同的类型:

  • PRE
  • ROUTING
  • POST
  • ERROR

9.2 案例

首先创建项目,添加zuul 注册到 eureka 上:
在这里插入图片描述
创建成功之后,将 zuul 注册到 eureka 上:

spring.application.name=zuul
server.port=2020
eureka.instance.hostname=zuul
eureka.client.service-url.defaultZone=http://localhost:1111/eureka

然后再启动类上开启网关代理:

package org.javaboy.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
// 开启网关代理
@EnableZuulProxy
public class ZuulApplication {

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

}

配置完成之后,重启 zuul 接下来,在浏览器中,通过 zuul 的代理就可以访问到 provider 了。

在这里插入图片描述

在这个访问地址中,provider 就是要访问的服务名称,/hello 则是要访问的服务接口。

这是一个简单的例子,zuul 中的路由规则也可以自己进行配置。

zuul.routes.javaboy-a.path=/javaboy-a/**
zuul.routes.javaboy-a.service-id=provider

上面这个配置,表示 /javaboy-a/**,满足这个匹配规则的请求,将被转发到 provider 实例上。
上面的两行配置,也可以简化为:

# 简化的配置
zuul.routes.provider=/javaboy-a/**

9.3 Zuul 请求过滤

对于来自客户端的请求,可以再 zuul 中进行预处理,例如权限判断等。
定义一个简单的权限过滤器:

package org.javaboy.zuul.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * @author: yueLQ
 * @date: 2021-06-16 21:11
 */
@Component
public class PermissFilter extends ZuulFilter {
    /**
     *  过滤器的类型,权限判断一般是 pre
     * @return
     */
    @Override
    public String filterType() {
        /**
         * pre:路由之前
         * routing:路由之时
         * post: 路由之后
         * error:发送错误调用
         */

        return "pre";
    }

    /**
     *  过滤器的优先级
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 是否过滤
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 这里去做核心的业务逻辑
     * @return 这个方法虽然有返回值,但是这个返回值目前是无所谓的
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        // 获取当前请求
        HttpServletRequest request = ctx.getRequest();
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        // 如果不满足请求停止
        if (!"javaboy".equals(username)||!"123".equals(password)){
            // 如果请求条件不满足的话,直接从这里给出响应
            // 响应停止
            ctx.setSendZuulResponse(false);
            // 响应状态码
            ctx.setResponseStatusCode(401);
            // 加一个响应头
            ctx.addZuulResponseHeader("content-type","text/html;charset=utf-8");
            // 响应体
            ctx.setResponseBody("非法访问");
        }
        return null;
    }
}

重启 zuul,接下来,发送请求必须带上 usernamepassword 参数,否则请求不同,如下:
在这里插入图片描述
在这里插入图片描述


9.4 其他配置

匹配规则
例如有两个服务,一个叫 consumer,另一个叫 consumer-hello,在做路由规则设置的时候,假如出现如下配置:

zuul.routes.consumer=/consumer/**
zuul.routes.consumer-hello=/consumer/hello/**

此时,如果出现如下访问地址 http://localhost:2020/consumer/hello/123,会出现冲突。实际上,这个地址是希望和 consumer-hello 这个服务匹配的,这个时候,只需要把配置文件改为 yml 格式就可以了。


忽略路径
默认情况下,zuul 注册到 eureka 上之后,eureka 上所有的注册服务都会被自动代理。如果不想给某个一个服务做代理,可以忽略该服务,配置如下:

zuul.ignored-services=provider

如上配置,表示忽略 provider 服务,此时就不会自动代理 provider 服务了。
也可以忽略某一类地:

zuul.ignored-patterns=/**/hello/**

这个表示请求路径中如果包含 hello,则不作代理。


前缀
也可以给路由加前缀

zuul.prefix=/javaboy

这样,以后所有的请求地址自动多了前缀,/javaboy

在这里插入图片描述

第 10 章 Spring Cloud Gateway

特点:

  • 限流
  • 路径重写
  • 动态路由
  • 集成 spring cloud discoveryClient
  • 集成hystrix

zuul 对比:

  1. zuulnetflix 公司的开源产品,spring cloud Gatewayspring 家族中的产品,可以和 spring 家族中的其他组件更好的融合。
  2. zuul1 不支持长链接,例如 websocket
  3. spring cloud Gateway 支持限流
  4. spring cloud Gateway 基于 netty 来开发,实现了异步和非阻塞,占用资源更小,性能强于 zuul

10.1 基本用法

spring cloud gateway 支持两种不同的用法:

  • 编码配置
  • yml 配置

编码配置

首先创建 spring boot 项目,添加 spring cloud gateway 模块:
在这里插入图片描述

创建成功之后,这里直接配置一个 RouteLocator 这样一个 bean,就可以实现请求转发。

package org.javaboy.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class GatewayApplication {

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

	@Bean
	RouteLocator routeLocator(RouteLocatorBuilder builder){
		return builder.routes()
				// 开启配置,第一个参数就是配置规则的名字,第二个参数就是访问 get 的时候会跳转到 http://httpbin.org/get 上
                .route("javaboy_route",r->r.path("/get").uri("http://httpbin.org/get"))
               .build();
	}
}

这里需要提供一个 RouteLocator 这个 bean,就可以实现请求转发。配置完成之后,重启项目,访问 http://localhost:8080/get

properties 配置

# 名字
spring.cloud.gateway.routes[0].id=javaboy_route
# 路由到那里去
spring.cloud.gateway.routes[0].uri=http://httpbin.org/get
# 路由条件
spring.cloud.gateway.routes[0].predicates[0]=Path=/get

yml 配置

spring:
  cloud:
    gateway:
      routes:
        - id: javaboy_route
          uri: http://httpbin.org/get
          predicates:
            - Path=/get

10.2 结合微服务

首先给 Gateway 添加依赖,将他注册到 eureka 上。
添加依赖:

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

添加配置:

spring:
  cloud:
    gateway:
      routes:
        - id: javaboy_route
          uri: http://httpbin.org/get
          predicates:
            - Path=/get
      discovery:
        locator:
          enabled: true  # 开启自动代理


  application:
    name: gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1111/eureka
logging:
  level:
    org.springframework.cloud.getway: debug

在这里插入图片描述

接下来我们就可以通过 gateway 访问到其他注册在 eureka 上的服务了,访问方式和 zuul 一样。

在这里插入图片描述


10.3 Predicate

通过时间进行匹配:

spring:
  cloud:
    gateway:
      routes:
        - id: javaboy_route
          uri: http://httpbin.org/get
          predicates:
                - After=2022-06-17T01:01:01+08:00[Asia/Shanghai]

表示,请求时间在 2022-06-17T01:01:01+08:00[Asia/Shanghai] 时间之后,才会被路由。

除了 After 之外,还有两个关键字:

  • before 表示在某个时间点之前进行请求转发。
  • between,表示在两个时间点之间,两个时间点用逗号分割。

也可以通过请求的方式匹配,就是请求方法:

spring:
  cloud:
    gateway:
      routes:
        - id: javaboy_route
          uri: http://httpbin.org/get
          predicates:
           - Method=GET

这个配置只会给 get 请求进行路由。

通过请求的路径进行匹配:

spring:
  cloud:
    gateway:
      routes:
        - id: javaboy_route
          uri: http://www.javaboy.org
          predicates:
             - Path=/2019/0612/{segment}

表示路径满足规则 /2019/0612/ 这个规则,都会进行转发,例如:
http://www.javaboy.org/2019/0612/git-install.html
http://www.javaboy.org/2019/0612/git-basic.html

通过参数进行匹配:

spring:
  cloud:
    gateway:
      routes:
        - id: javaboy_route
          uri: http://www.javaboy.org
          predicates:
             - Query=name

请求中一定要有 name 参数才会进行转发,否则不进行转发。
也可以指定参数和参数值。
例如参数的 keynamevalue 必须以 java 开始:

spring:
  cloud:
    gateway:
      routes:
        - id: javaboy_route
          uri: http://www.javaboy.org
          predicates:
            - Query=name,javaboy.*

以上这些方式也可以组合使用。


10.4 Filter

spring cloud gateway 中的过滤器分为两大类:

  • GatewayFilter
  • GlobalFilter

AddRequestParameter 过滤器的使用:

spring:
  cloud:
    gateway:
      routes:
        - id: javaboy_route
          uri: lb://provider   # lb 的意义:一个全局的过滤器(LoadBalanceClient),如果我们的 provider 启动了多个实例会自动的进行负载均衡
          filters:
            - AddRequestParameter=name,javaboy  # 第一个是参数,第二个是传入的值
          predicates:
            - Method=GET
   

这个过滤器就是在请求转发的时候,自动额外添加参数。

在这里插入图片描述

第 11 章 Spring Cloud Config

分布式配置中心解决方案:
国内:

  • 360:QConf
  • 淘宝:diamond
  • 百度:disconf

国外:

  • Apache Commons Configuration
  • owner
  • cfg4j

简介

spring cloud config 是一个分布式系统配置管理的解决方案,他包含了 clientserver。配置文件放在 Server 端,通过接口的形式提供给 client

spring cloud config 主要功能:

  • 集中管理各个环境,各个微服务的配置文件
  • 提供服务端和客户端的支持
  • 配置文件的修改后,可以快速生效
  • 配置文件通过 git或者svn 进行管理,天然支持版本回退功能。
  • 支持高并发查询,也支持多种语言开发。

准备工作

准备工作主要给 github 上提交数据。
本地准备好相应的配置文件,提交到 github 上:https://github.com/gtxxq007/configRepo


10.1 ConfigServer 搭建

首先创建一个 ConfigServer 工程,创建的时候添加 ConfigServer 依赖:
在这里插入图片描述

项目创建成功之后,项目的启动类上添加注解,开启 config server 功能:

package org.javaboy.configserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigserverApplication {

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

}

然后在配置文件中配置仓库的基本信息:

spring.application.name=config-server
server.port=8081

# 配置文件仓库地址
spring.cloud.config.server.git.uri=https://github.com/gtxxq007/configRepo.git
# 仓库中配置文件的目录
spring.cloud.config.server.git.search-paths=client1
# 仓库的用户名和密码
spring.cloud.config.server.git.username=gtxxq007
spring.cloud.config.server.git.password=root

配置完成之后,启动项目即可,接下里就可以访问配置文件了,访问地址如下

http://localhost:8081/client1/dev/main
在这里插入图片描述

实际上,访问地址有如下规则:

  • /{application}/{profile}/[{label}]

  • /{application}-{profile}.yml 这种默认的分支是 master 分支

  • /{application}-{profile}.properties 这种默认的分支是 master 分支

  • /{label}/{application}-{profile}.yml
    在这里插入图片描述

  • /{label}/{application}-{profile}.properties
    在这里插入图片描述

  • application 表示配置文件名

  • profile 表示配置文件 profile,例如 testdevprod

  • label 表示 git 分支,参数可选,默认就是 master

接下来,我们可以修改配置文件,并且重新提交到 github ,此时,刷新 ConfigServer 接口,就可以及时看到最新的配置内容了。


10.2 ConfigClient 搭建

创建一个 spring boot 项目,添加 ConfigClient的依赖:
在这里插入图片描述

项目创建成功之后,resource 目录下,添加 bootstrap.properties 配置,内容如下:

# 下面三行配置,分别对应 config-server 中的 {application}、{profile} 以及{label} 的占位符
spring.application.name=client1
spring.cloud.config.profile=dev
spring.cloud.config.label=main
# 告诉他我们配置文件的位置在哪里
spring.cloud.config.uri=http://localhost:8081
server.port=8082

接下来创建一个 HelloController 进行测试:

package org.javaboy.configclient.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;

/**
 * @author: yueLQ
 * @date: 2021-06-21 14:19
 */
@RestController
public class HelloController {
    @Value("${javaboy}")
    String javaboy;

    @GetMapping("hello")
    public String hello() throws UnsupportedEncodingException {
        String str=new String(javaboy.getBytes("ISO8859-1"),"GBK");
        return str;
    }
}

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


10.3 配置

使用占位符灵活控制查询目录:
修改 config-server 配置文件:

# 仓库中配置文件的目录
spring.cloud.config.server.git.search-paths={application}

这里的 {application} 占位符,表示链接上来的 client1spring.application.name 属性的值。
config-server 中,也可以用 {profile} 表示 clientspring.cloud.config.profile,也可以使用 {label} 表示 clientspring.cloud.config.label

虽然,在实际的开发环境中,配置文件一般都放置在 git 仓库中,但是,config-server 也支持将配置文件放在 classpath 下:

config-server 中添加如下配置:

# 表示让 config-server,从 classpath 下查找配置,而不是从 git 仓库中查找
spring.profiles.active=native

文件的目录结构如下:
在这里插入图片描述

也可以在 config-server 中,添加如下配置,表示指定配置文件的位置:

spring.cloud.config.server.native.search-locations=file:/E:/configRepo/client1

10.4 配置问价加解密

常见方案

  • 不可逆
  • 可逆加密

不可逆加密,就是理论上无法根据加密后的密文推算出明文。一般用在密码加密上,常见的算法如 MD5 消息摘要算法、SHA 安全散列算法。

可逆加密,看名字就知道可以根据加密后的密文推断出明文的加密方式,可逆加密一般又分为两种:

  • 对称加密
  • 非对称加密

对称加密是指加密的密钥和解密的密钥是一样的。常见算法 des3desaes

非对称加密就是加密的密钥和解密的密钥不一样,加密的叫做公钥,可以告诉任何人,解密的叫做私钥,只能自己知道。常见的算法有 RSA



对称加密

首先下载不限长度的 JCEhttps://www.oracle.com/java/technologies/javase-jce8-downloads.html

将下载的文件解压,解压出来的 jar 拷贝到 java 安装目录中:D:\Java\jdk1.8.0_161\lib\security
然后,在 config-serverbootstrap.yml 配置文件中,添加如下内容配置秘钥:

# 密钥
encrypt:
  key: 'javaboy'

然后,启动 config-server,访问如下地址,查看秘钥配置的是否 OKhttp://localhost:8081/encrypt/status

然后,访问:http://localhost:8081/encrypt,注意这是一个 post 请求,访问该地址,可以对一段明文进行加密。把加密后的明文存储到 git 仓库,存储的时候,一定要注意加一个 {cipher} 前缀。

在这里插入图片描述


非对称加密

非对称加密需要我们首先生成一个密钥对。

在命令行执行如下命令,生成 keystore:

keytool -genkeypair -alias config-server -keyalg RSA -keystore D:\springcloud\config-server.keystore
  • -genkeypair 生成秘钥对
  • -alias 给生成的密钥对取一个别名
  • -keyalg指定生成秘钥的算法
  • -keystore 生成的 keystore 存储的位置

命令执行结束之后,拷贝生成的 keystore 文件到 config-serverresource 目录下。
然后在 config-serverbootstrap.properties 目录中,添加如下配置:

# 文件的位置
encrypt.key-store.location=config-server.keystore
# 文件的别名
encrypt.key-store.alias=config-server
# 密码
encrypt.key-store.password=111111

encrypt.key-store.secret=111111

重启 config-server ,测试方法和对称加密一样。
注意 ,在 pom.xml 文件中的 build 节点上,添加如下配置,放置 keystore 文件被过滤掉。

<resources>
			<resource>
				<directory>src/main/resources</directory>
				<includes>
					<include>**/*.properties</include>
					<include>**/*.keystore</include>
				</includes>
			</resource>
		</resources>

10.5 ConfigServer安全管理

防止用户直接通过访问 config-server 看到配置文件内容,我们可以通过 spring security 来保护 config-server 接口。

首先在 config-server 中添加 spring security 依赖:

<dependency>
	 <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-security</artifactId>
	<version>2.3.3.RELEASE</version>
</dependency>

添加完成依赖之后,config-server 中的接口就自动被保护起来了。

默认生成的密码不好记,所以我们可以在 config-server 中,配置自己的用户名称和密码。
config-serverapplication.properties 配置文件中,添加如下配置,固定用户名和密码。

spring.security.user.name=javaboy
spring.security.user.password=javaboy123

然后,在 config-clientbootstrap.properties 配置文件中,添加如下配置:

spring.cloud.config.username=javaboy
spring.cloud.config.password=javaboy123

10.6 服务化

前面的配置都是直接在 config-client 中写死了 config-server 的地址。
如图:
在这里插入图片描述

如上配置,不符合微服务的理念。

启动 Eureka,然后为了让 config-serverconfig-client 都能注册到 eureka 上,给他们两个添加如下依赖:

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

然后,在他们两个的配置文件 application.properties 中配置注册中心信息。

eureka.client.service-url.defaultZone=http://localhost:1111/eureka

然后,修改 config-client 的配置文件,不在直接写死 config-server 的地址了。

# 开启通过 eureka 获取 config-server 的功能
spring.cloud.config.discovery.enabled=true
# 配置 config-server 服务名称
spring.cloud.config.discovery.service-id=config-server

注意,加入 eureka client之后,启动 config-server 可能会报错,此时我们生成一个 jks 格式的秘钥。

keytool -genkeypair -alias mytestkey -keyalg RSA -keypass 111111 -keystore D:\springcloud\config-service.jks -storepass 111111

生成之后,拷贝到 configserverresource 目录下,同时修改 bootstrap.properties 配置:


# 文件的位置
encrypt.key-store.location=config-service.jks
# 文件的别名
encrypt.key-store.alias=mytestkey
# 密码
encrypt.key-store.password=111111

encrypt.key-store.secret=111111

同时也修改 pom.xml 中的过滤条件:

     <resources>
			<resource>
				<directory>src/main/resources</directory>
				<includes>
					<include>**/*.properties</include>
					<include>**/*.jks</include>
				</includes>
			</resource>
	  </resources>

10.7 动态刷新

当配置文件发生变化之后,config-server 可以及时的感知到变化,但是 config-client 不会及时的感知到变化,默认情况下, config-client 只有重启才能加载到最新的配置文件。

首先给 config-client 添加如下依赖:

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

然后,添加配置,使 refresh 端点暴露出来。

management.endpoints.web.exposure.include=refresh

然后,再给 config-client 使用了配置文件的地方加上 @RefreshScope 注解,这样,当配置改变之后,只需要调用 refresh 端点, config-client 中的配置可以自动刷新。

@RestController
@RefreshScope
public class HelloController {
    @Value("${javaboy}")
    String javaboy;

    @GetMapping("hello")
    public String hello() throws UnsupportedEncodingException {
        String str=new String(javaboy.getBytes("ISO8859-1"),"GBK");
        return str;
    }
}

重启 config-client,以后,只要配置文件发生变化,发送 POST 请求,调用 http://localhost:8082/actuator/refresh
接口即可,配置文件就会自动刷新。
在这里插入图片描述


10.8 请求失败重试

config-client 在调用 config-server 时候,一样也可能发生请求失败的问题,这个时候,我们能可以配置一个请求重试的功能。

要在 config-client添加重试的功能,只需要添加如下依赖即可。

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

		<dependency>
			<groupId>org.springframework.retry</groupId>
			<artifactId>spring-retry</artifactId>
		</dependency>

然后,修改配置文件,开启失败快速反应。

# 开启失败快速反应
spring.cloud.config.fail-fast=true

然后,注释掉配置文件的用户名和密码,重启 config-client,此时加载配置文件失败,就会自动重试。
在这里插入图片描述
也可以通过如下配置保证服务的可用性:

# 开启失败快速反应
spring.cloud.config.fail-fast=true
# 请求重试的初始时间间隔
spring.cloud.config.retry.initial-interval=1000
# 最大的重试次数
spring.cloud.config.retry.max-attempts=6
# 重试时间间隔乘数,默认使 1.1
spring.cloud.config.retry.multiplier=1.1
# 最大间隔时间
spring.cloud.config.retry.max-interval=2000

在这里插入图片描述

第 11 章 Spring Cloud Bus

 

11.1 配置文件自动刷新

Spring Cloud Bus 通过轻量级的消息代理连接各个微服务,可以用来广播配置文件的更改,或者管理服务监控。

安装 Rabbit MQ
DockerRabbit MQ 安装命令:

docker run -d --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 5672:5672  rabbitmq:3-management

首先给 config-serverconfig-client 分别添加 Spring Cloud Bus 依赖。

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

然后,给两个分别配置,使他们都连接到 Rabbit MQ 上:

spring.rabbitmq.host=81.68.158.166
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

然后记得在 config-server 中,添加开启 bus-refresh 端点:

management.endpoints.web.exposure.include=bus-refresh

由于给 config-server 中的所有接口都添加了保护,所以刷新接口将无法直接访问。此时可以通过修改 Security 配置,对端点的权限做出修改:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启配置
        http.authorizeRequests()
                // 任何请求都要登录后才能访问
                .anyRequest().authenticated()
                .and()
                .httpBasic() // 允许 httpBasic
                .and()
                .csrf().disable(); // 禁止 csrf
    }
}

这段配置中,开启了 HttpBasic 登录,这样,在发送刷新请求的时候,可以直接通过 HttpBasic 的配置认证信息。

最后分别启动 config-serverconfig-client,然后修改配置信息提交到 GitHub,刷新 config-client 接口,查看是否发生变化。

然后,发送如下 post 请求:http://localhost:8081/actuator/busrefresh

在这里插入图片描述

这个 post 是针对 config-server 的,config-server 会把这个刷新的指令传到 rabbitmq,然后 rabbitmq 再把指令传给各个 client


11.2 刷新单个配置文件

如果更新配置文件之后,不希望每一个微服务去刷新配置文件,那么可以通过如下配置解决问题。

首先,给每一个 config-client 进行打包。

eureka.instance.instance-id=${spring.application.name}:${server.port}

然后,对 config-client 进行打包。

java -jar configclient-0.0.1-SNAPSHOT.jar --server.port=8082
java -jar configclient-0.0.1-SNAPSHOT.jar --server.port=8083

修改配置文件,并且提交到 GitHub 之后,可以通过如下方式刷新某一个微服务,例如只刷新 8082 的服务。

http://localhost:8081/actuator/busrefresh/client1:8082

client1:8082 表示服务的 instance-id,这里刷新比较慢等一下就好了。

在这里插入图片描述

第 12 章 Spring Cloud Stream

12.1 环境搭建

Spring Cloud Stream 用来构建消息驱动的微服务。

Spring Cloud Stream 中,提供了一个微服务和消息中间件之间的一个粘合剂,这个粘合剂叫做 BinderBinder负责与消息中间件进行交互。而我们开发者则通过 inputs 或者 outputs 这样的消息通道和 Binder 之间进行交互。

构建一个 Spring Cloud Stream 的项目,添加如下依赖 ,webrabbitmqcloud stream
在这里插入图片描述

项目创建成功之后,添加 RabbitMQ 的基本配置信息:

spring.rabbitmq.host=81.68.158.166
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

接下来创建一个简单的消息接收器:

// @EnableBinding 绑定某一个消息通道, Sink 是默认的消息通道
@EnableBinding(Sink.class) // 实现对消息通道的绑定
public class MsgReceiver {
    public final  static Logger logger= LoggerFactory.getLogger(MsgReceiver.class);
    @StreamListener(Sink.INPUT) // 监听消息的一个通道
    public void receive(Object payload){
        logger.info("Received"+payload);
    }
}

启动 stream 项目,然后在 rabbitmq 后台管理页面去发送一条消息。
在这里插入图片描述
后台结果:

在这里插入图片描述


12.2 自定义消息通道

首先创建一个名为 MyChannel 的接口:

package org.javaboy.stream;

import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;

/**
 * @author: yueLQ
 * @date: 2021-07-05 21:26
 */
public interface MyChannel {
    String INPUT = "javaboy-input";
    String OUTPUT = "javaboy-output";

    @Output(OUTPUT)
    MessageChannel output();
    @Input(INPUT)
    SubscribableChannel input();
}

  1. 注意,两个消息通道的名字是不一样的
  2. F 版本开始,默认使用通道的名称作为实例命令,所以这里的通道名称不可以相同(早期版本可以相同),这样的话,为了能够正常的发收消息,需要我们在 application.prperties 中做一些额外的配置。

接下来,自定义一个消息接收器,用来接受自己的消息通道里面的消息。

package org.javaboy.stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;

/**
 * @author: yueLQ
 * @date: 2021-07-05 21:43
 */
@EnableBinding(MyChannel.class)
public class MyReceiver2 {

   public static final Logger logger=LoggerFactory.getLogger(MyReceiver2.class);

   @StreamListener(MyChannel.INPUT)
   public void receive(Object payload){
       logger.info("received2:"+payload);
   }
}

在定义一个 HelloController 进行测试:

package org.javaboy.stream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: yueLQ
 * @date: 2021-07-05 21:47
 */
@RestController
public class HelloController {
    @Autowired
    MyChannel myChannel;

    @GetMapping("/hello")
    public void hello() {
        myChannel.output().send(MessageBuilder.withPayload("hello spring cloud stream!").build());
    }
}

同时,为了消息输入输出通道对接上(因为现在这两个通道名称不一样),在增加一点额外配置:

spring.cloud.stream.bindings..javaboy-input.destination=javaboy-topic
spring.cloud.stream.bindings..javaboy-output.destination=javaboy-topic

12.3 消息分组

默认情况下,如果消费者是一个集群,此时,一条消息会被多次消费,通过消费分组,我们可以解决这个问题。

只需要添加如下配置即可:

spring.cloud.stream.bindings.javaboy-input.group=g1
spring.cloud.stream.bindings.javaboy-output.group=g1

测试:将项目打包运行

java -jar stream-0.0.1-SNAPSHOT.jar --server.port=8081
java -jar stream-0.0.1-SNAPSHOT.jar --server.port=8080

访问 http://localhost:8080/hello 结果两个控制台,轮询打印输出。


12.4 消息分区

通过消息分区可以实现相同特征的消息总是被同一个实例进行处理,只需要添加如下配置即可:

spring.cloud.stream.bindings.javaboy-input.group=g1
spring.cloud.stream.bindings.javaboy-output.group=g1

# 开启消费分区 (消费者配置
spring.cloud.stream.bindings.javaboy-input.consumer.partitioned=true
# 消费者实例个数 (消费者配置
spring.cloud.stream.instance-count=2
# 当前实例的下标 (消费者配置
spring.cloud.stream.instance-index=0
# 这个表示这个消息将被下标为 1 的消费 (生产者配置
spring.cloud.stream.bindings.java-output.producer.partition-key-expression=1
#x 消费端节点数量(生产者配置
spring.cloud.stream.bindings.javaboy-output.producer.partition-count=2

接下来启动两个实例,注意,启动的时候,spring.cloud.stream.instance-index 要动态修改。

java -jar stream-0.0.1-SNAPSHOT.jar --server.port=8080 --spring.cloud.stream.instance-index=0
java -jar stream-0.0.1-SNAPSHOT.jar --server.port=8081 --spring.cloud.stream.instance-index=1

spring boot 版本2.5 以上失效


12.5 定时任务

每天定时执行的任务,可以使用 cron 表达式,有一种比较特殊的定时任务。例如几分钟后执行,这种可以结合 spring cloud stream + rabbitMQ 来实现

这个我们需要下载一个 rabbitmq 插件:https://dl.bintray.com/rabbitmq/community-plugins/3.7.x/rabbitmq_delayed_message_exchange/rabbitmq_delayed_message_exchange-20171201-3.7.x.zip

执行如下命令:
在这里插入图片描述

配置文件中,开启消息延迟功能:

# 开启消息延迟
spring.cloud.stream.rabbit.bindings.javaboy-input.consumer.default-exchange=true
spring.cloud.stream.rabbit.bindings.javaboy-output.producer.default-exchange=true

同时注意,消息输入输出通道的 destination 定义:

spring.cloud.stream.bindings.javaboy-input.destination=delay_msg
spring.cloud.stream.bindings.javaboy-output.destination=delay_msg

在发送消息的时候,设置消息延迟三秒:

package org.javaboy.stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @author: yueLQ
 * @date: 2021-07-05 21:47
 */
@RestController
public class HelloController {
    private Logger logger= LoggerFactory.getLogger(HelloController.class);
    @Autowired
    MyChannel myChannel;

    @GetMapping("/hello")
    public void hello() {
        logger.info("send msg"+new Date());
        myChannel.output().send(MessageBuilder.withPayload("hello spring cloud stream!").setHeader("x-delay",3000).build());
    }
}

同时,在接受消息的时候,也打印出延迟的时间:

package org.javaboy.stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;

import java.util.Date;

/**
 * @author: yueLQ
 * @date: 2021-07-05 21:43
 */
@EnableBinding(MyChannel.class)
public class MyReceiver2 {

   public static final Logger logger=LoggerFactory.getLogger(MyReceiver2.class);

   @StreamListener(MyChannel.INPUT)
   public void receive(Object payload){
       logger.info("received2:"+payload+new Date());
   }
}

在这里插入图片描述

第 13 章 链路追踪

 

13.1 简介

在大规模的分布式系统中,一个完整的系统是由很多个不同的服务共同支撑的。不同的系统可能分布在上千台服务器中,横跨多个数据中心。一旦系统出现问题,此时的定位就比较麻烦。

分布式链路追踪器:

在微服务的环境中,一次客户端请求,可能会引起数十次、上百次服务端服务之间的调用。一旦请求出现问题,我们需要考虑很多东西:

  • 如何快速定位问题
  • 如果快速确定此次客户端调用,都涉及那些服务
  • 到底是哪个服务出了问题

要解决如上问题,就涉及到分布式链路追踪器了。

分布式链路追踪系统主要用来跟踪服务调用记录以,一般来说,一个分布式链路追踪系统,有三个部分:

  • 数据收集
  • 数据存储
  • 数据展示

spring cloud SleuthSpring Cloud 提供的一套分布式链路追踪系统。

trace: 从请求到达系统开始,到给请求做出响应,这样一个过程为 trace
span:每次调用服务时,埋入的一个调用记录称之为 span
annotation:相当于 span 的语法,描述 span 所处的状态。


13.2 初体验

首先创建一个项目,引入 Spring Cloud Sleuth

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

接下来创建一个 HelloController,打印日志:

package org.javaboy.sleuth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author: yueLQ
 * @date: 2021-07-08 22:09
 */
@RestController
public class HelloController {

    private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

    @GetMapping("/hello")
    public String hello() {
        logger.info("hello Spring Cloud sleuth");
        return "hello Spring Cloud sleuth";
    }
}

可以给当前服务配置一个名字,这个名字在输出的日志中会体现出来。

spring.application.name=javaboy-sleuth

启动项目,测试,结果如下:

2021-07-08 22:40:57.480  INFO [javaboy-sleuth,fab33ac6d0948958,fab33ac6d0948958,false] 11004 --- [nio-8080-exec-7] org.javaboy.sleuth.HelloController       : hello Spring Cloud sleuth

如上便是 Spring Cloud Sleuth 的输出

在定义两个接口,hello2hello3,形成链路调用:

    @GetMapping("hello2")
    public String hello2() throws InterruptedException {
        logger.info("hello2");
        Thread.sleep(3000);
        return restTemplate.getForObject("http://localhost:8080/hello3", String.class);
    }

    @GetMapping("hello3")
    public String hello3() throws InterruptedException {
        logger.info("hello3");
        return "hello3";
    }

此时,访问 hello2,会先调用hello3,拿到返回结果,会给 hello2
在这里插入图片描述

一个 trace 由多个 span 组成,一个 trace 相当于一个调用链,而是一个 span 则是这个链中的每一次调用过程。

13.3 异步任务信息收集

spring cloud sleuth 中也可以收集到异步任务中的信息。

开启异步任务:

package org.javaboy.sleuth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableAsync
public class SleuthApplication {

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

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

创建一个 HelloService,提供一个异步任务方法

@Service
public class HelloService {

    private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

    @Async
    public String backgroundFun(){
        logger.info("backgroundFun");
        return "hello4";
    }
}

在创建一个 HelloController 中调用该异步方法:

    @Autowired
    private HelloService helloService;
    @GetMapping("hello4")
    public String hello4(){
        logger.info("hello4");
        return helloService.backgroundFun();
    }

启动项目进行测试,发现 Sleuth 也打印出了日志,在异步任务中,异步任务是单独的 spanid

在这里插入图片描述

spring cloud sleuth 也可以收集定时任务的信息。

首先开启定时任务支持:

package org.javaboy.sleuth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableAsync
@EnableScheduling
public class SleuthApplication {

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

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

然后,在 HelloService 中,添加定时任务,去调用 background 方法。

    @Scheduled(cron="0/10 * * * * ?")
    public void schel(){
     logger.info("start: ");
     backgroundFun();
     logger.info("end: ");
    }

然后访问 hello4 接口进行测试。

在定时任务中,每一次定时任务都会产生一个新的 Trace,并且在调用的过程中,SpanId 都是一致的,这个和普通的调用不一样。


13.4 Zipkin 简单介绍

Zipkin 本身是一个由 Twitter 公司开源的分布式追踪系统。
Zipkin 分为 server 端和 client 端,server 用来展示数据,client用来收集 + 上报数据。

安装:

Zipkin 要先把数据存储起来,这里我们使用 Elasticseach 来存储,首先安装 eses-head

es 安装命令:

docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.1.0

可视化工具有三种安装方式:

  1. 直接下载软件安装
  2. 通过 Docker 安装
  3. 安装 Chrome/Firefox 插件。

这里我们采用第三种方式。

RabbitMQ 安装。
Zipkin 安装


docker run -d -p 9411:9411  --name zipkin -e ES_HOSTS=172.28.163.168 -e STORAGE_TYPE=elasticsearch -e ES_HTTP_LOGGING=BASIC  -e RABBIT_URI=amqp://guest:guest@172.28.163.168:5672    openzipkin/zipkin
  • ES_HOSTS es 地址
  • STORAGE_TYPE 数据存储方式
  • RABBIT_UROI 要连接 rabbitMq 地址

13.5 实践

首先创建一个 zipkin 项目,添加 webzipkinrabbitmqstream

<?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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.javaboy</groupId>
    <artifactId>zipkin01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>zipkin01</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream</artifactId>
            <scope>test</scope>
            <classifier>test-binder</classifier>
            <type>test-jar</type>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

创建好项目之后,配置 zipkinrabbitMQ

spring.application.name=zipkin01
# 开启链路追踪
spring.sleuth.web.client.enabled=true
# 配置采样比例,默认为 0.1 1 为百分之百
spring.sleuth.sampler.probability=1
# zipkin 地址
spring.zipkin.base-url=http://101.200.140.74:9411
# 开启 zipkin
spring.zipkin.enabled=true
# 追踪消息的发布类型
spring.zipkin.sender.type=rabbit
# rabbitMQ
spring.rabbitmq.host=101.200.140.74
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest



接下来,提供一个 HelloController 的方法:

package org.javaboy.zipkin01.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: yueLQ
 * @date: 2021-07-10 22:21
 */
@RestController
public class HelloController {
    private static final Logger logger= LoggerFactory.getLogger(HelloController.class);
    @GetMapping("hello")
    public String hello(String name){
        logger.info("zipkin-hello");
        return "hello "+name+" !";
    }
}

然后在创建一个 zipkin02zipkin01 的依赖配置相同。

spring.application.name=zipkin02
# 开启链路追踪
spring.sleuth.web.client.enabled=true
# 配置采样比例,默认为 0.1 1 为百分之百
spring.sleuth.sampler.probability=1
# zipkin 地址
spring.zipkin.base-url=http://101.200.140.74:9411
# 开启 zipkin
spring.zipkin.enabled=true
# 追踪消息的发布类型
spring.zipkin.sender.type=rabbit
# rabbitMQ
spring.rabbitmq.host=101.200.140.74
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

server.port=8081




HelloController

package org.javaboy.zipkin02.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author: yueLQ
 * @date: 2021-07-10 22:31
 */
@RestController
public class HelloController {
    private static final Logger logger= LoggerFactory.getLogger(HelloController.class);
    @Autowired
    RestTemplate restTemplate;

    @GetMapping("hello")
    public  void hello(){
        String javaboy = restTemplate.getForObject("http://localhost:8080/hello?name={1}", String.class, "javaboy");
        logger.info(javaboy);
    }
}

zipkin:
在这里插入图片描述
在这里插入图片描述

es:
在这里插入图片描述
rabbitMQ:
在这里插入图片描述

在这里插入图片描述

第 14 章 Spring Cloud Alibaba

spring cloud alibaba 是阿里巴巴提供的一套微服务开发一站式解决方案。

主要提供的功能:

  • 服务限流和降级
  • 服务注册与发现
  • 分布式配置中心
  • 消息驱动
  • 分布式事务
  • 阿里云对象存储
  • 阿里云短信

提供的组件

  • sentinel
  • nacos

优势:

  1. 中文文档
  2. 没有另起炉灶,可以方便的集成到现有的项目中
  3. 阿里本身在高并发,高性能上的经验,让我们有理由相信这些组件的可靠。

14.1 Nacos

nacos 主要提供了服务发现,服务配置以及服务管理。

基本特性:

  • 服务发现
  • 动态配置
  • 动态 DNS 服务
  • 服务以及元数据的管理。

安装

解压之后,如果是 win,直接在 bin 目录下双击 startup.cmd 启动。
如果 linux,在 bin 目录下执行 sh startup.sh -m standalon 启动单机命令。

注意,系统一定要配置好 jdk,测试一下 java 和 javac 两个命令都要存在

在这里插入图片描述

nacos 启动成功之后,浏览器输入命令 http://101.200.140.74:8848/nacos/,就能看到登录页面了,如果有登录页面,默认用户名和密码都是 nacos

先看配置中心类似于 spring cloud config
首先在服务端配置,点击配置管理-》配置列表-》+
这里主要配置三个东西 Data IDGroup以及要配置的内容。

Data ID 的格式是 ${prefix}-${spring.profile.active}.${file-extension}

${prefix}的值,默认为 spring.application.name 的值。
${spring.profile.active} 表示当前所处于的环境
${file-extension} 表示配置文件的扩展名

在这里插入图片描述

配置完成之后,新建一个 spring boot 项目,添加 nacos 依赖:

	<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
			<version>2.2.0.RELEASE</version>
		</dependency>

在这里插入图片描述
新建 bootstarp.properties 配置文件,配置 nacos 信息。

spring.application.name=nacos
spring.cloud.nacos.server-addr=101.200.140.74:8848
spring.cloud.nacos.config.file-extension=properties

提供一个测试:

package org.javaboy.nacos.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: yueLQ
 * @date: 2021-07-18 23:07
 */
@RestController
@RefreshScope
public class HelloController {
    @Value("${name}")
    String name;

    @GetMapping("hello")
    public String hello(){
        return name;
    }
}


14.2 Nacos 做注册中心

nacos 做注册中心,可以代替 Eureka
创建 spring boot 项目,添加依赖:
在这里插入图片描述

添加配置:

spring.application.name=nacos01
spring.cloud.nacos.discovery.server-addr=101.200.140.74:8848

然后创建一个测试的 controller

package org.javaboy.nacos1.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: yueLQ
 * @date: 2021-07-20 21:18
 */
@RestController
public class HelloController {

    @Value("${server.port}")
    Integer port;

    @GetMapping("/hello")
    public String hello(){
        return "hello: "+port;
    }
}

将项目打包,启动两个实例:

java -jar nacos1-0.0.1-SNAPSHOT.jar --server.port=8080
java -jar nacos1-0.0.1-SNAPSHOT.jar --server.port=8081

在创建一个 consumer 消费者,配置,依赖和 nacos01 一致

spring.application.name=nacos02
server.port=8083
spring.cloud.nacos.discovery.server-addr=101.200.140.74:8848

添加 RestTemplate

package org.javaboy.nacos2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class Nacos2Application {

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

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

创建测试的 controller

package org.javaboy.nacos2.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author: yueLQ
 * @date: 2021-07-20 21:32
 */
@RestController
public class HelloController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("hello")
    public String hello(){
        String forObject = restTemplate.getForObject("http://nacos01/hello", String.class);
        return forObject;
    }
}

在这里插入图片描述

访问 http://localhost:8083/hello 接口,查看是否进行负载均衡。


14.3 Sentinel 介绍
  • 使用场景丰富
  • 有完备的实时监控
  • 广泛的开源生态

sentinel 整体上可以分为两个核心部分:

  • 核心库
  • 控制台
14.4 安装

首先下载控制台 jar,这是一个 spring boot 工程,下载之后,直接使用 spring boot 启动命令启动。
下载地址:
https://github.com/alibaba/Sentinel/releases

在这里插入图片描述


14.5 基本用法

创建一个 spring boot 工程,添加 sentinel 依赖:

		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
			<version>2.2.0.RELEASE</version>
		</dependency>

创建成功之后,配置 sentinel 控制台地址:

spring.application.name=sentinel
spring.cloud.sentinel.transport.dashboard=127.0.0.1:8080
server.port=8081

在创建一个测试的 controller

package org.javaboy.sentinel.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: yueLQ
 * @date: 2021-07-24 12:19
 */
@RestController
public class HelloController {

    @GetMapping("hello")
    public String hello(){
        return "hello sentinel";
    }
}

在这里插入图片描述
测试限流:

@Test
	void contextLoads() {
		RestTemplate restTemplate = new RestTemplate();

		for (int i = 0; i < 15; i++) {
			String forObject = restTemplate.getForObject("http://localhost:8081/hello", String.class);
			System.out.println("forObject = " + forObject+": "+ new Date());
		}

	}

在这里插入图片描述


14.6 Nacos 中配置流控规则

sentinel 中添加整合 nacos 的依赖:

    <!--		将 nacos 和sentinel 关联的依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>
        <!--nacos 配置中心的依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>

创建 bootstrap.properties 文件连接 nacos

spring.cloud.nacos.config.server-addr=101.200.140.74:8848

application.properties 中添加 sentienl 配置:

spring.application.name=sentinel
spring.cloud.sentinel.transport.dashboard=127.0.0.1:8080
server.port=8081
spring.cloud.sentinel.datasource.ds.nacos.server-addr=101.200.140.74:8848
# nacos 中 dataid 的值,随便填写
spring.cloud.sentinel.datasource.ds.nacos.data-id=sentinel-root
# group_id 默认值
spring.cloud.sentinel.datasource.ds.nacos.group-id=DEFAULT_GROUP
# 规则类型
spring.cloud.sentinel.datasource.ds.nacos.rule-type=flow

nacos 添加新的配置文件规则:
在这里插入图片描述

  • resource:相当于我们要拦截的限流的接口
  • limitApp:流控规则的调用源, 不同的调用源做不同的流控方案,例如手机端,小程序的,default 是不区分类型。
  • grade:阈值类型,1 是 QPS ,0 是线程数
  • count:单机阈值
  • clusterMode:是否是集群模式
  • strategy:流控模式,0 是 直接
  • controlBehavior:流控效果,0 快速失败

启动项目,访问 hello 接口,然后查看 sentinel 控制台:
在这里插入图片描述
如上图所示已经配置成功了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值