springcloud

什么是微服务

In short, the microservice architectural style , is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.

提炼:

  • 微服务是一种架构风格
  • 基于原来单个应用开发出一系列微小服务
  • 每个服务运行在自己的计算机进程内,也就是可独立部署和升级
  • 服务之间使用轻量级HTTP rest交互
  • 每个服务基于项目中的业务进行拆分
  • 这些服务都是基于分布式管理
  • 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术

官方定义:基于单个应用围绕业务进行拆分,拆分出的每个服务独立项目,他们独立部署运行在自己的进程里,基于分布式的管理

通俗定义:微服务是一种架构,这种架构是将单个的整体应用程序分割成更小的项目关联的独立的服务。一个服务通常实现一组独立的特性或功能,包含自己的业务逻辑和适配器。各个微服务之间的关联通过暴露api来实现。这些独立的微服务不需要部署在同一个虚拟机,同一个系统和同一个应用服务器中。

集群 cluster:同一种软件服务的多个服务节点共同为系统提供服务过程 称之为该软件服务集群

分布式 distribute: 不同的软件集群共同为一个系统提供服务 这个系统称之为分布式系统

为什么微服务?

单体应用

image-20240410183538363

优点

  • 单一架构模式在项目初期很小的时候开发方便,测试方便,部署方便,运行良好。

缺点

  • 应用随着时间的推进,加入的功能越来越多,最终会变得巨大,一个项目中很有可能数百万行的代码,互相之间繁琐的jar包。
  • 久而久之,开发效率低,代码维护困难
  • 还有一个如果想整体应用采用新的技术,新的框架或者语言,那是不可能的。
  • 任意模块的漏洞或者错误都会影响这个应用,降低系统的可靠性

微服务架构

image-20240410183556101

优点

  • 将服务拆分成多个单一职责的小的服务,进行单独部署,服务之间通过网络进行通信
  • 每个服务应该有自己单独的管理团队,高度自治
  • 服务各自有自己单独的职责,服务之间松耦合,避免因一个模块的问题导致服务崩溃

缺点

  • 开发人员要处理分布式系统的复杂性
  • 多服务运维难度,随着服务的增加,运维的压力也在增大
  • 服务治理 和 服务监控 关键

微服务是一种经过良好架构设计的分布式架构方案,===>springcloud: 一个工具 里边有各种各样组件 这些组件能帮我们解决上述问题

微服务架构特征

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
  • 面向服务:微服务对外暴露业务接口
  • 自治:团队独立、技术独立、数据独立、部署独立
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

架构的演变

image-20240410213244352

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 单一架构  All In One Application
- 起初当网站流量很小时,将所有功能都写在一个应用里面,对整个应用进行部署,以减少部署节点和成本。对于这个架构简化增删改查的工作量的 数据访问框架(ORM)是关键。
- 所有的代码都写在一起 JSP+Mysql+Tomcat 所有东西都写在jsp页面里
- ORM是关键 如何解决对象关系映射 ==》框架 Hibernate JPA Mybatis
# 2. 垂直架构 分层开发 Vertical Application
- 随着访问量的逐渐增大,单一应用增加机器带来的加速度越来越小,提升效率的方法之一是将应用拆成互不相干的几个应用,以提升效率。此时, 用于加速前端页面开发的web框架(MVC)是关键。
- MVC关键 控制器是关键 ===》struts struts2 springmvc springboot
# 3. 分布式服务架构 Distributed Service
- 当垂直应用越来越多,应用之间的交互不可避免,将核心业务抽取出来。作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时提高业务复用及整合的分布式服务框架(RPC)是关键。
- tomcat集群 mysql集群 redis集群组成分布式
- RPC关键 远程过程调用 作用:服务间通信一种手段
- RPC传输效率远高于http OSI七层 物理层(高) 数据链路层 网络层 传输层(RPC)会话层 表示层 应用层(http)
# 4. 流动计算架构即微服务架构 Elastic Computing
- 在分布式架构下,当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量。 提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA Service Oriented Architecture)是关键。
- 把一个系统基于业务进行一个个服务拆分 每个服务拆分出来到时候都是一个集群部署
- 服务的治理和监控是关键

微服务的解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
# 国内阿里系
- springboot + dubbo +zookeeper 最早期 由于dubbo长时间停更失去了机遇
# spring cloud 技术栈
1. spring cloud netflix 最早期一栈式解决方案 16-17 netflix后来闭源 组件
基于美国Netflix公司开源的组件进行封装,提供了微服务一栈式的解决方案。
2. spring cloud spring 自己封装微服务解决方案 spring团队吸收netflix后自己推出的 自己组件 + 继续使用netflix组件
目前spring官方趋势正在逐渐吸收Netflix组件的精华,并在此基础进行二次封装优化,打造spring专有的解决方案
3. spring cloud alibaba 阿里巴巴解决方案 基于spring cloud netflix 自己组件 + 继续使用spring,netflix组件
在Spring cloud netflix基础上封装了阿里巴巴的微服务解决方案。
# 如何选择?
- 我们三个组件都用用各自技术最优秀的组件去构建项目
- 我们要能区分哪些组件是netflix,spring,alibaba的
- 我们暂时只学习netflix,spring的

Spring Cloud引言

官方定义

Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.gconfiguration management ,service discovery, circuit breakers, intelligent routing, micro-proxy, control bus).Coordination of distributed systems leads to boiler plate patterns, and using Spring Cloud developers can quickly stand up services and applications that implement those patterns. —[摘自官网]

1
2
3
4
5
6
7
8
9
# 翻译
springcloud为开发人员提供了在分布式系统中快速构建些通用模式的工具(例如配置管理、服务发现、断路器、智能路由、微代理、控制总线)。分布式系统的协调导致了锅炉板模式,使用springcloud开发人员可以快速地建立实现这些模式的服务和应用程序。
# 通俗理解
springcloud是一个含概多个子项目的开发工具集,集合了众多的开源框架,他利用了Spring Boot开发的便利性实现了很多功能,如服务注册,服务注册发现,负载均衡等.SpringCloud在整合过程中主要是针对Netflix(奈飞)开源组件的封装.SpringCloud的出现真正的简化了分布式架构的开发
NetFlix是美国的一个在线视频网站,微服务业的翅楚,他是公认的大规模生产级微服务的杰出实践者,NetFlix的开源组件已经在他大规模分布式微服务环 境中经过多年的生产实战验证,因此Spring Cloud中很多组件都是基于NetFlix

简单定义:springcloud含有众多微服务工具集 提供了一系列组件(服务注册 发现 负载均衡 路由组件 统一配置管理) 去解决我们微服务遇到的每一个问题 帮助我们快速构建一套分布式应用

注:每个组件都是一个单独的项目 springcloud是一个由众多独立组件(子项目)组成的大型综合项目

核心架构及其组件

image-20240411000411299

1
2
3
4
5
6
7
8
# 1.核心组件说明
- eurekaserver、consul、nacos 服务注册中心组件
- rabbion & openfeign 服务负载均衡 和 服务调用组件
- hystrix & hystrix dashboard 服务断路器 和 服务监控组件
- zuul、gateway 服务网关组件
- config 统一配置中心组件
- bus 消息总线组件
# 这些核心组件接下来我们会一个个学习 最后基于我们的学习过的组件搭建一个大型微服务项目

springcloud命名和springboot版本选择

springcloud是利用了springboot的便利性 每一个微服务都是springboot应用 故和springboot的版本有极大的关联关系 必须要严格一致 不然是跑不起来的。

springcloud命名

1
2
3
4
5
6
7
8
# springcloud命名
- 最开始不是选择数字命名
springcloud是一个由众多独立组件(子项目)组成的大型综合项目,原则每个子项目上有不同的发布节奏,都维护自己发布版本号,为了更好的管理springcloud的版本,通过一个资源清单B0M(Bi11 of Materials) ,为避免与子项目的发布号混滑,所以没有采用版本号的方式。而是通过命名的方式。这些名字是按字母顺序排列的。如伦敦地铁站的名称(“天使"是第一个版本,“布里斯顿”是第二个版本,“卡姆查”是第三个版本)。当单个项目的点发布累积 到一个临界量,或者其中个项目中有一 个关键缺陷需要每个人都可以使用时, 发布序列将推出名称以“. SRX"结尾的“服务发布", 其中"X"是一个数字。

# 2.伦敦地铁站名称[了解] A-Z
Ange1、Brixton. Canden、Dalston. Edgware. F inchley、Greenwrich. Hoxton

# Hoxton是最后一个非数字版本 最新命名2023.0.1

image-20240410233742303

springcloud和springboot版本对应关系 官网可看

1
2
3
4
5
6
7
8
9
10
11
# 版本选择官方建议 https://spring.io/projects/spring-cloud#overview
- Angel 版本基于springboot1.2.x版本构建与1.3版本不兼容
- Brixton 版本基于springboot1.3.x版本构建与1.2版本不兼容
`2017年Brixton and Angel release官方宣布报废
- Camden 版本基于springboot1.4.x版本构建并在1.5版本通过测试
`2018年Camden release官方宣布报废
- Dalston、Edgware 版本基于springboot1.5.x版本构建目前不能再springboot2.0.x版本中使用
`Dalston(达尔斯顿)将于2018年12月官方宣布报废。Edgware将遵循Spring Boot 1.5.x的生命周期结束。
- Finchley 版本基于springboot2.0.x版本进行构建,不能兼容1.x版本
- Greenwich 版本基于springboot2.1.x版本进行构建,不能兼容1.x版本
- Hoxton 我们这里用的版本 版本基于springboot2.2.x版本进行构建

image-20240410234102737

提供者与消费者

服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)

服务调用关系

服务提供者:暴露接口给其它微服务调用
服务消费者:调用其它微服务提供的接口
提供者与消费者角色其实是相对的
一个服务可以同时是服务提供者和服务消费者

环境搭建

思路整理

1
2
3
4
5
6
7
springboot&springcloud
版本:
springcloud Hoxton.SR6(1-10)
springboot 2.2.5版本
jdk1.8即jdk8
maven 3.x
idea 2018

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1. 创建springcloud_parent ----springcloud父项目包括springboot+springcloud父项目
采用maven聚合方式全局搭建一个所谓的springcloud父项目 为了统一管理springboot和springcloud版本号
2. 在父项目中继承springboot父项目
<!--继承springboot的父项目-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
3. 维护springcloud依赖
<!--自定义properties属性-->
<properties>
<!--定义springcloud使用版本号-->
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>

<!--维护版本-->
<dependencyManagement>
<dependencies>
<!--维护springcloud版本,并不会引入具体依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version> <!--此处是引用故我们上边要定义出来-->
<!--
spring-cloud-dependencies也是一个父项目,一个项目只能有一个父项目我们上边已引 故只能用这种方式再次引入
-->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

创建一个普通的maven项目,删除src 作为父模块管理维护依赖 同时检查项目结构里项目和模块sdk jdk 和maven上的java版本以及maven配置仓库

image-20240410183707213
image-20240410183719596

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.blb</groupId>
<artifactId>springcloud_parent</artifactId>
<version>1.0-SNAPSHOT</version>

<!--继承springboot的父项目-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>

<!--自定义properties属性-->
<properties>
<!--定义springcloud使用版本号-->
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>

<!--维护版本-->
<dependencyManagement>
<dependencies>
<!--全局管理springcloud版本,并不会引入具体依赖-->
<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>

</project>

服务注册中心组件

服务的管理者 不完成任何业务功能 纯作服务注册中心

所谓服务注册中心就是在整个的微服务架构中单独提出一个服务,这个服务不完成系统的任何的业务功能,仅仅用来完成对整个微服务系统的服务注册,服务发现,服务健康状态的监控和管理功能,以及服务元数据信息存储

元数据信息存储:即记录集群每台主机的ip 和 端口

image-20240411151423751

1
2
3
4
# 1.服务注册中心
- 可以对所有的微服务的信息进行存储,如微服务的名称、IP、端口等
- 可以在进行服务调用时通过服务发现查询可用的微服务列表及网络地址进行服务调用
- 可以对所有的微服务进行心跳检测,如发现某实例长时间无法访问,就会从服务注册表移除该实例。

服务注册中心可以说是微服务架构中的通讯录,它记录了服务和服务地址的映射关系,在分布式架构中,服务会注册到这里,当服务需要调用其他服务时,就到这里找到服务地址进行调用。

常用的服务注册中心

springcloud支持的多种服务注册中心Eureka(netflix)Consul(java)Zookeeper(Go)、以及阿里巴巴推出Nacos。这些注册中心在本质上都是用来管理服务的注册和发现以及服务状态的检查的。

Eureka

neflix的组件

简介

Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务。SpringCloud将它集成在其子项目spring-cloud-netflix中, 以实现SpringCloud的服务注册和发现功能。
Eureka包含两个组件:Eureka Server和Eureka Client。

image-20240411153038810

eureka的作用

服务提供者启动时向eureka注册自己的信息
eureka保存这些信息
消费者根据服务名称向eureka拉取提供者信息

服务消费者利用负载均衡算法,从服务列表中挑选一个

服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
eureka会更新记录服务列表信息,心跳不正常会被剔除
消费者就可以拉取到最新的信息

开发Eureka Server服务注册中心

学习springcloud组件三步:引依赖、写配置、加注解

在父项目下创建springboot项目 springcloud_01eureka_server 不完成任何业务功能 纯作服务注册中心

引入springboot依赖 springcloud_01eureka_server/pom.xml

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

引入springboot配置文件

写入口类测试springboot是否可以启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.chabai;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author 刘晔
* @version 1.0
* description:
*/
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class,args);
}
}

注:若报错则可能是jdk版本太高不支持调低即可


以上是一个纯springboot项目构建过程,想要让它作为一个eureka server需经过以下配置


引入eureka server依赖

1
2
3
4
5
<!--引入eureka server依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

写配置文件 springcloud_01eureka_server/src/resources/application.properties

1
2
3
4
#eureka server端口号 默认是8761
server.port=8761
#eureka server服务注册中心地址 暴露服务地址 给客户端注册
eureka.client.service-url.defaultZone=http://localhost:8761/eureka

在入口类加入注解 开启Eureka Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.blb;

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

@SpringBootApplication
@EnableEurekaServer//开启当前应用是一个服务注册中心
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class,args);

}
}

访问

1
http://localhost:8761/

image-20240411171446838

问题

image-20240411171957538

通过配置解决

1
2
3
4
#指定服务名称,注意服务名不能出现下划线 默认服务名不区分大小写,推荐大写 服务名称至关重要 必须唯一
spring.application.name=eurekaserver
eureka.client.fetch-registry=false # 关闭eureka.client立即注册
eureka.client.register-with-eureka=false # 是否将自己注册到注册中心 让当前应用仅仅是服务注册中心

Eureka Client开发

Eureka Client就是基于业务拆分出来的一个个微服务,站在Eureka Server角度服务即是微服务又是客户端

在父项目下创建springboot项目 order_client 要见名知意 以订单服务为例

引入springboot依赖 pom.xml

1
2
3
4
5
<!--引入springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

**引入springboot配置文件 指定微服务端口 指定微服务名 ** application.properties

1
2
server.port=8888									   #指定微服务端口
spring.application.name=ORDERCLIENT #指定微服务名称唯一标识

写入口类测试springboot是否可以启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.chabai;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author 刘晔
* @version 1.0
* description:
*/
@SpringBootApplication
public class OrderClientApplication {
public static void main(String[] args) {
SpringApplication.run(OrderClientApplication.class,args);
}
}

引入引入eureka client依赖

1
2
3
4
5
<!--引入eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

继续写配置 指定服务注册地址(其实前边配置也可放到这块来写)

1
eureka.client.service-url.defaultZone=http://localhost:8761/eureka   #eureka注册中心地址

在入口类加入注解 开启eureka客户端 启动之前的8761的服务注册中心,再启动eureka客户端服务

作为客户端 必须保证服务端是正常开启的 我们才能正常注册客户端到服务注册中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.chabai;

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


@SpringBootApplication
@EnableEurekaClient //让当前微服务作为一个eureka serve客户端 进行微服务注册
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class,args);
}
}

开启服务端此时并没有服务注册

image-20240411180032690

开启客户端查看eureka server的服务注册情况

image-20240411175929233

eureka自我保护机制

自我保护机制

默认是开启自我保护的,但是只有网路通信故障等原因时才触发则它们将进入自我保护模式。这样做是为了确保灾难性网络事件不会清除 eureka 注册表数据,并将其传播到下游的所有客户端。

怎么判断网络正常与否

通过心跳机制,判断期望心跳数( 理想心跳数)和实际心跳数
如果上一分钟心跳数 < 期望心跳数的85%,此时我认为网络有波动,是不正常的, 就会触发自我保护机制
如果上一分钟心跳数 >= 期望心跳数的85%,此时我认为网络是正常的,不会触发自我保护;

自我保护机制触发现象

管理界面如下提示

image-20240411181154814

在自我保护模式下,eureka 服务器将停止逐出所有实例,直到:

  1. 注册中心会一直检测网络情况,当发现网络正常时,就会自动退出自我保护,这时不正常的客户端就会被剔除,或者
  2. 自我保护被禁用(见下文)

配置中关闭了自我保护之后,客户端每30s续约一次,服务端每60s扫描一次超过90s还没有续约的客户端,超时就会立即剔除。

在eureka server端禁用自我保护

1
2
eureka.server.enable-self-preservation=false  #关闭自我保护
eureka.server.eviction-interval-timer-in-ms=60*1000 #默认1分钟

eureka client配置自我保护阈值

1
2
eureka.instance.lease-expiration-duration-in-seconds=90 #用来修改eureka server默认接受心跳的最大时间 默认是90s
eureka.instance.lease-renewal-interval-in-seconds=30 #指定客户端多久向eureka server发送一次心跳 默认是30s

自我保护机制开或关选择

开发环境下,我们我们启动的服务数量较少而且会经常修改重启。如果开启自我保护机制,很容易触发Eureka客户端心跳占比低于85%的情况。使得Eureka不会剔除我们的服务,从而在我们访问的时候,会访问到可能已经失效的服务,导致请求失败,影响我们的开发。

在生产环境下,我们启动的服务多且不会反复启动修改。环境也相对稳定,影响服务正常运行的人为情况较少。适合开启自我保护机制,让Eureka进行管理。

自我保护机制好处

Eureka服务端为了防止Eureka客户端本身是可以正常访问的,但是由于网路通信故障等原因,造成Eureka服务端失去于客户端的连接,从而形成的不可用。

因为网络通信是可能恢复的,但是Eureka客户端只会在启动时才去服务端注册。如果因为网络的原因而剔除了客户端,将造成客户端无法再注册到服务端。

eureka停止更新

1
2
3
# https://github.com/Netflix/eureka/wiki 
- 在1.x版本项目还是很活跃 用的话推荐用1.x版本 但是不在建议用了
- 在2.x版本中停止维护

管理界面客户端ip+端口显示

默认是主机名+服务名+端口

1
# 略

Eureka集群搭建

Eureka Server集群
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 完全集群
a.创建3个springboot项目
b.都引入eureka server依赖
c.服务端都配置文件application.properties 注:一台机器跑三个节点端口号不能相同
配置文件如下:
nodel1:
server.port=8761
spring.application.name=eurekaserver
eureka.client.service-url.defaultZone=http://localhost:8762/eureka,http://localhost:8763/eureka
eureka.client.register-with-eureka=false #不再将自己同时作为客户端进行注册
eureka.client.fetch-registry=false #关闭作为客户端时从eureka server获取服务信息
nodel2:
server.port=8762
spring.application.name=eurekaserver
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,http://localhost:8763/eureka
eureka.client.register-with-eureka=false #不再将自己同时作为客户端进行注册
eureka.client.fetch-registry=false #关闭作为客户端时从eureka server获取服务信息
nodel3:
server.port=8763
spring.application.name=eurekaserver
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,http://localhost:8762/eureka
eureka.client.register-with-eureka=false #不再将自己同时作为客户端进行注册
eureka.client.fetch-registry=false #关闭作为客户端时从eureka server获取服务信息
d.在每个项目入口类加入 @EnableEurekaServer
# 也可利用ided简单创建
# 验证集群是否搭建成功
集群数据要保持一致 我只要向一个服务端注册 其它属于集群的服务端 都有这个注册的客户端 则集群搭建成功
但是我们不推荐这样只写一个注册中心地址 防止一个注册中心down掉 而无法向剩余正常注册中心注册 但可用这个测试服务端集群搭建是否成功
客户端配置如下:
server.port=8888 #服务端口号
spring.application.name=eurekaclient8888 #服务名称唯一标识
eureka.client.service-url.defaultZone=
http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka #向多个注册中心同时注册
Eureka Client集群
1
2
3
4
5
6
7
8
9
# 客户端服务集群即微服务集群 对于微服务我们所有代码配置都相同 只有部署到机器的端口不一样 
`注同一服务的集群:我们只需保证服务名一致,端口不一样即可
server.port=8888 #服务端口号
spring.application.name=eurekaclient8888 #服务名称唯一标识
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka #eureka注册中心地址

server.port=8889 #服务端口号
spring.application.name=eurekaclient8888 #服务名称唯一标识
eureka.client.service-url.defaultZone=http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka #eureka注册中心地址

image-20240410184031696

idea提供的简单搭建方法

image-20240412145035975

Eureka缺点

每次开发微服务 使用eureka 服务注册中心需要我们自己每次通过代码形式开发

Consul

引言和安装

简介

不是netflix spring alibaba的组件 属于google

consul是一个可以提供服务发现,健康检查,多数据中心,Key/Value存储等功能的分布式服务框架,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案,使用起来也较为简单。Consul用Golang实现,因此具有天然可移植性(支持Linux、Windows和Mac OS X);安装包仅包含一个可执行文件,方便部署。

作用:管理微服务中所有服务注册 发现 管理服务元数据信息存储(服务名 地址列表) 心跳健康检查

Consul安装

Consul下载网址 在指定目录解压即可,注意目录不要包含中文

启动consul服务注册中心

启动 Consul

启动服务注册中心,在consul安装目录下打开cmd 配置好环境变量后可不在安装目录启动

1
consul agent -dev #也可-server 我们测试用-dev单节点即可 集群的话再用-server

image-20240410184317358

访问 Consul 管理界面 Consul必须启动

1
http://localhost:8500   默认端口8500

管理界面基本介绍

1
2
3
dc1: 数据中心名称  datacenter 默认为:dc1 指定数据中心名称启动 consul agent -dev -datacenter-xx
services: 当前consul服务中心注册服务列表 默认:client和server同时启动自己注册自己 会出现一个consul
nodes: 用来查看consul集群节点

image-20240411221142809

开发consul 客户端即微服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
1. 创建独立springboot应用 consul_client
2. 引入spring-boot-starter-web依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3. 引入springboot配置文件 application.properties
#配置服务端口号和服务名
server.port=8082
spring.application.name=CONSULCLIENT
4. 写入口类测试springboot是否可以启动
package com.chabai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication//代表这是一个springboot入口应用
public class ConsulClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConsulClientApplication.class,args);
}
}
5. 引入consul客户端依赖 作用:能把当前服务注册到指定的consul服务注册中心
<!--引入consul客户端依赖 discovery:服务注册和发现-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
6. 再次写配置文件
#配置服务端口号和服务名
server.port=8082
spring.application.name=CONSULCLIENT
#consul server 服务注册地址
spring.cloud.consul.host=localhost #注册consul服务的主机
spring.cloud.consul.port=8500 #注册consul服务的端口号
spring.cloud.consul.discovery.service-name=${spring.application.name} #指定注册的服务名称 默认就是应用名 可自定义
7. 在入口类加客户端注解@EnableDiscoveryClient(除eureka client)
package com.chabai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;s
@SpringBootApplication //代表这是一个springboot入口应用
@EnableDiscoveryClient //作用:通用服务注册客户端注解 代表 consul client zk client nacos client
public class ConsulClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConsulClientApplication.class,args);
}
}

8.启动服务并在consul界面查看服务信息

image-20240410184253805
发现服务不能使用,原因如下

consul server 检测所有客户端心跳,但是发送心跳时client必须给与响应该服务才能正常使用,现在所有客户端中并没有引入健康检查依赖,所以导致健康检查始终不通过,导致服务不能使用

9.consul 开启健康监控检查

默认情况consul监控健康是开启的,但是必须依赖健康监控依赖才能正确监控健康状态所以直接启动会显示错误,引入健康监控依赖之后服务正常

1
2
3
4
5
<!--引入健康检查依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
10. 再次写配置文件
server.port=8082
spring.application.name=CONSULCLIENT
#consul server 服务注册地址
spring.cloud.consul.host=localhost #注册consul服务的主机
spring.cloud.consul.port=8500 #注册consul服务的端口号
#关闭consu服务的健康检查 在生产情况下不推荐关闭健康检查 默认开启true 设为flase不用引入健康检查依赖 就可正常启动服务
spring.cloud.consul.discovery.register-health-check=true
spring.cloud.consul.discovery.service-name=${spring.application.name} #指定注册的服务名称 默认就是应用名 可自定
11. 再次启动consul client如下:
服务正常

image-20240410184340734

12.并没有自我保护机制

不同注册中心区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1.CAP定理
- CAP定理:CAP定理又称CAP原则,指的是在一个分布式系统中,一致性(Consistency)、可用性
- (Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两
- 点,不可能三者兼顾。
`一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的
数据副本)
`可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用
性)
`分区容忍性(P),就是高可用性,一个节点崩了,并不影响其它的节点(100个节点,挂了几个,不影响服务,
越多机器越好)

# 2.Eureka特点
- Eureka中没有使用任何的数据强一致性算法保证不同集群间的Server的数据一致,仅通过数据拷贝的方式争取注册
- 中心数据的最终一致性,虽然放弃数据强一致性但是换来了Server的可用性,降低了注册的代价,提高了集群运行
- 的健壮性。

# 3.Consul特点
- 基于Raft算法,Consul提供强一致性的注册中心服务,但是由于Leader节点承担了所有的处理工作,势必加大了注
- 册和发现的代价,降低了服务的可用性。通过Gossip协议,Consul可以很好地监控Consul集群的运行,同时可以方
- 便通知各类事件,如Leader选择发生、Server地址变更等。
-

# 4.zookeeper特点
- 基于Zab协议,Zookeeper可以用于构建具备数据强一致性的服务注册与发现中心,而与此相对地牺牲了服务的可用
- 性和提高了注册需要的时间。

image-20240410184355695

微服务间通信方式

Rest Template

Rest Template通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
1. 什么是微服务
定义:基于单体应用围绕业务进行服务拆分,拆分出的每个服务独立项目,独立运行,他们独立部署运行在自己的计算机进程里,基于分布式的管理

2. 如何解决微服务的服务间通信问题 在springcloud中服务间调用方式主要是使用 http restful方式进行服务间调用 可实现服务间高度解耦 不同语言开发的服务可是实现通信
a.Http Rest方式 使用http协议进行数据传递 JSON格式
b.RPC方式 远程过程调用 二进制
RPC传输效率远高于http OSI七层 物理层(高) 数据链路层 网络层 传输层(RPC)会话层 表示层 应用层(http)

3. 如何在java代码中发起http方式请求
spring框架提供的HttpClient对象 RestTemplate 发起一个http请求 RestTemplate就相当于一个http客户端即浏览器 spring框架提供的RestTemplate类可用于在应用中调用rest服务,它简化了与http服务的通信方式,统一了RESTful的标准,封装了http链接, 我们只需要传入url及返回值类型即可。相较于之前常用的HttpClient,RestTemplate是一种更优雅的调用RESTful服务的方式。

4. 实现服务间通信案例 基于RestTemplate的服务调用 以用户和订单服务为例
1.开发两个测试服务 users_client orders_client 两个服务都是两个独立的springboot应用 注意:这里服务仅仅用来测试,没有实际业务意义
2.引入springboot依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3.提供springboot配置文件
users_client:
server.port=8888
spring.application.name=USERS
orders_client:
server.port=9999
spring.application.name=ORDERS
4.写入口类测试springboot是否可以启动
users_client:
package com.chabai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDiscoveryClient
public class UsersApplication {
public static void main(String[] args) {
SpringApplication.run(UsersApplication.class,args);
}
}
orders_client:
package com.chabai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDiscoveryClient
public class OrdersApplication {
public static void main(String[] args) {
SpringApplication.run(OrdersApplication.class,args);
}
}
5.引入consul客户端依赖 作用:能把当前服务注册到指定的consul服务注册中心 引入健康检查依赖
<!--引入consul客户端依赖 discovery:服务注册和发现-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--引入健康检查依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
6.再次写配置文件
users_client:
server.port=8888
spring.application.name=USERS
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.service-name=${spring.application.name}#可不写都是默认的
spring.cloud.consul.discovery.register-health-check=true #可不写都是默认的
orders_client:
server.port=9999
spring.application.name=ORDERS
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.service-name=${spring.application.name} #可不写都是默认的
spring.cloud.consul.discovery.register-health-check=true #可不写都是默认的
7.在入口类加客户端注解@EnableDiscoveryClient(除eureka client) 让当前微服务作为一个服务客户端 进行微服务注册
8.启动服务并在consul界面查看服务信息
成功==》服务已准备好

image-20240412004103063

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
9.编写controller 
UsersController:
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);

@GetMapping("user")
public String invokeDemo() {
log.info("user demo ...");
//1.调用订单服务 服务地址url:http://localhost:9999/order 必须是get方式 接收返回值 String类型
RestTemplate restTemplate = new RestTemplate();
String orderResult = restTemplate.getForObject("http://localhost:9999/order ", String.class);
log.info("调用订单服务成功:{}",orderResult);//logi才能正常导入类
return "调用订单服务成功,结果为:"+orderResult;
}
}
OrdersController:
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrdersController {

private static final Logger log = LoggerFactory.getLogger(OrdersController.class);

@GetMapping("order")
public String demo(){
log.info("order demo ...");
return "order demo ok";
}
}

10.Spring中对REST请求的处理方式 延申了解即可不是重点

深入了解可参考:Spring中RestTemplate的使用方法 - lasdaybg - 博客园 (cnblogs.com)

Spring中可以使用RestTemplate来操作REST资源,主要包含以下几个方法:

  • getForEntity(),getForObject(),发送HTTP GET请求,getForEntity()返回的是ResponseEntity对象,里面包含响应实体对象及响应状态码,而getForObject()则直接返回响应实体对象;
  • postForEntity(),postForObject(),发送HTTP POST请求,postForEntity()返回的是ResponseEntity对象,里面包含响应实体对象及响应状态码,而postForObject()则直接返回响应实体对象;
  • put(),发送HTTP PUT请求;
  • delete(),发送HTTP DELETE请求;
  • exchange(),可以发送GET、POST、PUT和DELETE中的任意一种请求,同时还可以自定义请求头。

11.启动两个服务 访问:http://localhost:8888/user 成功实现通信 如下:

image-20240412004535371

12.使用Rest Template对象实现服务间通信存在的问题

Rest Template是直接基于服务地址调用没有在服务注册中心获取服务,也没有办法完成服务的负载均衡如果需要实现服务的负载均衡需要自己书写服务负载均衡策略

使用RestTemplate通信存在的问题

image-20240412105119245

解决负载均衡问题

自定义负载均衡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.chabai.controller;

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

@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);

@GetMapping("user")
public String invokeDemo() {
log.info("user demo ...");
//1.调用订单服务 服务地址url:randomHost()随机生成 必须是get方式 接收返回值 String类型
RestTemplate restTemplate = new RestTemplate();
String orderResult = restTemplate.getForObject("http://"+randomHost()+"/order", String.class);
log.info("调用订单服务成功:{}",orderResult);//logi才能正常导入类
return "调用订单服务成功,结果为:"+orderResult;
}

//自定义随机策略
public String randomHost(){
List<String> hosts = new ArrayList<>();
hosts.add("localhost:9999"); //假设有两个订单服务
hosts.add("localhost:9990");

//生成随机数 只能在0-hosts.size()波动
int i = new Random().nextInt(host.size());
return host.get(i);
}
}
Ribbon负载均衡客户端组件

spring基于neflix开源组件进行的封装 springcloud-netflix-ribbon netflix的组件 作用:用来实现请求调用时负载均衡

1
2
3
# 0.说明
- 官方网址:https://github.com/Netflix/ribbon
- Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用

1.Rubbon组件实现负载均衡原理

根据调用服务的服务id去服务注册中心获取对应服务id的服务列表,并将服务列表拉取到本地进行缓存,然后在本地通过默认的轮询的负载均衡策略在现有的列表中选择一个可用的节点提供服务,图解如下:

image-20240412122231163

注:如果刚缓存到本地其中一台服务器down了,如果轮询又刚好到这台服务器,那么本次请求失败

2.使用Ribbon组件+RestTemplate实现请求的负载均衡调用 以用户和订单服务为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2.0场景:使用用户服务调用订单服务 
# 2.1用户服务引入ribbon依赖
- 如果使用的是eureka client 和 consul client,无须引入依赖,因为在eureka,consul客户端依赖 中默认集成了ribbon组件
- 如果使用的client中没有ribbon依赖需要显式引入如下依赖
<!--引入ribbon依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
# 2.2使用RestTemplate + ribbon 进行服务调用并实现负载均衡 三种方法
- DisvoveryClient
- LoadBalanceClient
- @LoadBalance
- consul客户端依赖已集成ribbon组件 故我们一启动springboot工厂就会有DisvoveryClient、LoadBalanceClient 对象 如果我们要使用这两个对象直接注入就可以了
# 2.2.1Discoveryclient 服务注册发现客户端对象
- discoveryclient 服务发现对象 根据服务id去服务注册中心获取对应的服务列表到本地中
- 缺点:没有负载均衡,需要自己实现负载均衡
注:ribbon只是一个负载均衡客户端 不负责发请求 发请求还是要用RestTemplate
代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;

@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);
private DiscoveryClient discoveryClient; //服务注册发现客户端对象
@Autowired //现在spring推荐我们使用构造器注入
public UsersController(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}

@GetMapping("user")
public String invokeDemo() {
log.info("user demo ...");

//2.使用ribbon组件+RestTemplate实现负载均衡调用
//2.1使用Discoveryclient 进行客户端调用
//获取服务列表到本地
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("ORDERS");//写服务名大小写均可
serviceInstances.forEach(serviceInstance -> log.info("服务主机:{} 服务端口:{} 服务地址:{}",serviceInstance.getHost(),serviceInstance.getPort(),serviceInstance.getUri()));

//使用RestTemplate进行微服务间通信
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(serviceInstances.get(0).getUri() + "/order", String.class);
//String result = new RestTemplate().getForObject(serviceInstances.get(0).getUri()+ "/order", String.class); 也可合并这样写

return "ok"+ result;
}
}
1
2
3
4
# 2.2.2loadBalanceClient 负载均衡的客户端对象
- LoadBalancerClient 负载均衡对象客户端,根据服务id去服务注册中心获取服务列表,根据默认负载均衡策略,选择 列表中一台机器进行返回
- 缺点:使用时需要每次先根据服务id获取一个负载均衡机器之后再通过restTemplate调用服务
代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.chabai.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

import java.util.List;
@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);

@Autowired
private LoadBalancerClient loadBalancerClient; //负载均衡的客户端对象

@GetMapping("user")
public String invokeDemo() {
log.info("user demo ...");

ServiceInstance serviceInstance = loadBalancerClient.choose("ORDERS"); //默认负载均衡策略轮询策略
log.info("服务主机:{} 服务端口:{} 服务地址:{}",serviceInstance.getHost(),serviceInstance.getPort(),serviceInstance.getUri());
String result = new RestTemplate().getForObject(serviceInstance.getUri()+ "/order", String.class);

return "ok" + result;
}
}

image-20240412161010608
可以看出就是轮询的方式跳转

1
2
3
4
5
# 2.2.3@LoadBalanced      负载均衡的客户端注解
- @LoadBalance 负载均衡客户端注解
- 修饰范围:作用在方法上
- 作用: 让当前方法|当前对象具有负载均衡特性
代码实现如下:

建配置类 src/main/com/chabai/config/Beanconfig

使用RestTemplate()每次都需要我们new RestTemplate()太麻烦 我们可以把它交给工厂管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.chabai.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration //代表这是一个springboot配置类 相当于 spring.xml ==>工厂 创建对象 bean id class=""
public class BeanConfig {

//工厂中创建restTemplate 日后使用 声明为成员变量注入即可
@Bean
@LoadBalanced //使对象具有ribbon的负载均衡特性
public RestTemplate restTemplate(){
return new RestTemplate();
}
}

@LoadBalanced注解使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

package com.chabai.controller;

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

import java.util.List;

@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);

@Autowired
private RestTemplate restTemplate;

@GetMapping("user")
public String invokeDemo() {
log.info("user demo ...");

//2.3@LoadBalanced 作用:使对象具有ribbon的负载均衡特性
String result = restTemplate.getForObject("http://ORDERS/order", String.class);

return "ok" + result;
}
}

Ribbon组件细节

Ribbon组件支持哪些负载均衡策略

追负载均衡源码追出支持哪些负载均衡策略

首先分析如何追:

追逐注解源码无意义 注解在java中就是一个标识 它里边不会具体写出负载均衡策略是什么

再次分析可发现负载均衡是在LoadBalancerClient类的choose()方法实现的

**第一步:**查看LoadBalancerClient源码发现没有实现choose()方法,再看继承的父类有choose()方法但没有具体实现故choose()方法只能是LoadBalancerClient的子类实现的,选中LoadBalancerClient ctrl+h查看继承关系 发现有两个子类,只有可能是这两个子类实现的choose()方法

image-20240412220549899

image-20240412220614144

image-20240412220354948

**第二步:**进入两个子类ctrl+f12发现都实现了choose()方法 我们无法判断默认是在哪个子类的choose()方法实现的负载均衡

第三步:到这里 无法判断是哪个 追不下去了 可返回第一步打断点调试 看默认实现类是哪个 发现是choose()方法的默认实现类是RibbonLoadBalancerClient

image-20240412213135018

**第四步:**进入RibbonLoadBalancerClient ctrl+12查看文件结构 找到Choose方法 分析发现负载均衡具体在RibbonLoadBalancerClientl类的getServer()方法里面实现

image-20240412210047792

第五步:查看getServer()源码 发现是在ILoadBalancer的chooseServer()方法实现负载均衡的 点进去ILoadBalancer()查看源码 发现ILoadBalancer并没有实现chooseServer()方法 故只可能是父类实现的chooseServer()方法 我们查看chooseServer()的实现类 发现有三个实现类 image-20240412210501874

image-20240412210902821

**第六步:**到这里 无法判断是哪个 追不下去了 可返回第五步打断点调试 看默认实现类是哪个

打断点 调试 f7进入方法

image-20240412222450797

f8下一步 发现进入ZoneAwareLoadBalancer类但是别急 继续f8 发现有这么一段代码 return super.chooseServer(key) super代表返回父类的方法 我们查看父类DynamicServerListLoadBalancer发现没有chooseServer()方法,那么就只能是父类的父类BaseLoadBalancer实现的chooseServer()方法,我们查看BaseLoadBalancer类 发现确实是BaseLoadBalancer类实现的chooseServer()方法,我们也可以通过f7进入方法 发现确实是进入了BaseLoadBalancer类也确实是这个类实现chooseServer()方法 完全匹配我们追父类时的猜想。调试验证的图片如下,追父类就不给出图片了

image-20240412224910771

**第七步:**继续调试f8到return this.rule.choose(key) 发现是底层的规则做的负载均衡

image-20240412225438968

我们f7进入规则 f8下一步到 Optional server = this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key) 这句的意思就是根据传的key从所有规则获取一种规则给我们 此时的key是default 即采用默认规则

image-20240412225913226

我们返回BaseLoadBalancer找看这个rule即规则到底是什么

image-20240412230431095

image-20240412230454820

从而我们可以推出默认负载均衡策略是RoundRobinRule轮询 且真正决定负载均衡策略的是IRule这个接口

**第八步:**根据private static final IRule DEFAULT_RULE = new RoundRobinRule()可知看IRule这个接口有哪些实现类即可看出有哪些负载均衡策略

选中IRule右键 图表 显示图 新面板 右键 将类添加到图 选择IRule 双击添加 选中IRule右键添加实现添加完所有实现即可

image-20240412232122765

内置负载均衡规则类 名称 规则描述
RoundRobinRule 轮询策略 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。按顺序循环选择server
AvailabilityFilteringRule 可过滤策略 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule 响应时间加权测率 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule 区域敏感性策略 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。
BestAvailableRule 最低并发策略 忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule 随机策略 随机选择一个可用的服务器。
RetryRule 重试策略 重试机制的选择逻辑

负载均衡原理

image-20240410184605069
基本流程如下:

  • 拦截我们的RestTemplate请求http://userservice/user/1
  • RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
  • DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
  • eureka返回列表,localhost:8081、localhost:8082
  • IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
  • RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求

修改服务的默认负载均衡策略

方式一:修改配置类或在启动类定义一个新的IRule

1
2
3
4
@Bean
public IRule randomRule(){
return new RandomRule();
}

方式二 :配置文件方式 推荐

在user-service的application.yml文件中

1
2
#修改用户服务调用订单服务默认负载均衡策略不在使用轮询 使用随机策略 写全限命名 复制引用即可
ORDERS.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

两者区别,方式一是访问所有服务都是随机,方式二是访问userservice这一种服务是随机

如果方式一想要特殊化定制,这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下

放在启动类扫描的包外面

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MySelfRule {

@Bean
public IRule myRule(){
return new RandomRule();
}
}

加上@RibbonClient注解 name是服务名称,configuration是配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.lun.myrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;

@SpringBootApplication
@EnableEurekaClient
//添加到此处
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class OrderMain80
{
public static void main( String[] args ){
SpringApplication.run(OrderMain80.class, args);
}
}

饥饿加载

1
2
3
4
5
6
7
8
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
123
ribbon:
eager-load:
enabled: true
clients: userservice

Ribbon默认负载轮询算法原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
默认负载轮训算法: rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始。

List instances = discoveryClient.getInstances(“CLOUD-PAYMENT-SERVICE”);

如:

List [0] instances = 127.0.0.1:8002
List [1] instances = 127.0.0.1:8001
8001+ 8002组合成为集群,它们共计2台机器,集群总数为2,按照轮询算法原理:

当总请求数为1时:1%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求数位2时:2%2=О对应下标位置为0,则获得服务地址为127.0.0.1:8002
当总请求数位3时:3%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求数位4时:4%2=О对应下标位置为0,则获得服务地址为127.0.0.1:8002
如此类推…

Ribbon组件停止维护

1
2
# Ribbon组件现在状态
- https://github.com/Netflix/ribbon

image-20240412234628178

1
# ribbon-core ribbon-loadbalancer 依然在大规模生产实践中部署 意味着日后如果实现服务间通信负载均衡依然使用ribbon组件

未解决问题:代码写死

解决负载均衡问题,但是没有解决路径写死问题,还是不利于我们的维护

前边两大组件快速回顾image-20240413141633592

image-20240413141351478

OpenFeign 组件

实现服务间通信组件 属于spring

简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# OpenFeign组件历史
- Feign(neflix) --> 维护状态-->OpenFeign(spring) 特性和使用方式一摸一样
# 什么是Feign组件
- OpenFeign组件一个Rest Client相当于http client 和RestTemplate作用一样都是http client 但不同的是RestTemplate是Http客户端而OpenFeign是伪Http客户端。需要我们给底层声明。
- Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性(可以使用springmvc的注解),可使用Feign 注解和JAX-RS注解。Feign支持可插拔的编码器和解码器。Feign默认集成了Ribbon,默认实现了负载均衡的效果并且springcloud为feign添加了springmvc注解的支持。
- 伪Http客户端: OpenFeign确实可以帮我们发请求,但是真正帮我们发请求发的不是它,是它底层包裹的spring框架提供的HttpClient对象RestTemplate发起的http请求(可结合服务器和代理服务器理解)。
# 为什么OpenFeign
- OpenFeign优势
封装后使服务间通信变得简单:
1.使用:写一个接口 加一个注解
2.调用服务代码:自动完成数据传递过程对象转换
默认集成了Ribbon,默认实现了负载均衡的效果
- 解决了RestTemplate+Ribbon的存在问题所有问题
1.路径写死引起的代码冗余 维护成本增高
2.使用不灵活 不能自动转换响应结果为对应对象
3.必须使用Ribbon实现负载均衡
# 现在实现服务间通信的方法
- RestTemplate
- RestTemplate+Ribbon
- OpenFeign
# 为什么说OpenFeign使用起来麻烦呢?
OpenFeign不是一个真正的http client,使用时好多东西都需要我们显示的去给它指定
要想使用它好的特性 就要学会适应它的麻烦

OpenFeign组件使用

以商品和类别服务为例 让类别服务调用商品服务

创建两个独立的springboot应用 并注册到服务注册中心 consul

1
2
3
4
5
6
7
8
0.启动服务注册中心consul
1.在父模块新建两个maven空项目 分别为 categorie_client product_client
2.引入springboot依赖 引完依赖别忘了import change
3.引入springboot配置文件 注:这里可把后续需做的配置能提前写的可先写一下 端口号和服务名最好先配下
4.构建入口类测试springboot应用是否可以正常启动
5.引入服务注册中心consul和健康检查依赖 引完依赖别忘了import change
6.写配置注册到consul server
7.在入口类加注解@EnableDiscoveryClient 开启服务注册 并测试是否可以正常注册到consul

使用OpenFeign进行服务调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
8.开发服务接口(controller) 在里边写服务 ProductController CategoryController
9.先不进行服务调用测试两个服务接口是否正常
10.在服务调用方引入依赖OpenFeign 即在类别服务引入
11.在服务调用方入口类加入注解@EnableFeignClients 开启Feign客户端调用支持
12.实现服务调用: 写接口加注解 回到CategoryController实现真正调用商品服务
- 要单独写接口故我们要新建包放这个接口 com/chabai/feignclient 命名调用服务名+Client 这个接口不是一个普通的接口是一个`fegin接口`, 我们要把要将来要调用服务的方法声明到这个接口
- 加注解 注解包含调用服务服务的id 这个注解写完这个接口就被工厂托管了
如:@FeignClient(value="PRODUCT") //value:用来书写调用服务的服务id 可不写value
- 写接口 即写被调用商品服务的客户端 接口只声明不实现 声明被调用商品服务接口即可,日后这个接口的方法都会通过openfegin来通信,其实底层还是使用RestTemplate
如:商品服务里此时只有product服务 get请求方式 我们可以这样写接口
package com.chabai.feignclient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
//声明调用商品服务接口
@FeignClient(value="PRODUCT") //value:用来书写调用服务的服务id 可不写value
public interface ProductClient {
//声明调用商品服务中的produc接口
@GetMapping("/product")
public String product(); //声明方法和服务路径和被调用服务方法和服务路径一样即可
//返回值和形参列表和路径必须一致,方法名可以不一致 public也可不写
}
**这个被调用商品服务的客户端主要是基于SpringMVC的注解来声明远程调用的信息**,比如:
- 服务名称:PRODUCT
- 请求方式:GET
- 请求路径:/product
- 请求参数:xxx
- 返回值类型:String
这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。

- 回到CategoryController调用商品服务中的produc接口
我们希望在这里调用商品服务,故回到这里,因为我们已有被调用商品服务的客户端,故直接注入即可,最后调用其里边需要调用的方法即可完成调用
package com.chabai.controller;
import com.chabai.feignclient.ProductClient;
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;
@RestController
public class CategoryController {
private static final Logger log = LoggerFactory.getLogger(CategoryController.class);

@Autowired
private ProductClient productClient;

@GetMapping("/category")
public String category(){
log.info("category service ...");
String product = productClient.product();
return "category ok" +product;
}
}
13.测试是否成功调用服务
1
2
3
14.测试负载均衡  默认实现了负载均衡
- 使用idea快速创建功能再创建一个 商品服务节点
- 发现确实实现了负载均衡

服务间通信之参数传递

1
2
3
4
5
6
7
8
9
10
# 微服务架构中服务间通信手段?
http协议: springcloud: 1.RestTeamplate 2.OpenFegin(推荐)
# 服务间通信之参数传递
服务和服务之间通信,不仅仅是调用,往往在调用过程中还伴随着参数传递,接下来重点来看看OpenFeign在调用服务时如何传递参数
# 参数传递类型
- 传递零散类型参数
- 传递对象类型
- 数组或集合类型
# 流程
我们执行CategoryController 执行到productClient.test("茶白", 22) 开始执行调用伴随着参数传递 openfegin会根据我们在feignclient的声明参数,传递类型和接口 底层进行一系列的加工成对应请求 ProductController里接到请求会有对应接口做出处理 最终完成伴随着参数传递的调用
零散类型传递

如: 八种基本类型 + String + 日期类型

querystring方式传递参数 ?name=chabai

注意:在openfegin接口声明中必须给参数加入注解@RequestParm指明传递方式为querystring方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
1. 在被调用服务的接口即商品接口里定义一个接收零散类型参数接口 接收?name=xxx&age=xxx方式请求传递的参数  ProductController
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProductController {

private static final Logger log = LoggerFactory.getLogger(ProductController.class);

@Value("${server.port}") //属性注入 注解方式
private int port;

//定义一个接收零散类型参数接口 接收?name=xxx&age=xxx方式传递的参数
@GetMapping("test")
//默认把?后的参数赋值变量 可不加@RequestParam("name")
public String test(String name,Integer age){
log.info("name: {} age: {}",name,age);
return "test ok ,当前服务的端口为:"+port;
}
}
2. 声明调用商品服务中的test接口name,age参数 ProductClient
package com.chabai.feignclient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

//声明调用商品服务接口
@FeignClient(value="PRODUCT") //value:用来书写调用服务的服务id 可不写value
public interface ProductClient {

//声明调用商品服务中test接口传递name,age参数
@GetMapping("test")
String test(String name,Integer age);
}
3. 回到CategoryController调用商品服务中的test接口传递参数
package com.chabai.controller;
import com.chabai.feignclient.ProductClient;
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;

@RestController
public class CategoryController {

private static final Logger log = LoggerFactory.getLogger(CategoryController.class);

@Autowired
private ProductClient productClient;

@GetMapping("/category")
public String category(){
log.info("category service ...");
String result = productClient.test("茶白", 22); //注 我们这里写死是为了演示以后跟业务整合可把业务对象值传过来,或者利用category方法接收的参数
return "category ok " +result;
}
}
4. 启动CategoryController服务会报错:test()方法不让我们传递多参数
“Method has too many Body parameters”
- 解释:类别服务调用商品服务,要用到openfegin来通信,但是openfegin是一个伪http client,底层有可能走的还是RestTemplate对象(真正的http client),传参的时候懵了,它是接受这个参数的,但是底层要用真实的http client对象传这个参,底层压根不知道这个参数是哪种方法传过来的,无法组织(零散类型传参有两种方式,底层无法判断,没有组织参数能力,没有浏览器聪明),
- 但注意传一个参数不会报错,当只有一个参数时底层不用加注解默认走的是?方式传递参数
- 日后我们不管几个参数,都明确告诉要怎么传
5. 解决办法
- 在openfegin接口声明中必须给参数加入注解@RequestParm指明传递方式为?name=chabai
代码如下:
- 声明调用商品服务中的test接口传递name,age参数 显性声明以querystring方式传递
package com.chabai.feignclient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

//声明调用商品服务接口
@FeignClient(value="PRODUCT") //value:用来书写调用服务的服务id 可不写value
public interface ProductClient {

//声明调用商品服务中test接口传递name,age参数 以querystring方式传递
@GetMapping("test")
//底层在组织的时候以?name=xxx&age=xxx方式拼到test后边作为请求
String test(@RequestParam("name") String name, @RequestParam("age") Integer age); //@RequestParam("xxx")传的变量名以xxx为主 必须显示写出key即@RequestParam("name")
//不然会报错
//我们要传递参数到商品服务的故一定要和商品服务的变量名保持一致
//不一致商品服务方法接收不到参数

//声明调用商品服务中的produc接口
@GetMapping("/product")
public String product(); //声明请求方法和服务路径和被调用服务请求方法和服务路径一样即可
}
6. 访问http://localhost:8081/category测试

路径传递参数 url/chabai/22

注意:在openfegin接口声明中必须给参数加入注解@PathVariable指明传递方式为路径传递方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
1. 在被调用服务的接口即商品接口里定义一个接收零散类型参数接口 接收test1/22/chabai方式请求传递的参数 ProductController
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

private static final Logger log = LoggerFactory.getLogger(ProductController.class);
@Value("${server.port}") //属性注入 注解方式
private int port;

//定义一个接收零散类型参数接口 接收test1/22/chabai方式请求传递的参数
@GetMapping("test1/{id}/{name}")
//默认把?后的参数赋值变量 必须加@PathVariable("id") 明确表示要取路径id的值作为变量id的值
public String test1(@PathVariable("id") Integer id, @PathVariable("name") String name){
log.info("name: {} id: {}",name,id);
return "test1 ok ,当前服务的端口为:"+port;
}
}
2. 声明调用商品服务中的test1接口传递name,age参数 显性声明以路径传递参数传递
package com.chabai.feignclient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

//声明调用商品服务接口
@FeignClient(value="PRODUCT") //value:用来书写调用服务的服务id 可不写value
public interface ProductClient {

//声明调用商品服务中test1接口传递name,age参数 以路径传递方式传递零散类型参数
@GetMapping("test1/{id}/{name}")
//底层在组织的时候把id的值赋给路径{id}站位作为请求
String test1(@PathVariable("id") Integer id, @PathVariable("name") String name);
}
3. 回到CategoryController调用商品服务中的test1接口传递参数
package com.chabai.controller;
import com.chabai.feignclient.ProductClient;
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;

@RestController
public class CategoryController {

private static final Logger log = LoggerFactory.getLogger(CategoryController.class);

@Autowired
private ProductClient productClient;

@GetMapping("/category")
public String category(){
log.info("category service ...");
String result = productClient.test1(22,"chabai"); //注 我们这里写死是为了演示以后跟业务整合可把业务对象值传过来,或者利用category方法接收的参数
return "category ok " +result;
}
}
4. 访问http://localhost:8081/category测试
对象类型参数传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
0. 要传递对象故我们先定义一个对象 新建实体类  com/chabai/entity/Product
package com.chabai.entity;
import java.util.Date;

public class Product {
private Integer id;
private String name;
private Double price;
private Date bir;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Double getPrice() {
return price;
}

public void setPrice(Double price) {
this.price = price;
}

public Date getBir() {
return bir;
}

public void setBir(Date bir) {
this.bir = bir;
}

public Product(Integer id, String name, Double price, Date bir) {
this.id = id;
this.name = name;
this.price = price;
this.bir = bir;
}

public Product() {
}

@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
", price=" + price +
", bir=" + bir +
'}';
}
}
1. 在被调用服务的接口即商品接口里定义一个接收对象类型的接口
package com.chabai.controller;
import com.chabai.entity.Product;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
public class ProductController {

private static final Logger log = LoggerFactory.getLogger(ProductController.class);

@Value("${server.port}") //属性注入 注解方式
private int port;

//定义一个接收对象类型参数的接口
@PostMapping("/test2") //接收对象站在rest角度我们一般都是post put patch请求方式 查询或者删除一般传零散类型
public String test2(Product product){
log.info("product:{}",product);
return "test2 ok ,当前服务的端口为:"+port;
}
}
2.声明调用商品服务中test2接口传递一个商品对象
package com.chabai.feignclient;
import com.chabai.entity.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

//声明调用商品服务接口
@FeignClient(value="PRODUCT") //value:用来书写调用服务的服务id 可不写value
public interface ProductClient {

@PostMapping("/test2")
//声明调用商品服务中test2接口传递一个商品对象
String test2(Product product);
}

3.cv实体类到category_client服务 注:真正开发时实体类是公共搭建的 不用cv即可访问到

4.回到CategoryController调用商品服务中的test2接口传递参数
package com.chabai.controller;
import com.chabai.entity.Product;
import com.chabai.feignclient.ProductClient;
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 java.util.Date;

@RestController
public class CategoryController {

private static final Logger log = LoggerFactory.getLogger(CategoryController.class);

@Autowired
private ProductClient productClient;

@GetMapping("/category")
public String category(){
log.info("category service ...");
String result = productClient.test2(new Product(11,"chabai",123.123,new Date()));//注 我们这里写死是为了演示以后跟业务整合可把业务对象值传过来,或者利用category方法接收的参数
return "category ok " +result;
}
}
5.null不能正确接收到参数

6.显性声明以json字符串传递对象 加注解@RequestBody application/json方式 声明和接收处都加 推荐
@RequestBody:底层传递对象时 会把对象组织成jiso字符串形式的请求
!!! ......我这里就不写了 加上@RequestBody即可

7.以form表单方式传递对象 加注解@RequestPart 暂时有问题先不使用
@RequestPart:底层传递对象时 会把对象组织成form请求体形式的请求

数组和集合

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1. 定义一个接收数组类型参数的接口
//定义一个接收数组类型参数的接口
@GetMapping("/test3")
public String test3(String[] ids){ //可不加默认就是?方式取数据
for (String id : ids) {
log.info("id:{}",id);
}
return "test3 ok ,当前服务的端口为:"+port;
}
2. 声明调用商品服务中test3接口 传递一个数组类型
//声明调用商品服务中test3接口 传递一个数组类型 底层只能以queryString方式即/test3?ids=23ids=22方式传递
@GetMapping("/test3")
String test3(@RequestParam("ids") String[] ids);
3. 回到CategoryController调用商品服务中的test3接口传递参数
String result = productClient.test3(new String[]{"20","21","22"});
4. 可手动转为list集合 List<String> Strings = Arrays.asList(ids);

集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
0. 定义用来接收集合类型参数的对象 vos/CollectionVo
package com.chabai.vos;
import java.util.List;

//定义用来接收集合类型参数的对象
public class CollectionVo {

private List<String> ids; //接收的集合声明到这里//springmvc写对象的属性名就可以自动创建对象并为属性赋值

public List<String> getIds() {
return ids;
}
public void setIds(List<String> ids) {
this.ids = ids;
}
}
1. 定义一个接收集合类型参数的接口 注:
//定义一个接收集合类型参数的接口 //springmvc不能直接接收集合类型参数,如果想要接收集合参数必须将集合放入对象中,使用对象的方式接收
//vo(value object):用来传递数据对象成为值对象---用来收参 dto(data transfer(传输) object) :数据传输对象---用来传数据 两个相对应的
//接收集合参数必须将集合放入对象中,使用对象的方式接收 我们可以放到entity的对象里去接收
//但是entity的对象是和库表映射的,为了不污染我们的实体 我们可以新建vos包放vo 业务数据复杂的时候我们把一次性的数据统一的放到一个对象里边即vo去接收数据
@GetMapping("/test4")
public String test4(CollectionVo collectionVo){
collectionVo.getIds().forEach(id-> log.info("id:{}",id));
return "test4 ok ,当前服务的端口为:"+port;
}
2. 声明调用商品服务中test4接口 传递一个集合类型
//声明调用商品服务中test4接口 传递一个list集合类型
//我们通过测试发现地址栏是test4?ids=21&ids=22&ids=23形式商品服务对应接口可以正确接收到数据
//故我们feign底层只要把请求组装成test4?ids=21&ids=22&ids=23形式就可成功传递数据 这个思想你要明白
//这里传递参数无非就是把我们通过http client手动通过任务栏传参数构建请求 转变为我们声明请求方式和参数给OpenFeign帮我们构建请求从而达成一样的效果
//和数组拼接方式一样
@GetMapping("/test4")
String test4(@RequestParam("ids") String[] ids);
3. 回到CategoryController调用商品服务中的test4接口传递参数
String result = productClient.test4(new String[]{"20","21","22"});

服务间通信之响应处理

使用openfeign调用服务并返回对象

使用openfeign调用服务并返回对象 类别调用 商品服务 根据类别服务传递的id 把查询到的商品对象返回给类别服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 定义一个接口,接收id类型参数,返回一个基于id查询的对象
//定义一个接口,接收id类型参数,返回一个基于id查询的对象
@GetMapping("/product/{id}")
public Product product(@PathVariable Integer id){
log.info("id:{}",id);
return new Product(id,"chabai",232.21,new Date());
}
2. 声明调用根据id查询商品信息的接口
//声明调用根据id查询商品信息的接口
@GetMapping("/product/{id}")
Product product(@PathVariable("id") Integer id);
3. 回到CategoryController调用商品服务中的product接口传递参数 调用服务并返回对象
@GetMapping("/category")
public Product category(){
log.info("category service ...");
Product product = productClient.product(22);//注 我们这里写死是为了演示以后跟业务整合可把业务对象值传过来,或者利用category方法接收的参数
log.info("product:{}",product);
return product;
}
使用openfeign调用服务并返回所有list集合

使用openfeign调用服务并返回所有list集合 类别调用 商品服务 根据类别服务传递的类的id 把查询到某个类别下的所有商品返回给类别服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
注:集合也是一个对像 多个对象组成了一个集合
1. 定义一个接口,接收id类型参数,返回一个基于id查询的集合
//定义一个接口,接收id类型参数,返回一个基于id查询的集合
@GetMapping("/products")
public List<Product> findByCategoryId(Integer categoryId){
log.info("类别:{}",categoryId);
//调用业务逻辑根据类别id查询商品列表 我们这里是模拟
List<Product> products = new ArrayList<>();
products.add(new Product(21,"c",22.22,new Date()));
products.add(new Product(22,"b",22.23,new Date()));
products.add(new Product(23,"b",22.24,new Date()));
return products;
}
2. 声明调用商品服务根据类别id查询一组商品信息
//声明调用商品服务根据类别id查询一组商品信息
@GetMapping("/products")
List<Product> findByCategoryId(@RequestParam("categoryId") Integer categoryId);
3. 回到CategoryController调用商品服务中的findByCategoryId接口传递参数 调用服务并返回list集合
@GetMapping("/category")
public List<Product> category(){
log.info("category service ...");
List<Product> products = productClient.findByCategoryId(21);//注 我们这里写死是为了演示以后跟业务整合可把业务对象值传过来,或者利用category方法接收的参数
products.forEach(product -> log.info("product:{}",product));
return products;
}
}
使用openfeign调用服务并返回所有Map集合

使用openfeign调用服务并返回所有Map集合 类别调用 商品服务 根据类别服务传递的类的id 把查询到某个类别下的所有商品返回给类别服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1. 定义一个接口,接收id类型参数,返回一个基于id查询的map集合
//定义一个接口,接收id类型参数,返回一个基于id查询的map集合
@GetMapping("/productsPage")
public Map<String, Object> findByCategoryIdAndPage(Integer page, Integer rows , Integer categoryId){
log.info("当前页:{} 每页显示的记录数:{} 当前的类别:{}",page,rows,categoryId);
//根据类别id分页查询符合当前页的集合数据 List<Product> 里边有所有符合的对象 select * from t_product where categoryId=xx limit (page -1)*rows,rows
//根据类别id查询当前类别下的总条数 Long select count(id) from t_product where categoryId=xx
Map<String, Object> map = new HashMap<>();
List<Product> products = new ArrayList<>();
products.add(new Product(21,"c",22.22,new Date()));
products.add(new Product(22,"b",22.23,new Date()));
products.add(new Product(23,"b",22.24,new Date()));
int total = 1000;
map.put("rows",products);
map.put("total",total);
return map; //返回的是一个map集合里边有两个key vale 一个<rows,products> 一个<total,1000>
}
2. 声明调用商品服务根据类别id分页查询商品信息 以及总条数
//声明调用商品服务根据类别id分页查询商品信息 以及总条数
@GetMapping("/productsPage")
Map<String, Object> findByCategoryIdAndPage(@RequestParam("page") Integer page,@RequestParam("rows") Integer rows , @RequestParam("categoryId") Integer categoryId);
3. 回到CategoryController调用商品服务中的findByCategoryIdAndPage接口传递参数 调用服务并返回map集合
@GetMapping("/category")
public Map<String,Object> category(){
log.info("category service ...");
Map<String, Object> objectMap = productClient.findByCategoryIdAndPage(1, 5, 1);//注 我们这里写死是为了演示以后跟业务整合可把业务对象值传过来,或者利用category方法接收的参数
//当我们再调别人接口的时候,发了一个请求调支付宝接口,人家返回过来的是一个json字符串,我们不能把这个字符串强转为我们的java对象
//即:List<Product> rows = (List<Product>)(objectMap.get("rows"))会报错 objectMap.get("rows")我们拿到的是一个json格式数组字符串
//虽然我们这里写了一个强转但是我们底层是当成一个object 我们不能把一个object强转为list
//解释:Java中不能将一个Object对象直接强转为List,因为它们之间没有继承或实现关系。要将Object对象转换为List,你需要明确知道这个Object对象实际上是一个List类型的实例,然后使用合适的转换方法,比如类型转换或者通过构造函数创建一个新的List。
//解决办法:自定义格式解析
return objectMap;
自定义格式解析json字符串

自定义格式解析json字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
4. 解决办法:自定义格式解析 既然这个接口虽然响应给我们的我们自己写的是个map但最后响应给我们的是个json那么我们就把它当做普通字符串来处理 
- 定义一个String去接收 自己去处理这个序列化 不用feign底层的序列化
//声明调用商品服务根据类别id分页查询商品信息 以及总条数
@GetMapping("/productsPage")
String findByCategoryIdAndPage(@RequestParam("page") Integer page,@RequestParam("rows") Integer rows , @RequestParam("categoryId") Integer categoryId);
- 引入序列化依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
- 自定义格式解析
@GetMapping("/category")
public String category(){
log.info("category service ...");
String result = productClient.findByCategoryIdAndPage(1, 5, 1);//注 我们这里写死是为了演示以后跟业务整合可把业务对象值传过来,或者利用category方法接收的参数
//当我们再调别人接口的时候,发了一个请求调支付宝接口,人家返回过来的是一个json字符串,我们不能把这个字符串强转为我们的java对象
//即:List<Product> rows = (List<Product>)(objectMap.get("rows"))会报错 objectMap.get("rows")我们拿到的是一个json格式数组字符串
//虽然我们这里写了一个强转但是我们底层是当成一个object 我们不能把一个object强转为list
//解释:Java中不能将一个Object对象直接强转为List,因为它们之间没有继承或实现关系。要将Object对象转换为List,你需要明确知道这个Object对象实际上是一个List类型的实例,然后使用合适的转换方法,比如类型转换或者通过构造函数创建一个新的List。
//解决办法:自定义格式解析
log.info("result:{}",result);
// 自定义json反序列化 当有复杂对象我们处理不了时 你给我响应的时一个json 我就定义一个String去接收 自己去处理这个序列化 不用feign底层的序列化
//feign底层只能处理简单的序列化 复杂的只能我们自己来
// 对象转json--》序列化 json字符串转对象---》反序列化
JSONObject jsonObject = JSONObject.parseObject(result);
Object total = jsonObject.get("total");
Object rows = jsonObject.get("rows");
log.info("total:{}",total);
log.info("rows:{}",rows);

//二次json反序列化
List<Product> products = jsonObject.parseArray(rows.toString(), Product.class);
products.forEach(product ->log.info("product:{}",product));
return result;

openfeign细节

openfeign默认超时处理

openfeign默认超时处理是什么

默认情况下,openFiegn在进行服务调用时,要求服务提供方处理业务逻辑时间必须在1S内返回,如果超过1S没有返回则OpenFeign会直接报错,不会等待服务执行,但是往往在处理复杂业务逻辑是可能会超过1S,因此需要修改OpenFeign的默认服务调用超时时间

模拟超时

  • 服务提供方加入线程等待阻塞
1
2
3
4
5
6
@GetMapping("/product")
public String product() throws InterruptedException {
log.info("进入商品服务...")
Thread.sleep(1000);
return "product ok,当前提供端口服务:"+port;
}
  • 前台页面显示 超时

Read timed out executing GET http://PRODUCT/product

修改OpenFeign默认超时时间

修改当前服务调用指定服务时间

1
2
3
#注意应该在调用者的配置文件修改,填的是服务名id
feign.client.config.PRODUCT.connectTimeout=5000 #配置指定服务连接超时
feign.client.config.PRODUCT.readTimeout=5000 #配置指定服务等待超时

修改当前服务调用所有服务时间

1
2
feign.client.config.default.connectTimeout=5000  		#配置所有服务连接超时
feign.client.config.default.readTimeout=5000 #配置所有服务等待超时
OpenFeign调用详细日志展示

openfeign伪httpclient客户端对象,用来帮助我们完成服务间通信 底层用http协议 完成服务间调用

日志:openfeign为了更好方便在开发过程中调试openfeign数据传递,和响应处理,openfeign在设计时添加了日志功能,默认openfeign日志功能需要手动开启的

往往在服务调用时我们需要详细展示feign的日志,默认feign在调用是并不是最详细日志输出,因此在调试程序时应该开启feign的详细日志展示。feign对日志的处理非常灵活可为每个feign客户端指定日志记录策略,每个客户端都会创建一个logger默认情况下logger的名称是feign的全限定名需要注意的是,feign日志的打印只会DEBUG级别做出响应。

展示日志

1
2
#展示openfeign日志
logging.level.com.chabai.feignclient=debug

我们可以为openfeign客户端配置各自的logger ,lever对象 ,告诉feign记录哪些日志logger,lever 有以下几种值

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

基于配置文件修改feign的日志级别可以针对单个服务

1
2
3
4
# 针对某个微服务的配置   
feign.client.config.PRODUCT.loggerLevel=full #开启指定服务日志展示 四个值根据需求选择
logging.level.root=info # 根日志展示级别
logging.level.com.chabai.feignclient=debug #指定feign调用客户端对象所在的包必须为debug级别

基于配置文件修改feign的日志级别也可以针对所有服务

1
2
3
4
# 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
feign.client.config.default.loggerLevel=full #全局开启服务日志展示 四个值根据需求选择
logging.level.root=info # 根日志展示级别
logging.level.com.baizhi.feignclients=debug #指定feign调用客户端对象所在包,必须是debug级别

image-20240410184821821

注:第一个框是请求(根据声明和参数底层给我们拼接成的对应调用请求),第二个框是响应情况

Java代码方式修改日志(了解即可)

也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象

1
2
3
4
5
6
public class DefaultFeignConfiguration  {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}

如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:

1
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class) 

如果是局部生效,则把它放到对应的@FeignClient这个注解中:

1
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class) 
Feign使用优化(暂放)

Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:

  • URLConnection:默认实现,不支持连接池

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。

依赖

1
2
3
4
5
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

配置

1
2
3
4
5
6
7
8
9
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数

总结,Feign的优化:

1.日志级别尽量用basic

2.使用HttpClient或OKHttp代替URLConnection

① 引入feign-httpClient依赖

② 配置文件开启httpClient功能,设置连接池参数

Hystrix组件

服务雪崩

现象:在一个时刻微服务系统中所有微服务均不可用的这种现象,称之为服务雪崩现象
引发:在服务之间进行服务的调用时由于某一个服务故障,导致级联服务故障的现象,称为雪崩效应,雪崩效应的提供方不可用,导致消费方不可用,逐渐放大的过程

注:springboot内嵌tomcat,里边帮我们处理请求的是一个个线程,一个操作系统创建线程的数量是有限的,一旦过大操作系统会崩溃,Tomcat默认线程数150个,超过150个再来的请求只能等待前边执行完才行

根本原因:调用链路中链路某一服务因为执行业务时间过程,或者是大规模出现异常导致自身服务不可用,并把这种不可用放大情况

图解雪崩效应:

如存在以下调用链路:
image-20240410184921208

Service A的流量波动很大,流量经常会突然性增加!那么在这种情况下,就算Service A能扛得住请求,Service B和Service C未必能扛得住这突发的请求。此时,如果Service C因为抗不住请求,变得不可用。那么Service B的请求也会阻塞,慢慢耗尽Service B的线程资源,Service B就会变得不可用。紧接着,Service A也会不可用。图示如下:

image-20240416174428691

如何解决微服务系统服务雪崩问题?

a. 服务熔断

服务熔断

对某一个服务调用链路服务的保护

定义:有点类似保险丝,熔断器本身是一种开关装置,当某个服务单元发生故障之后,通过断路器(hystrix)的故障监控,某个异常条件被触发,直接熔断整个服务。向调用方法返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,就保证了服务调用方的线程不会被长时间占用,避免故障在分布式系统中蔓延,乃至雪崩。如果目标服务情况好转则恢复调用。服务熔断是解决服务雪崩的重要手段

服务熔断图示:

image-20240416182129455

服务降级

站在整个系统架构的考虑 系统层面的优化

定义:服务压力剧增的时候根据当前的业务情况及流量对一些服务和页面有策略的降级,以此缓解服务器的压力,以保证核心任务的进行。同时保证部分甚至大部分任务客户能得到正确的响应。也就是当前的请求处理不了了或者出错了,给一个默认的返回

通俗定义: 当网站|服务流量突然增加时,有策略关闭微服务系统中某些边缘服务 保证系统核心服务正常运行

服务降级图示:

image-20240410184935713

降级和熔断总结

共同点

  • 目的很一致,都是从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段;
  • 最终表现类似,对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用;
  • 粒度一般都是服务级别,当然,业界也有不少更细粒度的做法,比如做到数据持久层(允许查询,不允许增删改);
  • 自治性要求很高,熔断模式一般都是服务基于策略的自动触发,降级虽说可人工干预,但在微服务架构下,完全靠人显然不可能,开关预置、配置中心都是必要手段;sentinel(alibaba)

异同点

  • 触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;
  • 管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般需要对业务有层级之分(比如降级一般是从最外围服务边缘服务开始)

总结

熔断必会触发降级,所以熔断也是降级一种,区别在于熔断是对调用链路的保护,而降级是对系统过载的一种保护处理

组件定义

服务熔断器 监控器 防雪崩利器 属于Netflix已处于维护模式 了解思想即可 以后用sentinel(alibaba)

定义:在分布式环境中,许多服务依赖项不可避免地会失败,Hystrix是一个库,它通过添加延迟容忍和容错逻辑来帮助你控制这些分布式服务的交互。Hystrix通过隔离服务之间地访问点,停止它们之间的级联故障以及提供后备选项来实现这一点,所有这些都可以提高系统的整体弹性。

通俗定义: Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统中,许多依赖不可避免的会调用失败,超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障(服务雪崩现象),提高分布式系统的弹性。

作用:hystrix 用来保护微服务系统 实现 服务降级 服务熔断(防雪崩利器)

服务熔断的实现

image-20240416234155609

断路器打开关闭的条件

如果触发一定条件断路器会自动打开,过了一点时间正常之后又会关闭

断路器打开关闭的条件: (两个阈值)

1、 当满足一定的阀值的时候(默认10秒内超过20个请求次数)

2、 当失败率达到一定的时候(默认10秒内超过50%的请求失败)

注意:

1、 到达以上阀值,断路器将会开启

2、 一旦断路器开启之后,所有请求都不会进行转发,只有在断路关闭之后才可用

3、 一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。重复4和5

断路器流程:

image-20240410185304064

整个流程:当hystrix监控到对该服务接口调用触发的两种阈值时,会在系统中自动触发熔断器,在熔断器打开期间内,任何到该接口请求均不可用,同时在断路器打开5秒后断路器会处于半开状态,此时断路器允许放行一个请求到该服务接口,如果该请求执行成功,断路器彻底关闭,如果该请求执行失败,断路器重新打开

服务熔断备选处理

1
# 被调用服务没有down掉 只是商品(我们暂且模拟调用)服务调用库存服务失败率达到了我给你熔断了,过段时间还能正常处理

新建一个springboot并注册到consul server

1
2
3
4
5
6
7
8
9
# 注:熟练之后需要依赖可一下引完
1.使用maven新建一个空项目 springcloud_hystrix
2.引入springboot依赖
3.提供springboot配置文件
4.写入口类测试springboot是否可以启动
5.引入consul客户端依赖 作用:能把当前服务注册到指定的consul服务注册中心 引入健康检查依赖
6.再次写配置文件
7.在入口类加客户端注解@EnableDiscoveryClient(除eureka client) 让当前微服务作为一个服务客户端 进行微服务注册
8.启动服务并在consul界面查看服务信息

给调用服务方提供一个服务接口 DemoController 并测试接口是否正常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.chabai.controller;

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

@RestController
public class DemoController {

private static final Logger log = LoggerFactory.getLogger(DemoController.class);

@GetMapping("/demo")
public String demo(){
log.info("demo ok ...");
return "demo ok";
}
}

模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

private static final Logger log = LoggerFactory.getLogger(DemoController.class);

@GetMapping("/demo") //queryString方式传一个参数 即?id=
public String demo(Integer id){
log.info("demo ok ...");
if (id<=0){
throw new RuntimeException("无效id");
}
return "demo ok,id:"+id;
}
}

结果 访问http://localhost:8990/demo?id=1正常 http://localhost:8990/demo?id=-1报错 没有熔断 会服务雪崩

image-20240416230555321

此时再传一个正常参数可立刻返回请求结果 注意:有断路器就不行了

image-20240416230541295

所有微服务项目中引入hystrix依赖

这个组件只能监控当前这个服务自身的状态,每个微服务都要引入 一旦引入这个组件就具有服务熔断功能

1
2
3
4
5
<!--引入hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

开启断路器 在入口类加入注解@EnableCircuitBreaker

1
2
3
4
5
6
7
8
9
@SpringBootApplication  //代表springboot应用
@EnableDiscoveryClient //代表服务注册中心客户端 consul client
@EnableCircuitBreaker //开启hystrix服务熔断
public class HystrixApplication {
public static void main(String[] args) {

SpringApplication.run(HystrixApplication.class,args);
}
}

方法一:使用HystrixCommand注解 为每一个调用接口提供自定义备选处理 熔断之后快速响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.chabai.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

private static final Logger log = LoggerFactory.getLogger(DemoController.class);

//失败之后快速响应
@HystrixCommand(fallbackMethod = "demoFallBack")//失败之后处理 fallbackMethod 书写失败处理方法名
@GetMapping("/demo") //queryString方式传一个参数 即?id=
public String demo(Integer id){
log.info("demo ok ...");
if (id<=0){
throw new RuntimeException("无效id");
}
return "demo ok,id:"+id;
}
//自己备选处理
//注意:fallbackMethod方法返回类型和参数列表必须要和熔断的方法一样 方法名任意
public String demoFallBack(Integer id){
return "当前活动过于火爆,服务已经被熔断";
}
}

结果 多次错误请求 导致熔断器打开 正常请求也无法得到响应

image-20240416232649805

image-20240416232759158

过一段时间熔断器关闭正确请求可以得到响应

image-20240416233159718

方法二:使用Hystrix提供默认备选处理 提供统一备选处理 不需要每次单独写一个备选处理了

除了个别重要核心业务有专属,其它普通的可以通过@DefaultProperties(defaultFallback = “”)统一跳转到统一处理结果页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.chabai.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Demo2Controller {

private static final Logger log = LoggerFactory.getLogger(Demo2Controller.class);

//失败之后快速响应
@HystrixCommand(defaultFallback = "defaultFallBack")//失败之后 fallbackMethod 书写快速失败方法名
@GetMapping("/demo2") //queryString方式传一个参数 即?id=
public String demo(Integer id){
log.info("demo ok ...");
if (id<=0){
throw new RuntimeException("无效id");
}
return "demo ok,id:"+id;
}

//默认的备选处理,返回类型必须是String类型,不能有参数列表,同时存在优先自定义
public String defaultFallBack(){
return "网络连接失败,请重试";
}
}

服务降级的实现

服务降级: 站在系统整体负荷角度 实现: 关闭系统中某些边缘服务 保证系统核心服务运行
Emps 核心服务 Depts 边缘服务

feign服务降级备选处理

1
# 用户调商品调用库存服务 库存服务被作为边缘服务down掉了 openfeign会拿到无法访问此网站异常返回给商品服务 当用户调用商品时也会收到这样的异常 为了让用户有更好体验 我们在openfeign这边也做熔断策略即一次快速处理 Hystrix也可以帮我们实现

新建一个springboot并注册到consul server

1
2
3
4
5
6
# 我这里就快速构建了不再按部就班了
1.使用maven新建一个空项目 springcloud_hystrix_openfeign
2.引入springboot依赖 引入consul客户端依赖 引入健康检查依赖
3.提供springboot配置文件 编写项目端口号,名称,以及服务注册地址
4.写入口类并添加注解@SpringBootApplication和@EnableDiscoveryClient
5.启动服务看springboot是否可以启动 并在consul界面查看服务是否成功注册

给服务方提供一个服务接口 DemoController 并测试接口是否正常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestDemoController {

private static final Logger log = LoggerFactory.getLogger(TestDemoController.class);

@GetMapping("/test")
public String test(){
log.info("test ok ...");
return "test ok";
}
}

通过openfeign调用服务springcloud_hystrix的DemoController接口的demo方法并传一个int类型id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 引入Open Feign依赖 import change
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
# 在入口类加注解@EnableFeignClients开启feign调用
# 定义一个接口feignclient接收我们给openfeign的调用声明
# 加注解@FeignClient("HYSTRIX"),并在接口里写调用方法声明
- 写声明时别忘了使用对应注解@RequestParam("id")告诉底层怎么传递参数
- 加入注解后我们的接口已被工厂托管
# 回到服务消费方 注入openfeign客户端 并写调用代码完成调用
package com.chabai.controller;
import com.chabai.feignclients.HystrixClient;
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;

@RestController
public class TestDemoController {

private static final Logger log = LoggerFactory.getLogger(TestDemoController.class);

//注入openfeign的客户端对象
@Autowired
private HystrixClient hystrixClient;

@GetMapping("/test")
public String test(){
log.info("test ok ...");
String result = hystrixClient.demo(21);
log.info("demo result:{}",result);
return "test ok";
}
}
# 访问http://localhost:8999/test 测试是否可调用成功

被调用服务被作为边缘服务down掉了,openfeign调用服务时会接收到异常 需做服务降级备选处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 引入hystrix依赖
- 注意openfeign底层已经依赖了hystrix,所有引入了openfeign的依赖就不用在显示引入hystrix的依赖了
# 配置文件开启feign支持hystrix
- feign.hystrix.enabled=true #开启openfeign支持降级 默认是关闭的必须写
# 在feign客户端调用加入fallback指定降级处理
//fallback 这个属性用来指定当前服务不可用时,默认的备选处理 我们必须写个类实现这个接口 这个接口每一个实现的方法就是对应接口每一个方法的备选处理
@FeignClient(value = "HYSTRIX",fallback = HystrixClientFallBack.class)
public interface HystrixClient {

@GetMapping("/test")
public String test(Integer id);
}
# 开发fallback处理类并在类中开发降级处理方法
//HystrixClient的默认备选处理类
@Component //注意要给工厂托管
public class HystrixClientFallBack implements HystrixClient{

//openfeign服务降级备选处理方法
@Override
public String demo(Integer id) {
return "当前服务不可用,请稍后再试";
}
}
# down掉被调用服务访问http://localhost:8999/test
结果:test ok当前服务不可用,请稍后再试

Hystrix Dashboard(暂放)

仪表盘:用来显示状态信息 可有可无 锦上添花 帮助我们看看项目中断路器的状态

定义:Hystrix Dashboard的一个主要优点是它收集了关于每个HystrixCommand的一组度量。Hystrix仪表板以高效的方式显示每个断路器的运行状况。

作用:监控每一个@HystruxCommand注解创建一组度量,构建一组信息,然后通过图形化方式展示当前方法@HystruxCommand的状态信息

项目中引入依赖

1
2
3
4
5
<!--引入hystrix dashboard 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

入口类中开启hystrix dashboard

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrixDashboard //开启当前应用为仪表盘应用
public class HystrixDashBoardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashBoardApplication.class,args);
}
}

启动hystrix dashboard应用

1
http://localhost:7006(dashboard端口)/hystrix

端口号为你创建的springboot应用的端口号

监控的项目中入口类中加入监控路径配置[新版本坑],并启动监控项目

1
2
3
4
5
6
7
8
9
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}

image-20240410185238959
image-20240410185324393

解决方案

1
2
3
4
5
6
7
8
9
- 新版本中springcloud将jquery版本升级为3.4.1,定位到monitor.ftlh文件中,js的写法如下:
$(window).load(function()

- jquery 3.4.1已经废弃上面写法

- 修改方案 修改monitor.ftlh为如下调用方式:
$(window).on("load",function()

- 编译jar源文件,重新打包引入后,界面正常响应。

在maven仓库中找到这个jar包的位置

1
D:\Maven\apache-maven-3.8.1\maven-repo\org\springframework\cloud\spring-cloud-netflix-hystrix-dashboard\2.2.3.RELEASE

可能你的就是maven仓库的位置前面不一样,按照自己的路径一直往下找
在templates目录下的monitor.ftlh
image-20240410185351891
修改111行和149行的代码
image-20240410185411285
image-20240410185434664
注意前面那段配置应该放在要监视的微服务项目里面,可以创建一个配置文件,我这里就放在启动类里了

image-20240410185449804
image-20240410185502893
这次就没有报错
image-20240410185520426
image-20240410185534354

停止维护

1
2
3
4
5
6
7
8
9
10
11
12
# Hystrix
- 官网:https://github.com/Netflix/Hystrix
- Hystrix is no longer in active development, and is currently in maintenance mode.
# 日后如何解决服务雪崩
- 1. Hystrix (at version 1.5.18) is stable enough to meet the needs of Netflix for our existing applications.
- 2. resilience4j: Meanwhile, our focus has shifted towards more adaptive implementations that react to an application’s real time performance rather than pre-configured settings (for example, through adaptive concurrency limits). For the cases where something like Hystrix makes sense, we intend to continue using Hystrix for existing applications, and to leverage open and active projects like resilience4j for new internal projects. We are beginning to recommend others do the same.(resilience4j 属于spring cloud)
- 3. sentinel: 流量卫兵 属于spring cloud alibab 流量控制 降级策略 推荐
# Dashboard
- The hystrix-dashboard component of this project has been deprecated and moved to Netflix-Skunkworks/hystrix-dashboard. Please see the README there for more details including important security considerations.
# Dashboard替代产品
- Netflix-Skunkworks/hystrix-dashboard
- sentinel dashboard 推荐

Gateway组件

网关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 什么是网关
- 网关统一服务入口,可方便实现对平台众多服务接口进行管控,对访问服务的身份认证、防报文重放与防数据篡改、功能调用的业务鉴权、响应数据的脱敏、流量与并发控制,甚至基于API调用的计量或者计费等等。

# 网关作用
- 网关 = 路由转发 + 过滤器
- 网关统一所有微服务入口
- 路由转发:接收一切外界请求,转发到后端的微服务上去,以及请求过程负载均衡
- 在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成

# 网关功能详解
- 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。

- 路由和负载均衡:一切请求都必须先经过网关,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡

- 限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大

- ......

# 为什么需要网关
- 1.网关可以实现服务的统一管理
- 2.网关可以解决微服务中通用代码的冗余问题(如权限控制,流量监控,限流等)

# 网关组件在微服务中架构

image-20240410185549841

zuul

属于netflix

1
2
3
4
5
6
# 定义
zuul是从设备和网站到Netflix流媒体应用程序后端的所有请求的前门。作为一个边缘服务应用程序,zuul被构建为支持动态路由、监视、弹性和安全性
# 版本说明
目前zuul组件已经从1.0更新到2.0,但是作为springcloud官方不再推荐使用zuul2.0,但是依然支持zuul2.0
# springcloud官方集成zuul文档 可自行学习
https://cloud.spring.io/spring-cloud-netflix/2.2.x/reference/html/#netflix-zuul-starter

gateway

属于spring 推荐

官网:https://spring.io/projects/spring-cloud-gateway

说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 和zuul区别
Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux(异步非阻塞IO模型),属于响应式编程的实现,具备更好的性能

# 官方说明
这个项目提供了一个在`springmvc之上`构建API网关的库。springcloudgateway旨在提供一种简单而有效的方法来路由到api,并为api提供横切关注点,比如:安全性、监控/度量和弹性

# 特性
- 基于springboot2.x 和 spring webFlux 和 Reactor 构建 响应式异步非阻塞IO模型
- 动态路由
- 请求过滤

# 注:gateway等价于路由转发(router)+请求过率(filter)

# gateway核⼼概念
- Route(路由):路由是构建⽹关的基本模块,它由ID,⽬标URI,⼀系列的断⾔和过滤器组成,如果断⾔为true则匹配该路由
- Predicate(断⾔、谓词):开发⼈员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断⾔相匹配则进⾏路由 后边章节会细讲
- Filter(过滤):指的是Spring框架中GatewayFilter的实例,使⽤过滤器,可以在请求被路由前或者之后对请求进⾏修改
# 我们学习的就是路由转发(router)+请求过率(filter)

gateway网关使用

路由转发

配置文件配置路由 推荐 完全展开方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 开发一个独立的springboot应用
- 新建一个maven空项目 springcloud_gateway
- 引入springboot和consul依赖
- 引入配置文件编写配置 建议用yml方式编写 防止混乱
- 构建启动类测试springboot是否可以正常启动以及服务是否可以成功注册
# 引入网关依赖
<!--引入gateway网关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
# 我们先看路由转发 启动类别和商品服务 通过网关访问这两个服务
- 在配置文件编写规则配置
server:
port: 7979
spring:
application:
name: GATEWAY
cloud:
consul:
host: localhost
port: 8500
gateway:
routes:
- id: category_router #路由商品和对象的唯一标识 路由id,自定义,只要唯一即可
uri: http://localhost:8081 #用来书写类别服务地址
predicates: #断言 用来配置路由规则 也就是判断请求是否符合路由规则的条件
- Path=/category/** #指定路由规则 这个是按照路径匹配,只要以/category/开头就符合要求 即转到http://localhost:8081/category/**
- id: product_router
uri: http://localhost:8083
predicates:
- Path=/product #更精确只要/product 即只要地址栏访问product 就转到http://localhost:8083/product
- 启动会直接报错,这是因为网关是用最新的WebFlux编程模型开发的,也是一种web模型只是在springmvc的模型之上又做了优化,两种web模型不兼容
- 解决办法:在依赖种去代表springmvc的spring-boot-starter-web !!!
- 重新import change再次启动
访问:http://localhost:7979/category 和 http://localhost:7979/product都会正确转发

java代码方式配置路由 新建配置类 放com/chabai/config包下 优于配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GatewayConfig {

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
//类别路由配置
.route("category_router", r -> r.path("/category/**") //指明路径断言
.uri("http://localhost:8081"))
//商品路由配置
.route("product_router", r -> r.path("/product/**")
.uri("http://localhost:8083"))
.build();
}
}
网关的路由解析规则以及查看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 访问网关的路径
http://localhost:7979/category
# 现有网关的配置
server:
port: 7979
spring:
application:
name: GATEWAY
cloud:
consul:
host: localhost
port: 8500
gateway:
routes:
- id: category_router #路由商品和对象的唯一标识 路由id,自定义,只要唯一即可
uri: http://localhost:8081 #用来书写类别服务地址
predicates: #断言 用来配置路由规则 也就是判断请求是否符合路由规则的条件
- Path=/category/** #指定路由规则 这个是按照路径匹配,只要以/category/开头就符合要求 即转到http://localhost:8081/category/**
- id: product_router
uri: http://localhost:8083
predicates:
- Path=/product #更精确只要/product 即只要地址栏访问product 就转到http://localhost:8083/product
# 流程
访问http://localhost:7979/category 会去网关7979的路由列表去匹配一个对应的 然后保留路径/category 把匹配到的uri拼接到前边对后端服务进行访问
# 访问多个路径 配置文件支持多路径逗号隔开匹配的
- id: product_router
uri: http://localhost:8083
predicates:
- Path=/product,/product1
# 问题:每次这样写很麻烦
# 解决办法:通配符方式写 在ProductController添加总入口 @RequestMapping("/product")
- id: product_router
uri: http://localhost:8083
predicates:
- Path=/product/**
#更精确只要/product 即只要地址栏访问product 就转到http://localhost:8083/product
# 访问:http://localhost:7979/product/product1测试结果
# 通过网关提供的web路径查看路由详细规划 | 看配置文件也可(直接看即可) 这里讲解web路径查看
- gateway提供路由访问规则列表的web界面,但是默认是关闭的,如果想要查看服务路由规则可以在配置文件中开启
management:
endpoints:
web:
exposure:
include: "*" #开启所有web端点暴露
- 访问:http://localhost:7979/actuator/gateway/routes查看
网管在路由转发时如何实现请求的负载均衡

现有网关配置存在问题

现有路由配置方式在uri的属性中路径写死为服务的某个节点,这样没法实现请求的负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 7979
spring:
application:
name: GATEWAY
cloud:
consul:
host: localhost
port: 8500
gateway:
routes:
- id: category_router #路由商品和对象的唯一标识 路由id,自定义,只要唯一即可
uri: http://localhost:8081 #用来书写类别服务地址
predicates: #断言 用来配置路由规则 也就是判断请求是否符合路由规则的条件
- Path=/category/** #指定路由规则 这个是按照路径匹配,只要以/category/开头就符合要求 即转到http://localhost:8081/category/**
- id: product_router
uri: http://localhost:8083
predicates:
- Path=/product/** #更精确只要/product 即只要地址栏访问product 就转到http://localhost:8083/product

如何配置网关转发实现负载均衡 Ribbon组件 负载均衡客户端组件 gateway默认集成了ribbon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 7979
spring:
application:
name: GATEWAY
cloud:
consul:
host: localhost
port: 8500
gateway:
routes:
- id: category_router #路由商品和对象的唯一标识 路由id,自定义,只要唯一即可
uri: http://localhost:8081 #用来书写类别服务地址
predicates: #断言 用来配置路由规则 也就是判断请求是否符合路由规则的条件
- Path=/category/** #指定路由规则 这个是按照路径匹配,只要以/category/开头就符合要求 即转到http://localhost:8081/category/**
- id: product_router
#uri: http://localhost:8083
# lb是负载均衡,根据服务名拉取服务列表,实现负载均衡
uri: lb://PRODUCT #lb代表转发后台服务使用负载均衡,PRODUCT 代表服务注册中心上的服务名
predicates:
- Path=/product/** #更精确只要/product 即只要地址栏访问product 就转到http://localhost:8083/product

路由配置小总结

路由配置包括:3和4后边会学到

  1. 路由id:路由的唯一标示
  2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
  3. 路由断言(predicates):判断路由的规则
  4. 路由过滤器(filters):对请求或响应做处理

访问http://localhost:7979/product/product 发现已实现了请求的负载均衡

1
2
第一次:product ok,当前提供服务的端口:8082
第二次:product ok,当前提供服务的端口:8083
常用路由predicate(断言,验证)
1
2
3
# 断言 predicate  当请求到达网关时,网关前置处理,当请求满足断言的所有条件后,会向后端服务转发,在向后端服务转发之前会经过过滤器 ,不满足断言立即返回  自上而下匹配到第一个就停止

# springcloud给我们提供了大量的断言,过滤工厂,功能已经写好了,我们按需选择用就行

网关断言使用 Route Predicate Factories

官网:https://docs.spring.io/spring-cloud-gateway/docs/3.0.8/reference/html/#gateway-request-predicates-factories

名称 说明 示例
After Route 某个时间点后的请求才能生效 - After=2021-08-18T13:30:33.993+08:00[Asia/Shanghai]
Before Route 某个时间点之前的请求才能生效 - Before=2021-08-18T13:30:33.993+08:00[Asia/Shanghai]
Between Route 某两个时间点之间的请求才能生效 - Between=2021-08-18T13:30:33.993+08:00[Asia/Shanghai], 2021-08-1T13:30:33.993+08:00[Asia/Shanghai]
Cookie Route 请求必须包含某些cookie才能生效 - Cookie=name,chabai
Header Route 请求必须包含某些header才能生效 - Header=X-Request-Id, \d+
Host Route 请求必须是访问某个host(域名)才能生效 - Host=.somehost.org,.anotherhost.org
Method Route 请求方式必须是指定方式才能生效 - Method=GET,POST
Path Route 请求路径必须符合指定规则才能生效 - Path=/red/{segment},/blue/**
Query Route 请求参数必须包含指定参数才能生效 - Query=name, Jack或者- Query=name
RemoteAddr Route 请求者的ip必须是指定范围才能生效 - RemoteAddr=192.168.1.1/24
Weight Route 权重处理才能生效
…… …… ……

predicate和filter再配置文件如何使用

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates: # 断言
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=name,dyk

我们这里演示几个常用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
指定日期之后的请求进行路由
After=2020-07-21T11:33:33.993+08:00[Asia/Shanghai]

指定日期之前的请求进行路由,过了时间失效
Before=2020-07-21T11:33:33.993+08:00[Asia/Shanghai]

指定时间段内有效
Between=2020-07-21T11:33:33.993+08:00[Asia/Shanghai],2021-07-21T11:33:33.993+08:00[Asia/Shanghai]

指定cookie的请求进行路由

curl http://localhost:8888/product --cookie "name=chabai" #curl工具访问添加cookie win10默认安装

- Cookie=name,[A-Za-z0-9]+ # [A-Za-z0-9]+ 这是正则表达式
curl工具访问如下图:

image-20240410185741078

1
2
3
基于请求头中的指定属性的正则匹配路由(这里全是整数)   #可用postman工具添加请求头
- Header=X-Request-Id, \d+
如下图:

image-20240410185759374

1
2
基于指定的请求方式请求进行路由
- Method=GET,POST
网关的过滤 Filter
1
2
3
# 当请求满足断言的所有条件后,会像后端服务转发,再向后端服务转发之前会经过过滤器

# GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理

image-20240410185811666

网关过滤使用 GateWayFilter Factories

官网:https://docs.spring.io/spring-cloud-gateway/docs/3.0.8/reference/html/#gatewayfilter-factories

名称 说明
AddRequestHeader Filter 给当前请求添加一个请求头
RemoveRequestHeader Filter 移除请求中的一个请求头
AddResponseHeader Filter 给响应结果中添加一个响应头
RemoveResponseHeader Filter 从响应结果中移除有一个响应头
RequestRateLimiter Filter 限制请求的流量
…… ……

常用filter使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用来给路由对象所有转发的请求加入指定请求头信息
- AddRequestHeader=X-Request-red, blue

用来给路由对象所有转发的请求加入指定请求参数
- AddRequestParameter=red, blue

用来给路由对象所有转发的请求加入指定响应头信息
- AddResponseHeader=X-Response-Red, AAA
-
用来给路由对象所有转发的请求加入指定请求的url加上指定前缀的信息
- PrefixPath=/mypath
如浏览器访问网关地址 /list 前缀地址/mypath 转发到服务器地址为: uri+前缀地址+地址栏路径 /mypath/list

用来给对象路由的转发请求的url去掉指定n个前缀
- StripPrefix=2
如浏览器访问网关地址 /product/list StripPrefix=1 ===>/list

使配置的filter对所有的路由都生效

如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=name,dyk

自定义全局网关filter 当内置filter不能满足我们时 可自定义 所有请求都要经过全局filter之后再转发到后端服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//放在com/chabai/filters下 
@Configuration

public class CustomGlobalFilter implements GlobalFilter, Ordered {

private static final Logger log = LoggerFactory.getLogger(CustomGlobalFilter.class);

//类似javaweb doFilter
//exchange ;交换 request response 封装了 request response
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("进入自定义的filter");
Mono<Void> filter = chain.filter(exchange); //放行filter继续向后执行微服务
log.info("响应回来filter处理"); //执行完微服务回来
return filter;
}
//order:排序
@Override
// 用来指定filter执行顺序 默认顺序按照自然排序进行 -1在所有filter执行之前执行
public int getOrder() {
return 0;
}
}

过滤器执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter

请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:
image-20240410185836727
排序的规则是什么呢?

  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
  • GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
  • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
  • 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。
网关跨域问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
cloud:
gateway:
# 。。。
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期

Config组件

config(配置)又称为 统一配置中心顾名思义,就是将配置统一管理,配置统一管理的好处是在日后大规模集群部署服务应用时相同的服务配置一致,日后再修改配置只需要统一修改全部同步,不需要一个一个服务手动维护。

image-20240410185853827
SpringCloud Config分为服务端和客户端两部分。

服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。

客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。

  • 集中管理配置文件
  • 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
  • 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
  • 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
  • 将配置信息以REST接口的形式暴露 - post/crul访问刷新即可…

Config Server 开发

引入依赖

1
2
3
4
5
<!--引入统一配置中心-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>

开启统一配置中心服务

1
2
3
4
5
6
7
@SpringBootApplication
@EnableConfigServer
public class Configserver7878Application {
public static void main(String[] args) {
SpringApplication.run(Configserver7878Application.class, args);
}
}

修改配置文件

1
2
3
4
server.port=7878
spring.application.name=configserver
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500

直接启动服务报错

没有指定远程仓库的相关配置
image-20240410190025743

创建远程仓库

github创建一个仓库,在统一配置中心服务中修改配置文件指向远程仓库地址

1
2
3
4
5
6
#指定仓库的url
spring.cloud.config.server.git.uri=https://github.com/chenyn-java/configservers.git
# 指定访问的分支
spring.cloud.config.server.git.default-label=master
#spring.cloud.config.server.git.username= 私有仓库访问用户名
#spring.cloud.config.server.git.password= 私有仓库访问密码

拉取远端配置规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- label/name-profiles.yml|properties|json
`label 代表去那个分支获取 默认使用master分支
`name 代表读取那个具体的配置文件文件名称
`profile 代表读取配置文件环境
1234
server:
port: 7878

spring:
application:
name: configserver #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://github.com/chenyn-java/configservers.git #GitHub上面的git仓库名字
####搜索目录仓库名
search-paths:
- configservers
####读取分支
label: master

#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka

Config Client 开发

项目中引入config client依赖

1
2
3
4
5
<!--引入config client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

编写配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 #开启统一配置中心服务
spring.cloud.config.discovery.enabled=true
#指定统一配置服务中心的服务唯一标识
spring.cloud.config.discovery.service-id=configserver
#指定从仓库的那个分支拉取配置
spring.cloud.config.label=master
#指定拉取配置文件的名称
spring.cloud.config.name=client
#指定拉取配置文件的环境
spring.cloud.config.profile=dev
12345678910
server:
port: 3355

spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: client #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上client-dev.yml的配置文件被读取
uri: http://localhost:7878 #配置中心地址


#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka

远程仓库配置文件

1
2
3
4
5
6
7
8
9
10
11
远程仓库创建配置文件
- client.properties [用来存放公共配置][]
spring.application.name=configclient
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500

- client-dev.properties [用来存放研发相关配置][注意:这里端口为例,以后不同配置分别存放]
server.port=9099

- client-prod.properties [用来存放生产相关配置][]
server.port=9098

image-20240410185909799

直接启动过程中发现无法启动直接报错

报错原因

项目中目前使用的是application.pAAroperties启动项目,使用这个配置文件在springboot项目启动过程中不会等待远程配置拉取,直接根据配置文件中内容启动,因此当需要注册中心,服务端口等信息时,远程配置还没有拉取到,所以直接报错

解决方案

应该在项目启动时先等待拉取远程配置,拉取远程配置成功之后再根据远程配置信息启动即可,为了完成上述要求springboot官方提供了一种解决方案,就是在使用统一配置中心时应该将微服务的配置文件名修改为bootstrap.(properties|yml),bootstrap.properties作为配置启动项目时,会优先拉取远程配置,远程配置拉取成功之后根据远程配置启动当前应用。

  • applicaiton.yml是用户级的资源配置项
  • bootstrap.yml是系统级的,优先级更加高

名称必须为bootstrap.yml/properties

手动配置刷新

在生产环境中,微服务可能非常多,每次修改完远端配置之后,不可能对所有服务进行重新启动,这个时候需要让修改配置的服务能够刷新远端修改之后的配置,从而不要每次重启服务才能生效,进一步提高微服务系统的维护效率。在springcloud中也为我们提供了手动刷新配置和自动刷新配置两种策略,这里我们先使用手动配置文件刷新。

引入actuator监控依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在config client端加入刷新暴露端点
1
management.endpoints.web.exposure.include=*          #开启所有web端点暴露

修改YML,添加暴露监控端口配置:

1
2
3
4
5
6
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
在需要刷新代码的类中加入刷新配置的注解

@RefreshScope

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RefreshScope
@Slf4j
public class TestController {
@Value("${name}")
private String name;
@GetMapping("/test/test")
public String test(){
log.info("当前加载配置文件信息为:[{}]",name);
return name;
}
}

修改之后在访问

  • 发现并没有自动刷新配置?
  • 必须调用刷新配置接口才能刷新配置
需要手动发送post请求才会刷新配置
1
curl -X POST http://localhost:9099/actuator/refresh

在次访问发现配置已经成功刷新

springcloudalibaba

简介和组件介绍

image-20240418224444557

image-20240418224416322

环境搭建

思路整理

1
2
3
4
# 创建全局父项目维护版本
继承springboot父项目 2.2.5.RELEASE
维护springcloud依赖 Hoxton.SR6
维护springcloudalibaba依赖 2.1.2.RELEASE

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>springcloudalibaba_parent</artifactId>
<version>1.0-SNAPSHOT</version>

<!--继承springboot的父项目-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<!--定义springcloud使用版本号-->
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>

<!--定义springcloudaliba使用版本号-->
<spring.cloud.alibaba.version>2.1.2.RELEASE</spring.cloud.alibaba.version>

</properties>

<!--维护依赖-->
<dependencyManagement>
<dependencies>
<!--全局管理springcloud版本,并不会引入具体依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--维护sprincloudalibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

</dependencyManagement>

</project>

创建一个普通的maven项目,删除src 作为父模块管理维护依赖 同时检查项目结构里项目和模块sdk jdk 和maven上的java版本以及maven配置仓库

image-20240419112525488

nacos组件

nacos简介

1
2
3
4
5
# Nacos组件
命名: Name Service(服务注册中心) Configuration Service(统一配置中心)
Nacos名字由来 = Name + Configuration +Service
作用: 服务注册中心|统一配置中心
# 我们先来看作为服务注册中心怎么用

nacos安装和配置

从下载地址下载nacos 版本选择1.4.6 linux版

1
2
3
- 官网地址: https://nacos.io
- github仓库地址 : https://github.com/alibaba/nacos
- 下载地址 : https://github.com/alibaba/nacos/releases

环境准备

1
2
3
- 推荐使用linux/unix/mac 必备
- 64 bit JDK 1.8+ 必备
- maven 3.2.x+

上传jdk,nacos安装包到服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 安装jdk
rpm -ivh jdk-xxxx.rpm
# 配置环境变量 rpm安装会配一个临时环境变量 但是建议还是要配
- 找到jdk的bin目录:find / -name java

- 把它添加到环境变量
a. vim /etc/profile
添加两句话 export JAVA_HOME=/usr/lib/jvm/jdkXXXXXX export PATH=$PATH:$JAVA_HOME/bin
wq退出

b.source /etc/profile重新加载配置生效
# 安装nacos
- 1.解压缩nacos安装包 tar -zxvf nacos-server-1.4.6.tar.gz
- 2.解压缩之后有个nacos目录 查看nacos目录结构
drwxr-xr-x 2 root root 4096 Apr 19 12:31 bin 启动关闭nacos脚本目录
drwxr-xr-x 2 502 games 4096 May 25 2023 conf 配置nacos配置文件目录
drwxr-xr-x 2 root root 4096 Apr 19 12:31 target nacos-service 核心jar包
- 注:如果使用的使aliyun服务器记的在安全组规则开启端口 不然后边无法访问web页面
- 3.启动nacos服务 默认nacos以集群模式启动,必须满足多个节点 我们这里单机启动
单机启动:在bin目录执行 ./startup.sh -m standalone
启动后如何看日志 后台启动的不能直接看到日志 查看日志: tail -f ncos.log nacos目录下的logs不是bin下
- 4.访问web端口
http://47.115.221.37:8848/nacos 中间是自己服务器公网ips 账号密码默认nacos
- 到此为止服务注册中心已经搭建完毕 接下来看看如何向服务注册中注册服务 以及一些细节问题

image-20240419125456139

nacos client开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建一个独立springboot应用
1.新建一个maven空项目 canosclient
2.引入springboot依赖 imort changes
3.引入配置文件 配置服务名和端口号
4.编写启动类测试springboot应用是否可以正常去启动
# 引入nacos client依赖
<!--引入nacosclient依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
# 在配置文件提供服务注册中心地址 我们这里先写总地址
spring.cloud.nacos.server-addr=47.115.221.37:8848
# 在启动类加上注解@EnableDiscoveryClient 开启服务注册 这个注解可以省略不写
# 启动服务看是否注册成功 注:关闭服务立即移除没有自我保护机制

nacos细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Nacos 作为服务注册中心细节
- 安装并启动nacos server
- 默监听web端口:8848
- 访问web界面:http://47.115.221.37:8848/nacos
# 开发微服务进行注册
- a.创建独立springboot应用
- b.引入依赖 springcloud-starter-alibaba-nacos-discovery
- c.编写配置 配置服务注册中心地址 spring.cloud.nacos.server-addr=47.115.221.37:8848 我们这里先写总地址 作为nacos server注册地址是spring.cloud.nacos.discovery.server-addr=${spring.cloud.nacos.server-addr}
可如下配置:
#nacos 总地址
spring.cloud.nacos.server-addr=47.115.221.37:8848
#作为nacos client注册地址 默认值引用了上边spring.cloud.nacos.server-addr 可不写
#spring.cloud.nacos.discovery.server-addr=${spring.cloud.nacos.server-addr}
- d.指定向nacos server注册服务名称
可如下配置
spring.application.name=NACOSCLIETN
#指定向nacos server注册服务名称 默认引用了我们写的微服务名 可不写
#spring.cloud.nacos.discovery.service=${spring.application.name}
如果我们这里给定服务名称以这里为主 ,即
spring.cloud.nacos.discovery.service=aa #服务名就是aa不是NACOSCLIETN

服务间通信

1
2
3
4
5
6
7
# 服务间通信 两种方式
- a.Http Rest 推荐
- b.RPC
# 使用Rest通信方式实现服务间通信
- 1.sprig框架提供的RestTemplate 并且使用Ribbon实现负载均衡
- 2.OpenFeign组件 推荐
演示如下;

新建两个微服务并注册到nacos server 用于测试通信

1
2
3
4
5
6
# 新建两个独立springboot应用并注册到nacos server 
- 新建一个maven空项目 users products
- 引入springboot和nacos client依赖
- 引入配置文件 配置服务端口号 名称 服务注册中心地址
- 编写启动类并加注解@EnableDiscoveryClient测试springboot是否可以成功启动和查看微服务在注册中心是否成功注册
# 接下来分别用两种方式实现服务通信 如下 用户服务调用商品服务

两个服务都暴漏一个接口供通信使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 首先在商品服务构建(暴漏)一个接口 供用户服务调用 com/chabai/controller/ProductsController
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class ProductsController {

private static final Logger log = LoggerFactory.getLogger(ProductsController.class);

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

@GetMapping("/product")
public String product(Integer id){
log.info("id:{}",id);
return "商品服务: "+id+ ",当前提供服务的端口为:"+port;
}
}
# 用户服务构建一个接口,里边调用商品服务接口
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);

@GetMapping("/invoke")
public String invokeProduct(){
log.info("调用用户服务");
return "调用用户服务成功";
}
}
# 测试两个微服务是否可以成功启动 此时并没有调用服务 即通信 我们只是通过浏览器测试接口是否则正常
- 访问:http://localhost:8986/product?id=1
- 访问:http://localhost:8987/invoke

方式1:RestTemplate实现通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# RestTemplate方式
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);

@GetMapping("/invoke")
public String invokeProduct(){
log.info("调用用户服务");
String result = new RestTemplate().getForObject("http://localhost:8986/product?id=1", String.class);
return result;
}
}

# 测试是否调用成功
访问:http://localhost:8987/invoke
# 缺点:1.无法实现负载均衡 2,代码写死到路径中不利于后续维护

Ribbon组件实现服务负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# Ribbon组件实现负载均衡
- 引入ribbon依赖 nacos client已集成ribbon我们不用显示引入
- 使用ribbon完成负载均衡 三种方法 DiscoveryClient LoadBalanceClient @LoadBalance
# 1.DiscoveryClient对象 只要引入ribbon依赖 工厂中就有了DiscoveryClient对象 需要时注入即可 也没有负载均衡
package com.chabai.controller;

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

import java.util.List;

@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);

@Autowired
private DiscoveryClient discoveryClient;

@GetMapping("/invoke")
public String invokeProduct(){
log.info("调用用户服务");

//使用ribbon组件实现堵负载均衡 三种方法 DiscoveryClient LoadBalanceClient @LoadBalance

//DiscoveryClient
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("PRODUCTS");
for (ServiceInstance serviceInstance : serviceInstances) {
log.info("服务主机:{} 服务端口:{} 服务uri:{}",serviceInstance.getHost(),serviceInstance.getPort(),serviceInstance.getUri());
}
String result = new RestTemplate().getForObject(serviceInstances.get(0).getUri() + "/product?id=1", String.class);
log.info("商品服务调用结果:{}",result);
return result;
}
}
# 测试
访问:http://localhost:8987/invoke
# 2.LoadBalanceClient对象 可实现负载均衡 只要引入ribbon依赖 工厂中就有了DiscoveryClient对象
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import java.util.List;

@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);


@Autowired
private LoadBalancerClient loadBalancerClient;

@GetMapping("/invoke")
public String invokeProduct(){
log.info("调用用户服务");

//LoadBalancerClient
ServiceInstance serviceInstance = loadBalancerClient.choose("PRODUCTS");//已经进行负载均衡返回的节点 轮询策略
String result = new RestTemplate().getForObject(serviceInstance.getUri() + "/product?id=1", String.class);
log.info("商品服务调用结果:{}",result);
return result;
}
}
# 测试
访问:http://localhost:8987/invoke

# 3.@LoadBalance 可实现负载均衡
- 写一个config包 把RestTemplate交给工厂管理 日后不用再手动new了 直接注入即可 并添加注解实现负载均衡
package com.chabai.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class BeansConfig {

@Bean
@LoadBalanced//负载均衡的客户端
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- 写调用接口
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;

@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);

@Autowired
private RestTemplate restTemplate; //这个对象才是具有负载均衡的restTemplate

@GetMapping("/invoke")
public String invokeProduct(){
log.info("调用用户服务");


//@LoadBalance
String result = this.restTemplate.getForObject("http://PRODUCTS/product?id=1", String.class);
log.info("商品服务调用结果:{}",result);
return result;
}
}
# 测试
访问:http://localhost:8987/invoke

方式2:OpenFeign组件实现通信 推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 引入openfeign依赖
<!--引入openfeign依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
# 在入口类添加注解@EnableFeignClients开启feign调用支持
# 新建feignclients包并建立ProductClient接口声明调用接口信息 feignclients/ProductClient
# 写接口
- 写注解@FeignClient("PRODUCTS") 声明是feign客户端 并且调用的服务是PRODUCTS
- 声明调用方法
package com.chabai.feignclients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient("PRODUCTS")
public interface ProductClient {

@GetMapping("/product")
String product(@RequestParam("id") Integer id);
}
# 回到调用服务写调用
package com.chabai.controller;

import com.chabai.feignclients.ProductClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@RestController
public class UsersController {

private static final Logger log = LoggerFactory.getLogger(UsersController.class);

@Autowired
private ProductClient productClient;

@GetMapping("/invoke")
public String invokeProduct(){
log.info("调用用户服务");

String result = productClient.product(1);
log.info("商品服务调用结果:{}",result);
return result;
}
}
# 测试
访问:http://localhost:8987/invoke

统一配置中心

nacos作为统一配置中心使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# nacos作为服务注册中心怎么用我们已经学习过,现在我们只考虑作为配置中心
- 1.他管理配置文件方式是在自己所在服务器上形成一个版本库,因此不再需要创建远程版本库
- 2.nacos作为统一配置中心管理配置文件时同样也存在版本控制
# 使用nacos统一配置中心功能
- 1.启动nacos server
- 2.开发微服务作为统一配置中心客户端将配置交给nacos进行管理
# 新建一个独立springboot应用 并注册到nacos server 注:还是需要注册到nacos server的 虽然你要把配置放到配置中心管理 但是你作为一个微服务肯定要注册到服务注册中心了
a.新建一个maven空项目 configclient 我们这里只是为了方便学习 真正业务要见名知意
b.引入springboot和nacos client依赖 import changes
c.引入配置文件 配置服务端口号 名称 服务注册中心地址
d.编写启动类并加注解@EnableDiscoveryClient测试springboot是否可以成功启动和查看微服务在注册中心是否成功注册
# 测试在配置文件中自定义的属性 是否可以在接口注入正常运行 即配置文件没有交给nacos配置中心 自己管理
a.在配置文件写自定义属性
#自定义属性
name=chabai
b.新构建一个接口用于测试 接口中注入配置文件自定义的属性 controller/DemoController
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

@Value("${name}")
private String name;

private static final Logger log = LoggerFactory.getLogger(DemoController.class);

@GetMapping("/demo")
public String demo(){
log.info("demo ok...");
return "demo ok name:"+name;
}
}
c.访问:http://localhost:8981/demo测试是否能正常
# 将自身配置交给远端nacos config server进行管理 如下图: dataId写错的话要删除重写无法修改

image-20240419213824454

image-20240419214546963

image-20240419214613265

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 我们已把配置交给他管理了  我们自己服务就可以不要配置文件了 需要时去nacos config server拉取对应配置即可
- 1.自身项目引入nacos config client依赖 之前那个依赖是把自身当成服务客户端 现在是把自身当成一个配置客户端
**引入这个依赖代表日后我们这个服务一启动就会把自己当成configclient就会根据下边写的配置去nacos config server对应位置拉取对应配置文件 从而根据拉取的配置文件的配置启动微服务**
<!--引入nacos config client依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 2.我们还需要引入配置文件告诉服务去哪拉取配置 application.properties
#告诉nacos config server地址
spring.cloud.nacos.config.server-addr=47.115.221.37:8848
#告诉从哪个命名空间获取配置 默认public为空可不写 注意要写命名空间生成的id不是命名空间名字
#spring.cloud.nacos.config.namespace=命名空间id 不是默认public时才需要写
#告诉去哪个组拉取个配置文件
spring.cloud.nacos.config.group=DEFAULT_GROUP
#从这个组拉取哪个配置文件
spring.cloud.nacos.config.name=configclient-prod
#拉取这个配置文件的哪个后缀的配置文件
spring.cloud.nacos.config.file-extension=properties
- 3. 运行发现报错Could not resolve placeholder 'name' in value "${name}",
原因:服务在启动时,会认为服务中的application.properties就是我们所需要的配置了,并不会等待拉取配置文件完成之后再启动
解决方案:让当前项目在启动时预先拉取配置,再以拉取的配置启动==》修改项目的application.properties为bootstrap.properties
# 访问:http://localhost:8981/demo测试
# 远端代码修改了,直接不启动的情况下,影响到当前项目 即自动配置刷新
- 在接口加入注解@RefreshScope//允许远端配置修改
package com.chabai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

@RestController
@RefreshScope//允许远端配置修改
public class DemoController {

@Value("${name}")
private String name;

private static final Logger log = LoggerFactory.getLogger(DemoController.class);

@GetMapping("/demo")
public String demo(){
log.info("demo ok...");
return "demo ok name:"+name;

}
}
# 修改nacos config server里对应配置代码 不重新启动微服务 再次访问:http://localhost:8981/demo测试 发现已改变

nacos作为统一配置中心细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 统一配置中心nacos config server
# dataId细节:代表完整配置文件名称 = prefix(前缀) + 环境(env) +file-extension(后缀)|
# =spring.cloud.nacos.config.name + spring.cloud.nacos.config.file-extension
故dataId可如下:
dataId = spring.cloud.nacos.config.name + spring.cloud.nacos.config.file-extension
dataId = ${prefix}-${spring.profile.active}.${file-extension}
# 微服务客户端拉取配置方式 二选一
- a.第一种获取配置文件方式 dataId = name +file-extension
#从这个组拉取哪个配置文件
spring.cloud.nacos.config.name=configclient-prod
#拉取这个配置的哪个后缀的配置文件
spring.cloud.nacos.config.file-extension=properties
- b.第二种获取配置文件方式 dataId = prefix + env +file-extension
spring.cloud.nacos.config.prefix=configclient #默认值是服务名我们这里要自己指定
spring.profiles.active=prod
spring.cloud.nacos.config.file-extension=properties
1
2
3
4
5
6
7
8
9
# 统一配置中心 nacos 三个重要概念 我们只是举个例子 没有标准
- 命名空间 namespace: 默认nacos安装完成之后会有一个默认命名空间 名字为public
作用:站在项目角度隔离每一个项目配置文件
例:我们可以自定义自己的命名空间一个命名空间就代表一个全局应用 防止多个应用混乱

- 组 group: 默认nacos中管理配置文件时不显示执行group名称之默认的组名称为DEFAULT_GROUP
作用:站在项目中每一个服务角度,隔离同一个项目中不同服务的配置文件

- 文件名 dataId: 获取一个配置文件的唯一标识
1
2
# 导入导出
# nacos config支持版本回退的 在历史版本 默认为空 需要自己拿着dataId 和 group搜索

nacos的mysql数据持久化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 1.nacos持久化
- 持久化:管理的配置信息持久化 数据持久化到硬盘 再次启动不会丢失
- 注意:默认nacos存在配置信息持久化,默认的持久化方式为内嵌数据库 derby nacos的data就是默认数据库存储点
- 缺点:无法更友好的展示数据
- 官方建议:在生产情况下推荐将配置存入mysql数据库 注意:nacos到目前为止只支持mysql且版本要求5.6.5+
- 优点:使用默认数据库是nacos在自己在管理数据 我们把数据放到mysql 日后不会因为nacos老化丢失数据 且方便我们图形化操作数据
# 2.将nacos持久化到mysqlh中
- a.在linux系统安装mysql数据库服务 版本要求5.6.5+
- 一定要卸载原来老的mysql
检查是否安装了Mysql
- Yum检查
yum list installed | grep mysql
删除:yum remove xxx
- rpm检查
rpm -qa | grep -i mysql
删除:rpm -e --nodeps xxx
口令查找Mysql的安装目录和残存文件
whereis mysql
find / -name mysql
删除:rm -rf xx
查看mysql配置文件
以my.cnf为例,一般在/etc/my.cnf,直接rm即可。
如果设置了开机启动,也需要关闭。
chkconfig --list | grep -i mysql
chkconfig --del mysqld
- 先yum install mysql-community-server -y成功就不需要添加源否则请按如下
- 添加官方的yum源创建并编辑mysql-community.repo文件
vi /etc/yum.repos.d/mysql-community.repo
- 粘贴以下内容到源文件中
[mysql57-community]
name=MySQL 5.7 Community Server
baseurl=http://repo.mysql.com/yum/mysql-5.7-community/el/7/$basearch/
enabled=1
gpgcheck=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql
- 安装mysql
yum install mysql-community-server -y
- 启动数据库
systemctl start mysqld
- 修改mysql数据库密码 会生成临时密码要获取后修改
grep 'temporary password' /var/log/mysqld.log
[Note] A temporary password is generated for root@localhost: pAltzf:Y*0:,
mysqladmin -u root -p password 回车 输入原始密码 再输入新密码 Root!Q2w
- 登录mysql
mysql -uroot -p'密码'
- 修改远程连接
grant all privileges on *.* to 'root'@'%' identified by '密码' with grant option;
flush privileges; 刷新权限
- b.nacos持久化mysql配置
- 创建一个数据库 nacos_config utf-8
- 执行nacos-mysql.sql nacos解压后在conf即可找到 拉到windows桌面 在拉到可视化工具执行即可
- 修改nacos配置文件 vim application.properties

image-20240420143433438

1
- 关闭重新启动nacos

image-20240420140509310

1
- c.访问:http://47.115.221.37:8848/nacos发现已换为mysql数据库

nacos集群

集群部署架构图

image-20240420144006693

image-20240420144324727

集群实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 1.集群的规划
- nacos01 47.115.221.37:8845
- nacos02 47.115.221.37:8846
- nacos03 47.115.221.37:8847
- mysql 47.115.221.37:3306 我们这里暂用mysql单节点 不用主从复制
- nginx 47.115.221.37:8999 实现nacos负载均衡
# 2.搭建nacos集群的注意事项
- 数据持久化必须为mysql
- 要求在一个机器启动时虚拟机内存不小于3g
# 3.搭建nacos集群
- 保证要用来复制节点的nacos不能有脏数据(rm -rf nacos/data/就变成和刚解压一样了),并为连接mysql数据库
- 重新初始化mysql 若mysql为空就不用初始化直接用即可 我们这里是为了避免有脏数据 (可选)
----以上两步保证现在我们用来复制的nacos既没有berby数据也没有mysql数据污染----
- 复制nacos为nacos01(cp -r nacos nacos01)
- 配置nacos01
修改nacos01 conf目录中cluster.conf.example为cluster.conf
cluster.conf文件添加所有集群节点
47.115.221.37:8845
47.115.221.37:8846
47.115.221.37:8847
- 复制nacos01 为nacos02 nacos03 ====》三个节点集群搭建完成
cp -r nacos01 nacos02
cp -r nacos01 nacos03
- 修改各自端口号 vim application.properties 三个都改为规划的端口号
- 分别启动三个节点 默认就是以集群方式启动 若更改配置记得先停止再重新启动
./nacos01/bin/startup.sh
./nacos02/bin/startup.sh
./nacos03/bin/startup.sh
- 不用的话及时关闭节点释放内存
- 这里做个说明我本来想用我服务器跑集群的,结果跑第二个nacos的时候,服务器就死机了,我的服务器是一核两g的,可以把启动文件里的参数改小一点
原因:nacos的默认启动内存参数为-Xms2g -Xmx2g -Xmn1,可能根本用不到这么多。如果只是在测试环境或者微服务数量相对比较少时,采用Nacos默认的JVM配置会浪费很多资源。 若更改配置记得先停止再重新启动
解决办法:start.sh修改下内存分配
standalone:表示单机模式运行,非集群模式
-Xms: 设定程序启动时占用内存大小
-Xmx: 设定程序运行期间最大可占用的内存大小
-Xmn:新生代大小
- 搭建成功如下 不知道多了个本地的 3个实例外部路由+一个内部路由(启动自带,不知原因,可能是网卡)

image-20240420164553746

nginx实现nacos负载均衡 即架构中的SLB

image-20240420144407362

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# nginx实现nacos负载均衡 
- 安装必要依赖
yum install -y gcc pcre-devel zlib-devel
- 下载nginx
https://nginx.org/en/download.html
- 将nginx上传到服务器并解压缩
tar -zxvf nginx-1.12.2.tar.gz
- 进入解压后的目录并查看nginx安装目录
cd nginx-1.12.2
ls
auto CHANGES CHANGES.ru conf configure contrib html LICENSE man README src
- 在安装目录执行命令(指定安装位置)
./configure --prefix=/usr/nginx
- 执行上述命令后再执行如下命令
make && make install
- 直接启动nginx 把它当作服务器启动
进入sbin目录执行 ./nginx
查看nginx进程是否启动成功: ps aux|grep nginx
查看web页面:http://主机:80 80是默认端口可不写
- 但我们想让它当作代理服务器启动 故要执行下边的操作再启动
先关闭服务 ./nginx -s stop | kill杀死进程
- 配置nginx.conf配置文件
a.加入如下配置
upstream nacos-servers{
server 47.115.221.37:8845;
server 47.115.221.37:8846;
server 47.115.221.37:8847;
}
b.修改
location / {
proxy_pass http://nacos-servers/;
}
- 加载配置启动nginx进行测试
./nginx -c /usr/nginx/conf/nginx.conf
查看nginx进程是否启动成功: ps aux|grep nginx
- 访问47.115.221.37:80/nacos 会根据负载均衡给我们选择一个节点返回 结果如下:

image-20240420174736056

1
2
# 日后我们配置文件项目不用再指定某个nacos server地址了
spring.cloud.nacos.server-addr=47.115.221.37:80

集群搭建遇到问题

但使用springboot启动服务通过nginx提供端口进行服务注册,会发现报错,以下是错误信息。而单机启动nacos ,这可以注册的上去,由此可以判定是集群模式下会出现此问题

image-20240420193940989

原因分析:

应该是nacos 注册时,需要找网卡的 ip地址,但是有多个. 而注册时轮询找的第一个网卡的ip地址.
我们查看nacos的集群配置文件cluster.conf也得到了验证,发现会多出来一条不属于自己配置的ip端口。

image-20240420194106132

解决方案:

1.修改每个nacos的配置文件aplication.properties 指定具体ip地址

image-20240420201148294

2.修改每个nacos-server\conf 的cluster.conf,将多出来的配置删除掉。

3.删除每个nacos-server下的data文件夹

4.先关闭再重启nacos集群和微服务,即可发现服务已注册进nacos集群

sentinel组件

sentinel 简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 历史
- 2012 年,Sentinel 诞生,主要功能为入口流量控制。
- 2013-2017 年,Sentinel 在阿里巴巴集团内部迅速发展,成为基础技术模块,覆盖了所有的核心场景。Sentinel 也因此积累了大量的流量归整场景以及生产实践。
- 2018 年,Sentinel 开源,并持续演进。
- 2019 年,Sentinel 朝着多语言扩展的方向不断探索,推出 C++ 原生版本,同时针对 Service Mesh 场景也推出了 Envoy 集群流量控制支持,以解决 Service Mesh 架构下多语言限流的问题。
- 2020 年,推出 Sentinel Go 版本,继续朝着云原生方向演进。
- 2021 年,Sentinel 正在朝着 2.0 云原生高可用决策中心组件进行演进;同时推出了 Sentinel Rust 原生版本。同时我们也在 Rust 社区进行了 Envoy WASM extension 及 eBPF extension 等场景探索。
- 2022 年,Sentinel 品牌升级为流量治理,领域涵盖流量路由/调度、流量染色、流控降级、过载保护/实例摘除等;同时社区将流量治理相关标准抽出到 OpenSergo 标准中,Sentinel 作为流量治理标准实现。
# 官网
- 官网:https://sentinelguard.io/zh-cn/index.html
- github:https://github.com/alibaba/Sentinel/wiki
- springcloud的alibaba工具集:https://spring.io/projects/spring-cloud-alibaba
# 介绍
- 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
# 作用
- alibaba开源用来对现有微服务系统进行保护 ===》替换hystrix
- hystrix 豪猪 用来保护微服务系统主要用来解决服务雪崩===》服务熔断
- sentinel 用来解决服务雪崩 ====》服务熔断(服务降级)、服务流控...
# 基本概念
- `资源` resource
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
- `规则` rule
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
# 特性
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。

- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。

- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。

image-20240421233918406

sentinel实现原理

1
2
3
4
5
6
7
8
# sentinel提供了两个服务组件 两个组件必须结合使用
- sentinel 用来实现微服务系统中服务熔断、降级等功能 ------->hystrix
- sentinel dashboard 用来监控微服务系统中流量调用等情况 流控 熔断 降级 配置----->hystrix dashboard
- 注:sentinel 以依赖方式引入微服务 ,sentinel dashboard类似后台管理系统 我们下载运行即可
# sentinel实现原理
- 保护哪个微服务哪个服务就引入sentinel,sentinel是真正用来实现微服务系统中服务熔断、降级等功能的
- 但是仅仅引入sentinel还不够 我们还需要在sentinel dashboard配置资源和规则对应关系
- 流程:我们在sentinel dashboard配置规则 ,sentinel dashboard会和sentinel组件进行通信 ,会把规则传给sentinel,当sentinel感知到规则就会在内存中形成这个规则,当我们日后运行该服务时就会运用该规则

sentinel dashboard使用

1
2
3
4
5
6
7
8
9
10
11
# sentinel dashboard下载 sentinel仓库--》tags选择版本下载jar包
- 网址:https://github.com/alibaba/Sentinel/tags
# 上传到linux服务器
# 环境
- 必须安装jdk 配置环境变量 jdk8+
# 直接启动springbootjar包运行 默认端口8080 推荐指定端口启动
- java -jar -Dserver.port=7777 sentinel-dashboard-1.8.7.jar
# 访问dashboard管理界面
- http://47.115.221.37:7777
- 默认账号密码sentinel
界面如下:

image-20240422001038282

sentinel组件使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 服务器启动服务注册中nacos 单例即可
# 开发微服务
- 新建一个独立springboot应用 并注册到nacos server
a.新建一个maven空项目 sentinel 我们这里只是为了方便学习 真正业务要见名知意
b.引入springboot web和nacos client依赖 import changes
c.引入配置文件 配置服务端口号 名称 服务注册中心地址
d.编写启动类并加注解@EnableDiscoveryClient测试springboot是否可以成功启动和查看微服务在注册中心是否成功注册
- 给外部暴漏一个rest接口用于测试 作为sentinel资源
@RestController
public class DemoController {

private static final Logger log = LoggerFactory.getLogger(DemoController.class);

@GetMapping("/demo")
public String demo(){
log.info("demo ok");
return "demo ok";
}
}
- 测试接口是否可以正常访问 http://localhost:8842/demo
# 在微服务引入sentinel依赖
<!--引入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
# 服务器开启7777端口
# 服务器启动sentinel dashboard
# 写配置文件连接到sentinel dashboard 告诉sentinel 它的dashboard ip 和 端口 以实现两者通信
#sentinel dashboard
#开启sentinel保护
spring.cloud.sentinel.enabled=true
#指定sentinel dashboard web地址 控制台地址
spring.cloud.sentinel.transport.dashboard=47.115.221.37:7777
# 应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServer
# 若不配置,会自动扫猫从8719开始扫猫,依次+1,知道值找到未被占用的端口
spring.cloud.sentinel.transport.port=8719
# 访问http://47.115.221.37:7777发现还是没有监控到
- Sentinel采用懒加载机制 dashboard信息必须在指定服务进行资源调用后才能初始化
- 必须经过一次调用才能监控到
# 再次访问接口http://localhost:8842/demo
# 刷新dashboard发现已监控到 成功建立连接
# 问题发现没有实时监控到服务
- 原因是本机网络是局域网,sentinel部署在服务器的话是无法访问到本机网络的,所以要么sentinel和应用都部署上服务器,要么都在本地运行。
- 必须本地跟服务器能ping通才行,它这个机制是双方要能互相传递数据的,应该是因为不仅需要sentinel客户端能访问sentinel-dashboard,同时sentinel-dashboard还需要能访问到sentinel客户端,所以在同一台机器上是能够正常访问的,但是由于本机ip不是公网ip,在未进行设置的情况下服务器端是无法访问到的,所以会报错。
- `查阅资料后暂未解决后边转为本地启动sentinel dashboard` 并修改相关服务配置
#开启sentinel保护
spring.cloud.sentinel.enabled=true
#指定sentinel dashboard web地址 控制台地址
spring.cloud.sentinel.transport.dashboard=localhost:7777
#应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServer
#若不配置,会自动扫猫从8719开始扫猫,依次+1,知道值找到未被占用的端口
spring.cloud.sentinel.transport.port=8719
# 关于系统吞吐量相关概念
- 1.QPS(Query-Per-Second): 称之为系统每秒的请求数
- 2.RT(Response Time): 每个请求的响应时间 单位ms
# 到这里为止我们在微服务引入了sentinel即有了资源 并且通过sentienl和dashboard间建立通信实现了对资源的实时监控 但是我们并没有起到保护作用 我们缺少两大概念之一的规则 我们必须在dashboard建立规则并通过通信传给sentinel才能实现保护

sentinel规则介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.五大规则 网站:https://github.com/alibaba/Sentinel/wiki 我们这里只记录前三个规则 后边两个可自行看文档使用
- a.流控规则:流量控制(flow control)
定义:其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
- b.降级规则: 熔断降级(DegradeRule)
定义: 其原理是监控应用中资源调用请求,达到指定阈值时自动触发熔断降级
- c.热点规则:热点参数限流(ParamFlow)
何为热点?热点即经常访问的数据。
定义:很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
- d.系统规则:系统自适应限流(SystemFlow)
定义:Sentinel 系统自适应过载保护从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
- e.授权规则:黑白名单控制(AuthorityController)
定义:很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制(黑白名单控制)的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

sentinel流量控制

1
2
3
4
5
# 流控规则:流量控制(flow control)
- 定义:其原理是监控应用流量的 `QPS``并发线程数`等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
# 流量控制种类
- 基于QPS控制 当每秒的请求数超过指定阈值之后对当前请求限流 超过阈值请求进行限流后续的请求不处理
- 基于并发线程数流量控制 当tomcat服务器创建的线程数超过指定的阈值之后对当前请求限流 不管多少请求都放进来都处理 只兼顾自己的线程数

基于QPS流量控制测试

1
2
3
4
5
6
7
8
9
10
11
12
13
`测试环境
- 1.服务器启动单例nacos server 本地配置端口号启动sentinel dashboard(服务器使用有点问题 暂用本地启动)
- 2.访问:http://localhost:7777/ http://47.115.221.37:8848/nacos/ 测试是否成功启动上述
- 3.启动上边写的微服务
- 4.访问服务暴漏的接口http://localhost:8842/demo测试接口是否正常 同时访问:http://localhost:7777/ http://47.115.221.37:8848/nacos/ 查看微服务是否成功注册 和 服务里的sentienl组件是否和dashboard间建立通信实现了对资源的实时监控
- 5.开始配置保护规则
一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:
- resource:资源名,即限流规则的作用对象
- count: 限流阈值
- grade: 限流阈值类型(QPS 或并发线程数)
- limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
- strategy: 调用关系限流策略
- controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)

image-20240422210946971

image-20240422211036994

image-20240422211235754

基于并发线程数流量控制

image-20240422211325276

注:这个我们手动测试可不行了,要用到压力测试工具测试

1
2
3
4
5
6
`压测工具下载 
- 网址:https://jmeter.apache.org/download_jmeter.cgi
`环境要求:使用此工具必须配置java环境java8+
`jmeter apache-jmeter-5.6.3.tgz 解压使用 虽然下载的是linux包我们windows也是直接可以用的
- 启动:在bin目录双击jmeter.bat 就会给我们打开一个图形化界面 小黑框别关
- 使用:先给测试计划写个名字 ---》右键添加线程组----》配置如下

image-20240422214912915

image-20240422212921212

image-20240422213027799

image-20240422213058360

image-20240422213738129

image-20240422213807914

image-20240422214524864

1
2
3
4
5
6
7
# 高级选项  即我们编辑流量控制规则里的高级选项 注:配置并发线程数 高级选项没有流控效果
- 1.流控模式
a.直接模式:当前配置资源在运行过程超过当前规则配置的阈值之后对该资源的请求做的处理是什么
b.关联模式:当前配置资源在运行过程超过当前规则配置的阈值之后对它所关联的资源进行请求做什么样的处理
c.链路模式(暂无法测试):当前配置资源在运行过程超过当前规则配置的阈值之后对它它链路中的资源请求做什么 处理
- 流控模式详细介绍如下:
- 实际演示(略):一些场景需要结合压测工具实现

直接模式+快速失败 资源名: /demo 针对来源: default 阈值类型: QPS 单机阈值 : 2

解释:对于demo的请求每秒请求超过2这个阈值之后对后续请求做直接失败处理

关联模式+快速失败 关联资源: /aa 资源名: /demo 针对来源: default 阈值类型: QPS 单机阈值 : 2

注:关联是反向关联 即被关联的影响原本的

解释:如果在这一时刻每秒对aa的请求数超过2这个阈值,本身没有影响 但会导致这个时候对demo的请求是失败的

链路模式+快速失败 入口资源: /product 资源名: /demo 针对来源: default 阈值类型: QPS 单机阈值 : 2

解释:如果在这一时刻每秒对demo的请求数超过2这个阈值,对于demo调用的product整条链路是失败的,别的服务也是无法请求该product链路的

1
2
3
4
5
6
- 2.流控效果 只适用于QPS限流
a.快速失败:直接拒绝请求 并抛出相应异常
b.warm up:(冷启动|预热)
c.排队等待:(始终匀速通过)
- 官方解释和使用场景如下:
- 实际演示(略):一些场景需要结合压测工具实现

快速失败

直接拒绝RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。

warm up

Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:

image-20240422230349104

排队等待

注意:匀速排队模式暂时不支持 QPS > 1000 的场景。

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

该方式的作用如下图所示:

image-20240422230401698

sentinel熔断规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 熔断降级
熔断:用来避免微服务架构中雪崩现象 达到某个阈值条件自动触发熔断
原理:当监控到调用链路中某个服务,出现异常(10s出现20个异常)自动触发熔断,在触发熔断期间对于该服务的调用是不可用的
# sentinel提供的熔断策略
- 慢调用比例
官方定义:慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
通俗定义:

- 异常比例
官方定义:异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
通俗定义:根据请求调用过程出现异常百分比进行熔断

- 异常数
官方定义:异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
通俗定义:根据请求调用过程中异常数进行熔断
# 熔断降级规则(DegradeRule)包含下面几个重要的属性:
# 实际演示(略):一些场景需要结合压测工具实现

image-20240422233649920

Field 说明 默认值
resource 资源名,即规则的作用对象
grade 熔断策略,支持慢调用比例/异常比例/异常数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow 熔断时长,单位为 s
minRequestAmount 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

热点参数限流(还需理解)

1
2
3
4
5
6
# 官方概述
- 何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
- 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
- Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。

image-20240422235428057

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 基本使用
# 必要配置
- 要使用热点参数限流功能,需要引入以下依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>x.y.z</version>
</dependency>
- 注意使用热点参数限流时,不能使用资源路径,必须使用资源别名
@SentinelResource(value="aaa",)
@GetMapping("/demo")
public String demo(){
log.info("demo ok");
return "demo ok";
}
# @SentinelResource注解详解
- value:资源名称,必需项(不能为空)

- entryType:entry 类型,可选项(默认为 EntryType.OUT)

- blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

- fallback / fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
- fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应 的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

- defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
- defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

`简单概括:blockHandler使用sentinel进行不同规则控制时的默认处理方案、fallback自定义业务出错时默认处理方案、defaultFallback指定一个业务错误时默认方案

`特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandler、fallback 和 defaultFallback,则被限流降级时会将 BlockException 直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException)
# 热点参数规则
- 然后为对应的资源配置热点参数限流规则

热点参数规则(ParamFlowRule)类似于流量控制规则(FlowRule):

image-20240422235710776

属性 说明 默认值
resource 资源名,必填
count 限流阈值,必填
grade 限流模式 QPS 模式
durationInSec 统计窗口时间长度(单位为秒),1.6.0 版本开始支持 1s
controlBehavior 流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持 快速失败
maxQueueingTimeMs 最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持 0ms
paramIdx 热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置
paramFlowItemList 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型
clusterMode 是否是集群参数流控规则 false
clusterConfig 集群流控相关配置

系统自适应限流

黑白名单限流

springcloud整合alibaba