微服务(MicroServices)最初是由 Martin Fowler 于 2014 年发表的论文 《MicroServices》 中提出的名词,它一经提出就成为了技术圈的热门话题。
微服务,我们可以从字面上去理解,即“微小的服务”,下面我们从“服务”和“微小”两个方面进行介绍。
所谓“服务”,其实指的是项目中的功能模块,它可以帮助用户解决某一个或一组问题,在开发过程中表现为 IDE(集成开发环境,例如 Eclipse 或 IntelliJ IDEA)中的一个工程或 Moudle。
“微小”则强调的是单个服务的大小,主要体现为以下两个方面:
微服务架构是一种系统架构的设计风格。与传统的单体式架构(ALL IN ONE)不同,微服务架构提倡将一个单一的应用程序拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间使用轻量级通信机制(通常是 HTTP RESTFUL API)进行通讯。
通常情况下,这些小型服务都是围绕着某个特定的业务进行构建的,每一个服务只专注于完成一项任务并把它做好 ,即“专业的人做专业的事”。
每个服务都能够独立地部署到各种环境中,例如开发环境、测试环境和生产环境等,每个服务都能独立启动或销毁而不会对其他服务造成影响。
这些服务之间的交互是使用标准的通讯技术进行的,因此不同的服务可以使用不同数据存储技术,甚至使用不同的编程语言。
在当今的软件开发领域中,主要有两种系统架构风格,那就是新兴的“微服务架构”和传统的“单体架构”。
单体架构是微服务架构出现之前业界最经典的软件架构类型,许多早期的项目采用的也都是单体架构。单体架构将应用程序中所有业务逻辑都编写在同一个工程中,最终经过编译、打包,部署在一台服务器上运行。
在项目的初期,单体架构无论是在开发速度还是运维难度上都具有明显的优势。但随着业务复杂度的不断提高,单体架构的许多弊端也逐渐凸显出来,主要体现在以下 3 个方面:
由于单体架构存在这些弊端,因此许多公司和组织都开始将将它们的项目从单体架构向微服务架构转型。
下面我们就来对比下微服务架构和单体架构到底有什么不同。
不同点 | 微服务架构 | 单体架构 |
---|---|---|
团队规模 | 微服务架构可以将传统模式下的单个应用拆分为多个独立的服务,每个微服务都可以单独开发、部署和维护。每个服务从设计、开发到维护所需的团队规模小,团队管理成本小。 | 单体架构的应用程序通常需要一个大型团队,围绕一个庞大的应用程序工作,团队管理的成本大。 |
数据存储方式 | 不同的微服务可以使用不同的数据存储方式,例如有的用 Redis,有的使用 MySQL。 | 单一架构的所有模块共享同一个公共数据库,存储方式相对单一。 |
部署方式 | 微服务架构中每个服务都可以独立部署,也可以独立于其他服务进行扩展。如果部署得当,基于微服务的架构可以帮助企业提高应用程序的部署效率。 | 采用单体架构的应用程序的每一次功能更改或 bug 修复都必须对整个应用程序重新进行部署。 |
开发模式 | 在采用微服务架构的应用程序中,不同模块可以使用不同的技术或语言进行开发,开发模式更加灵活。 | 在采用单体架构的应用程序中,所有模块使用的技术和语言必须相同,开发模式受限。 |
故障隔离 | 在微服务架构中,故障被隔离在单个服务中,避免系统的整体崩溃。 | 在单体架构中,当一个组件出现故障时,故障很可能会在进程中蔓延,导致系统全局不可用。 |
项目结构 | 微服务架构将单个应用程序拆分为多个独立的小型服务,每个服务都可以独立的开发、部署和维护,每个服务都能完成一项特定的业务需求。 | 单体架构的应用程序,所有的业务逻辑都集中在同一个工程中。 |
微服务具有以下特点:
微服务架构是一种系统架构风格和思想,想要真正地搭建一套微服务系统,则需要微服务框架的支持。随着微服务的流行,很多编程语言都相继推出了它们的微服务框架,下面我们就来简单列举下。
市面上的 Java 微服务框架主要有以下 5 种:
Go 语言中的微服务框架较少,使用的较多的是 GoMicro,它是一个 RPC 框架,具有负载均衡、服务发现、同步通信、异步通讯和消息编码等功能。
Phyton 中的微服务框架主要有 Flask、Falcon、Bottle、Nameko 和 CherryPy 等。
Molecular 是一种使用 NodeJS 构建的事件驱动架构,该框架内置了服务注册表、动态服务发现、负载均衡、容错功能和内置缓存等组件。
Spring Cloud 是一款基于 Spring Boot 实现的微服务框架。Spring Cloud 源自 Spring 社区,主要由 Pivotal 和 Netflix 两大公司提供技术迭代和维护。
随着微服务的火爆流行,国内外各大互联网公司都相继分享了他们在微服务架构中,针对不同场景出现的各种问题的解决方案和开源框架。
以上这些微服务框架或解决方案都具有以下 2 个特点:
这种情况下,搭建一套微分布式微服务系统,就需要针对这些问题从诸多的解决方案中做出选择,这使得我们不得不将大量的精力花费在前期的调研、分析以及实验上。
Spring Cloud 被称为构建分布式微服务系统的“全家桶”,它并不是某一门技术,而是一系列微服务解决方案或框架的有序集合。它将市面上成熟的、经过验证的微服务框架整合起来,并通过 Spring Boot 的思想进行再封装,屏蔽调其中复杂的配置和实现原理,最终为开发人员提供了一套简单易懂、易部署和易维护的分布式系统开发工具包。
Spring Cloud 中包含了 spring-cloud-config、spring-cloud-bus 等近 20 个子项目,提供了服务治理、服务网关、智能路由、负载均衡、断路器、监控跟踪、分布式消息队列、配置管理等领域的解决方案。
Spring Cloud 并不是一个拿来即可用的框架,它是一种微服务规范,共有以下 2 代实现:
这里我们介绍的 Spring Cloud 特指 Spring Cloud 的第一代实现。
Spring Cloud 包括 Spring Cloud Gateway、Spring Cloud Config、Spring Cloud Bus 等近 20 个服务组件,这些组件提供了服务治理、服务网关、智能路由、负载均衡、熔断器、监控跟踪、分布式消息队列、配置管理等领域的解决方案。
Spring Cloud 的常用组件如下表所示。
Spring Cloud 组件 | 描述 |
---|---|
Spring Cloud Netflix Eureka | Spring Cloud Netflix 中的服务治理组件,包含服务注册中心、服务注册与发现机制的实现。 |
Spring Cloud Netflix Ribbon | Spring Cloud Netflix 中的服务调用和客户端负载均衡组件。 |
Spring Cloud Netflix Hystrix | 人称“豪猪哥”,Spring Cloud Netflix 的容错管理组件,为服务中出现的延迟和故障提供强大的容错能力。 |
Spring Cloud Netflix Feign | 基于 Ribbon 和 Hystrix 的声明式服务调用组件。 |
Spring Cloud Netflix Zuul | Spring Cloud Netflix 中的网关组件,提供了智能路由、访问过滤等功能。 |
Spring Cloud Gateway | 一个基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关框架,它使用 Filter 链的方式提供了网关的基本功能,例如安全、监控/指标和限流等。 |
Spring Cloud Config | Spring Cloud 的配置管理工具,支持使用 Git 存储配置内容,实现应用配置的外部化存储,并支持在客户端对配置进行刷新、加密、解密等操作。 |
Spring Cloud Bus | Spring Cloud 的事件和消息总线,主要用于在集群中传播事件或状态变化,以触发后续的处理,例如动态刷新配置。 |
Spring Cloud Stream | Spring Cloud 的消息中间件组件,它集成了 Apache Kafka 和 RabbitMQ 等消息中间件,并通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件之间的隔离。通过向应用程序暴露统一的 Channel 通道,使得应用程序不需要再考虑各种不同的消息中间件实现,就能轻松地发送和接收消息。 |
Spring Cloud Sleuth | Spring Cloud 分布式链路跟踪组件,能够完美的整合 Twitter 的 Zipkin。 |
注:Netflix 是美国的一个在线视频网站,它是公认的大规模生产级微服务的杰出实践者,微服务界的翘楚。Netflix 的开源组件已经在其大规模分布式微服务环境中经过了多年的生产实战验证,成熟且可靠。
Eureka 注册中心 闭源 不再使用 使用nacos注册中心替代
Spring Boot 和 Spring Cloud 都是 Spring 大家族的一员,它们在微服务开发中都扮演着十分重要的角色,两者之间既存在区别也存在联系。
Spring Boot 是一个基于 Spring 的快速开发框架,它能够帮助开发者迅速搭 Web 工程。在微服务开发中,Spring Boot 专注于快速、方便地开发单个微服务。
Spring Cloud 是微服务架构下的一站式解决方案。Spring Cloud 专注于全局微服务的协调和治理工作。换句话说,Spring Cloud 相当于微服务的大管家,负责将 Spring Boot 开发的一个个微服务管理起来,并为它们提供配置管理、服务发现、断路器、路由、微代理、事件总线、决策竞选以及分布式会话等服务。
Spring Cloud 是基于 Spring Boot 实现的。与 Spring Boot 类似,Spring Cloud 也为提供了一系列 Starter,这些 Starter 是 Spring Cloud 使用 Spring Boot 思想对各个微服务框架进行再封装的产物。它们屏蔽了这些微服务框架中复杂的配置和实现原理,使开发人员能够快速、方便地使用 Spring Cloud 搭建一套分布式微服务系统。
Spring Boot 属于一种轻量级的框架,构建 Spring Boot 工程所需的依赖较少。
Spring Cloud 是一系列微服务框架技术的集合体,它的每个组件都需要一个独立的依赖项(Starter POM),因此想要构建一套完整的 Spring Cloud 工程往往需要大量的依赖项。
Spring Boot 不需要 Spring Cloud,就能直接创建可独立运行的工程或模块。
Spring Cloud 是基于 Spring Boot 实现的,它不能独立创建工程或模块,更不能脱离 Spring Boot 独立运行。
》注意:虽然 Spring Boot 能够用于开发单个微服务,但它并不具备管理和协调微服务的能力,因此它只能算是一个微服务快速开发框架,而非微服务框架。
Spring Cloud 包含了许多子项目(组件),这些子项目都是独立进行内容更新和迭代的,各自都维护着自己的发布版本号。
为了避免 Spring Cloud 的版本号与其子项目的版本号混淆,Spring Cloud 没有采用常见的数字版本号,而是通过以下方式定义版本信息。
{version.name} .{version.number}
Spring Cloud 版本信息说明如下:
在使用 Spring Boot + Spring Cloud 进行微服务开发时,我们需要根据项目中 Spring Boot 的版本来决定 Spring Cloud 版本,否则会出现许多意想不到的错误。
Spring Boot 与 Spring Cloud 的版本对应关系如下表(参考自 Spring Cloud 官网)。
Spring Cloud | Spring Boot |
---|---|
2020.0.x (Ilford) | 2.4.x, 2.5.x (从 Spring Cloud 2020.0.3 开始) |
Hoxton | 2.2.x, 2.3.x (从 Spring Cloud SR5 开始) |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgware | 1.5.x |
Dalston | 1.5.x |
注意:Spring Cloud 官方已经停止对 Dalston、Edgware、Finchley 和 Greenwich 的版本更新。
除了上表中展示的版本对应关系之外,我们还可以使用浏览器访问 https://start.spring.io/actuator/info,获取 Spring Cloud 与 Spring Boot 的版本对应关系(JSON 版)。
纯文本复制
{
……
"bom-ranges":{
……
"spring-cloud":{
"Hoxton.SR12":"Spring Boot >=2.2.0.RELEASE and <2.4.0.M1",
"2020.0.4":"Spring Boot >=2.4.0.M1 and <2.5.6-SNAPSHOT",
"2020.0.5-SNAPSHOT":"Spring Boot >=2.5.6-SNAPSHOT and <2.6.0-M1",
"2021.0.0-M1":"Spring Boot >=2.6.0.M1 and <2.6.0-SNAPSHOT",
"2021.0.0-SNAPSHOT":"Spring Boot >=2.6.0-SNAPSHOT"
},
……
},
……
}
Eureka 一词来源于古希腊词汇,是“发现了”的意思。在软件领域,Eureka 是 Netflix 公司开发的一款开源的服务注册与发现组件。
Spring Cloud 将 Eureka 与 Netflix 中的其他开源服务组件(例如 Ribbon、Feign 以及 Hystrix 等)一起整合进 Spring Cloud Netflix 模块中,整合后的组件全称为 Spring Cloud Netflix Eureka。
Eureka 是 Spring Cloud Netflix 模块的子模块,它是 Spring Cloud 对 Netflix Eureka 的二次封装,主要负责 Spring Cloud 的服务注册与发现功能。
Spring Cloud 使用 Spring Boot 思想为 Eureka 增加了自动化配置,开发人员只需要引入相关依赖和注解,就能将 Spring Boot 构建的微服务轻松地与 Eureka 进行整合。
Eureka 采用 CS(Client/Server,客户端/服务器) 架构,它包括以下两大组件:
Eureka 实现服务注册与发现的原理,如下图所示。
图1:Eureka 原理图
上图中共涉及到以下 3 个角色:
Eureka 实现服务注册与发现的流程如下:
服务注册中心(Eureka Server)所扮演的角色十分重要,它是服务提供者和服务消费者之间的桥梁。服务提供者只有将自己的服务注册到服务注册中心才可能被服务消费者调用,而服务消费者也只有通过服务注册中心获取可用服务列表后,才能调用所需的服务。
下面,我们通过一个案例来展示下 Eureka 是如何实现服务注册与发现的。
由于本案例中,会涉及到多个由 Spring Boot 创建的微服务,为了方便管理,这里我们采用 Maven 的多 Module 结构(即一个 Project 包含多个 Module)来构建工程。
创建一个名为 spring-cloud-demo2 的 Maven 主工程 ,然后在该主工程的 pom.xml 中使用 dependencyManagement 来管理 Spring Cloud 的版本,内容如下。
<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.0modelVersion>
<packaging>pompackaging>
<modules>
<module>micro-service-cloud-apimodule>
modules>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.6.RELEASEversion>
<relativePath/>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>spring-cloud-demo2artifactId>
<version>0.0.1-SNAPSHOTversion>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<junit.version>4.12junit.version>
<log4j.version>1.2.17log4j.version>
<lombok.version>1.16.18lombok.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Hoxton.SR12version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<finalName>microservicecloudfinalName>
<resources>
<resource>
<directory>src/main/resourcesdirectory>
<filtering>truefiltering>
resource>
resources>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-resources-pluginartifactId>
<configuration>
<delimiters>
<delimit>$delimit>
delimiters>
configuration>
plugin>
plugins>
build>
project>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-demo2artifactId>
<groupId>net.biancheng.cgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>micro-service-cloud-apiartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
project>
注:micro-service-cloud-api 是整个工程的公共子模块,它包含了一些其他子模块共有的内容,例如实体类、公共工具类、公共依赖项等。当其他子模块需要使用公共子模块中的内容时,只需要在其 pom.xml 引入公共子模块的依赖即可。
package net.biancheng.c.entity;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@NoArgsConstructor //无参构造函数
@Data // 提供类的get、set、equals、hashCode、canEqual、toString 方法
@Accessors(chain = true)
public class Dept implements Serializable {
private Integer deptNo;
private String deptName;
private String dbSource;
}
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>spring-cloud-demo2artifactId>
<groupId>net.biancheng.cgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>micro-service-cloud-eureka-7001artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>micro-service-cloud-eureka-7001name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
server:
port: 7001 #该 Module 的端口号
eureka:
instance:
hostname: localhost #eureka服务端的实例名称,
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #单机版服务注册中心
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer //开启 Eureka server,接受其他微服务的注册
public class MicroServiceCloudEureka7001Application {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudEureka7001Application.class, args);
}
}
Eureka Server 7001 服务注册中心
图2:Eureka 7001 服务注册中心
4. 搭建服务提供者
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>spring-cloud-demo2artifactId>
<groupId>net.biancheng.cgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>micro-service-cloud-provider-dept-8001artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>micro-service-cloud-provider-dept-8001name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>net.biancheng.cgroupId>
<artifactId>micro-service-cloud-apiartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-coreartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>springloadedartifactId>
<version>1.2.8.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-maven-pluginartifactId>
<version>1.4.0version>
<configuration>
<configurationFile>src/main/resources/mybatis-generator/generatorConfig.xmlconfigurationFile>
<verbose>trueverbose>
<overwrite>trueoverwrite>
configuration>
<dependencies>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
dependency>
<dependency>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-coreartifactId>
<version>1.4.0version>
dependency>
dependencies>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
server:
port: 8001 #服务端口号
spring:
application:
name: microServiceCloudProviderDept #微服务名称,对外暴漏的微服务名称,十分重要
################################################## JDBC 通用配置 ##########################################
datasource:
username: root #数据库登陆用户名
password: root #数据库登陆密码
url: jdbc:mysql://127.0.0.1:3306/bianchengbang_jdbc #数据库url
driver-class-name: com.mysql.jdbc.Driver #数据库驱动
############################### 不检查 spring.config.import=configserver:##################
# cloud:
# config:
# enabled: false
###################################### MyBatis 配置 ######################################
mybatis:
# 指定 mapper.xml 的位置
mapper-locations: classpath:mybatis/mapper/*.xml
#扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
type-aliases-package: net.biancheng.c.entity
configuration:
#默认开启驼峰命名法,可以不用设置该属性
map-underscore-to-camel-case: true
########################################### Spring cloud 自定义服务名称和 ip 地址###############################################
eureka:
client: #将客户端注册到 eureka 服务列表内
service-url:
defaultZone: http://eureka7001.com:7001/eureka #这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版)
instance:
instance-id: spring-cloud-provider-8001 #自定义服务名称信息
prefer-ip-address: true #显示访问路径的 ip 地址
########################################## spring cloud 使用 Spring Boot actuator 监控完善信息###################################
# Spring Boot 2.50对 actuator 监控屏蔽了大多数的节点,只暴露了 heath 节点,本段配置(*)就是为了开启所有的节点
management:
endpoints:
web:
exposure:
include: "*" # * 在yaml 文件属于关键字,所以需要加引号
info:
app.name: micro-service-cloud-provider-dept
company.name: c.biancheng.net
build.aetifactId: @project.artifactId@
build.version: @project.version@
package net.biancheng.c.mapper;
import net.biancheng.c.entity.Dept;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface DeptMapper {
//根据主键获取数据
Dept selectByPrimaryKey(Integer deptNo);
//获取表中的全部数据
List<Dept> GetAll();
}
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.biancheng.c.mapper.DeptMapper">
<resultMap id="BaseResultMap" type="net.biancheng.c.entity.Dept">
<id column="dept_no" jdbcType="INTEGER" property="deptNo"/>
<result column="dept_name" jdbcType="VARCHAR" property="deptName"/>
<result column="db_source" jdbcType="VARCHAR" property="dbSource"/>
resultMap>
<sql id="Base_Column_List">
dept_no
, dept_name, db_source
sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from dept
where dept_no = #{deptNo,jdbcType=INTEGER}
select>
<select id="GetAll" resultType="net.biancheng.c.entity.Dept">
select *
from dept;
select>
mapper>
package net.biancheng.c.service;
import net.biancheng.c.entity.Dept;
import java.util.List;
public interface DeptService {
Dept get(Integer deptNo);
List<Dept> selectAll();
}
package net.biancheng.c.service.impl;
import net.biancheng.c.entity.Dept;
import net.biancheng.c.mapper.DeptMapper;
import net.biancheng.c.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("deptService")
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public Dept get(Integer deptNo) {
return deptMapper.selectByPrimaryKey(deptNo);
}
@Override
public List<Dept> selectAll() {
return deptMapper.GetAll();
}
}
package net.biancheng.c.controller;
import lombok.extern.slf4j.Slf4j;
import net.biancheng.c.entity.Dept;
import net.biancheng.c.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 服务提供者的控制层
*/
@RestController
@Slf4j
public class DeptController {
@Autowired
private DeptService deptService;
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "/dept/get/{id}", method = RequestMethod.GET)
public Dept get(@PathVariable("id") int id) {
return deptService.get(id);
}
@RequestMapping(value = "/dept/list", method = RequestMethod.GET)
public List<Dept> list() {
return deptService.selectAll();
}
}
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient // Spring cloud Eureka 客户端,自动将本服务注册到 Eureka Server 注册中心中
public class MicroServiceCloudProviderDept8001Application {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudProviderDept8001Application.class, args);
}
}
图3:服务提供者注册到服务注册中心
从图 3 可以看到, Instances currently registered with Eureka (注册到 Eureka Server 的实例)选项中已经包含了一条服务信息,即已经有服务注册到 Eureka Server 上了。
Instances currently registered with Eureka 选项中包含以下内容:
Application:MICROSERVICECLOUDPROVIDERDEPT,该取值为 micro-service-cloud-provider-dept-8001 配置文件 application.yml 中 spring.application.name 的取值。
Status: UP (1) - spring-cloud-provider-8001,UP 表示服务在线, (1) 表示有集群中服务的数量,spring-cloud-provider-8001 则是 micro-service-cloud-provider-dept-8001 配置文件 application.yml 中 eureka.instance.instance-id 的取值。
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`dept_no` int NOT NULL AUTO_INCREMENT,
`dept_name` varchar(255) DEFAULT NULL,
`db_source` varchar(255) DEFAULT NULL,
PRIMARY KEY (`dept_no`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `dept` VALUES ('1', '开发部', 'bianchengbang_jdbc');
INSERT INTO `dept` VALUES ('2', '人事部', 'bianchengbang_jdbc');
INSERT INTO `dept` VALUES ('3', '财务部', 'bianchengbang_jdbc');
INSERT INTO `dept` VALUES ('4', '市场部', 'bianchengbang_jdbc');
INSERT INTO `dept` VALUES ('5', '运维部', 'bianchengbang_jdbc');
图4:服务提供者提供服务访问数据库
在微服务架构中,一个系统往往由十几甚至几十个服务组成,若将这些服务全部注册到同一个 Eureka Server 中,就极有可能导致 Eureka Server 因不堪重负而崩溃,最终导致整个系统瘫痪。解决这个问题最直接的办法就是部署 Eureka Server 集群。
我们知道,在 Eureka 实现服务注册与发现时一共涉及了 3 个角色:服务注册中心、服务提供者以及服务消费者,这三个角色分工明确,各司其职。但是其实在 Eureka 中,所有服务都既是服务消费者也是服务提供者,服务注册中心 Eureka Server 也不例外。
我们在搭建服务注册中心时,在 application.yml 中涉及了这样的配置:
eureka:
client:
register-with-eureka: false #false 表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务
这样设置的原因是 micro-service-cloud-eureka-7001 本身自己就是服务注册中心,服务注册中心是不能将自己注册到自己身上的,但服务注册中心是可以将自己作为服务向其他的服务注册中心注册自己的。
举个例子,有两个 Eureka Server 分别为 A 和 B,虽然 A 不能将自己注册到 A 上,B 也不能将自己注册到 B 上,但 A 是可以作为一个服务把自己注册到 B 上的,同理 B 也可以将自己注册到 A 上。
这样就可以形成一组互相注册的 Eureka Server 集群,当服务提供者发送注册请求到 Eureka Server 时,Eureka Server 会将请求转发给集群中所有与之相连的 Eureka Server 上,以实现 Eureka Server 之间的服务同步。
通过服务同步,服务消费者可以在集群中的任意一台 Eureka Server 上获取服务提供者提供的服务。这样,即使集群中的某个服务注册中心发生故障,服务消费者仍然可以从集群中的其他 Eureka Server 中获取服务信息并调用,而不会导致系统的整体瘫痪,这就是 Eureka Server 集群的高可用性。
示例 2
下面我们在示例 1 的基础上进行扩展,构建一个拥有 3 个 Eureka Server 实例的集群。
参照 micro-service-cloud-eureka-7001 的搭建过程,在主工程下另外再创建两个 Eureka Server:micro-service-cloud-eureka-7002 和 micro-service-cloud-eureka-7003,此时这 3 个 Eureka Server 无论是 Maven 依赖、代码还是配置都是一模一样的。
修改 micro-service-cloud-eureka-7001、micro-service-cloud-eureka-7002、micro-service-cloud-eureka-7003 中 application.yml 的配置,具体配置如下 。
micro-service-cloud-eureka-7001 中 application.yml 的配置如下。
server:
port: 7001 #端口号
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false 表示不向注册中心注册自己。
fetch-registry: false #false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #单机版
defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #集群版 将当前的 Eureka Server 注册到 7003 和 7003 上,形成一组互相注册的 Eureka Server 集群
micro-service-cloud-eureka-7002 中 application.yml 的配置如下。
server:
port: 7002 #端口号
eureka:
instance:
hostname: eureka7002.com #Eureka Server 实例名称
client:
register-with-eureka: false #false 表示不向注册中心注册自己。
fetch-registry: false #false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/ #将这个 Eureka Server 注册到 7001 和 7003 上
micro-service-cloud-eureka-7003 中 application.yml 的配置如下。
server:
port: 7003 #端口号
eureka:
instance:
hostname: eureka7003.com #Eureka Server 实例名称
client:
register-with-eureka: false #false 表示不向注册中心注册自己。
fetch-registry: false #false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ #将这个 Eureka Server 注册到 7001 和 7002 上
由于我们是在本地搭建的 Eureka Server 集群,因此我们需要修改本地的 host 文件,Windows 操作系统的电脑在 C:/Windows/System/drivers/etc/hosts 中修改,Mac 系统的电脑则需要在 vim/etc/hosts 中修改,修改内容如下。
#Spring Cloud eureka 集群
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
127.0.0.1 eureka7003.com
修改 micro-service-cloud-provider-dept-8001(服务提供者)配置文件 application.yml 中 eureka.client.service-url.defaultZone 的取值,将服务注册到 Eureka Server 集群上,具体配置如下。
eureka:
client: #将客户端注册到 eureka 服务列表内
service-url:
#defaultZone: http://eureka7001.com:7001/eureka #这个地址是 7001 注册中心在 application.yml 中暴露出来的注册地址 (单机版)
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #将服务注册到 Eureka Server 集群
启动 micro-service-cloud-eureka-7001,使用浏览器访问“http://eureka7001.com:7001/”,结果如下图。
图5:Eureka Server 集群 -7001
从上图可以看到, 服务提供者(micro-service-cloud-provider-dept-8001)的服务已经注册到了 Eureka Server 7001,并且在 DS Replicas 选项中也显示了集群中的另外两个 Eureka Server:Eureka Server 7002 和 Eureka Server 7003。
图6:Eureka Server 集群 -7002
从上图可以看到, 服务提供者(micro-service-cloud-provider-dept-8001)所提供的服务已经注册到了 Eureka Server 7002,并且在 DS Replicas 选项中也显示了集群中的另外两个 Eureka Server:Eureka Server 7001 和 Eureka Server 7003。
图7:Eureka Server 集群 -7003
从上图可以看到, 服务提供者(micro-service-cloud-provider-dept-8001)所提供的服务已经注册到了 Eureka Server 7003,并且在 DS Replicas 选项中也显示了集群中的另外两个 Eureka Server:Eureka Server 7001 和 Eureka Server 7002。
自此我们就完成了 Eureka Server 集群的搭建和使用。
当我们在本地调试基于 Eureka 的程序时,Eureka 服务注册中心很有可能会出现如下图所示的红色警告。
实际上,这个警告是触发了 Eureka 的自我保护机制而出现的。默认情况下,如果 Eureka Server 在一段时间内(默认为 90 秒)没有接收到某个服务提供者(Eureka Client)的心跳,就会将这个服务提供者提供的服务从服务注册表中移除。 这样服务消费者就再也无法从服务注册中心中获取到这个服务了,更无法调用该服务。
但在实际的分布式微服务系统中,健康的服务(Eureka Client)也有可能会由于网络故障(例如网络延迟、卡顿、拥挤等原因)而无法与 Eureka Server 正常通讯。若此时 Eureka Server 因为没有接收心跳而误将健康的服务从服务列表中移除,这显然是不合理的。而 Eureka 的自我保护机制就是来解决此问题的。
所谓 “Eureka 的自我保护机制”,其中心思想就是“好死不如赖活着”。如果 Eureka Server 在一段时间内没有接收到 Eureka Client 的心跳,那么 Eureka Server 就会开启自我保护模式,将所有的 Eureka Client 的注册信息保护起来,而不是直接从服务注册表中移除。一旦网络恢复,这些 Eureka Client 提供的服务还可以继续被服务消费者消费。
综上,Eureka 的自我保护机制是一种应对网络异常的安全保护措施。它的架构哲学是:宁可同时保留所有微服务(健康的服务和不健康的服务都会保留)也不盲目移除任何健康的服务。通过 Eureka 的自我保护机制,可以让 Eureka Server 集群更加的健壮、稳定。
Eureka 的自我保护机制也存在弊端。如果在 Eureka 自我保护机制触发期间,服务提供者提供的服务出现问题,那么服务消费者就很容易获取到已经不存在的服务进而出现调用失败的情况,此时,我们可以通过客户端的容错机制来解决此问题,详情请参考 Spring Cloud Netflix Ribbon 和 Spring Cloud Netflix Hystrix。
默认情况下,Eureka 的自我保护机制是开启的,如果想要关闭,则需要在配置文件中添加以下配置。
eureka:
server:
enable-self-preservation: false # false 关闭 Eureka 的自我保护机制,默认是开启,一般不建议大家修改
示例 3
下面我们通过一个实例,来验证下 Eureka 的自我保护机制。
在 micro-service-cloud-eureka-7001 的配置文件 application.yml 中添加以下配置,关闭 Eureka 的自我保护机制。
eureka:
server:
enable-self-preservation: false # false 关闭 Eureka 的自我保护机制,默认是开启,一般不建议大家修改
集群中的 micro-service-cloud-eureka-7002 和 micro-service-cloud-eureka-7002 不作任何修改,即它们的自我保护机制是开启的。
重启 Eureka Server 集群以及 micro-service-cloud-provider-dept-8001,使用浏览器访问“http://eureka7001.com:7001/”,结果如下图。
图9:Eureka 关闭自我保护机制
从图 8 ,您可以看到以下内容:
在 DS Replicas 选项上面出现了红色警告信息“THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.”,出现该信息则表示“ Eureka 自我保护模式已关闭。”
micro-service-cloud-provider-dept-8001 提供的服务已经注册到该 Eureka Server 中。
图10:Eureka 自我保护机制
从图 9 可以看出,micro-service-cloud-provider-dept-8001 提供的服务也已经注册到当前 Eureka Server 中,但 DS Replicas 选项上方没有任何警告提示。
图11:Eureka 关闭自我保护机制-2
在图 10 中 ,我们可以看到以下内容:
在 DS Replicas 选项上面出现了红色警告信息“RENEWALS ARE LESSER THAN THE THRESHOLD. THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.”,出现该信息则表示 Eureka 的自我保护模式已关闭,且已经有服务被移除。
micro-service-cloud-provider-dept-8001 提供的服务已经从服务列表中移除。
图12:Eureka 自我保护机制生效
在图 11 中 ,您可以看到以下内容:
在 DS Replicas 选项上面出现了红色警告信息“EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.”,出现该信息表明 Eureka 的自我保护机制处于开启状态,且已经被触发。
micro-service-cloud-provider-dept-8001 的服务信息依然保存 Eureka Server 服务注册表中,并未被移除。
Eureka 注册中心 闭源 不再使用 使用nacos注册中心替代
Spring Cloud Ribbon 是一套基于 Netflix Ribbon 实现的客户端负载均衡和服务调用工具。
Netflix Ribbon 是 Netflix 公司发布的开源组件,其主要功能是提供客户端的负载均衡算法和服务调用。Spring Cloud 将其与 Netflix 中的其他开源服务组件(例如 Eureka、Feign 以及 Hystrix 等)一起整合进 Spring Cloud Netflix 模块中,整合后全称为 Spring Cloud Netflix Ribbon。
Ribbon 是 Spring Cloud Netflix 模块的子模块,它是 Spring Cloud 对 Netflix Ribbon 的二次封装。通过它,我们可以将面向服务的 REST 模板(RestTemplate)请求转换为客户端负载均衡的服务调用。
Ribbon 是 Spring Cloud 体系中最核心、最重要的组件之一。它虽然只是一个工具类型的框架,并不像 Eureka Server(服务注册中心)那样需要独立部署,但它几乎存在于每一个使用 Spring Cloud 构建的微服务中。
Spring Cloud 微服务之间的调用,API 网关的请求转发等内容,实际上都是通过 Spring Cloud Ribbon 来实现的,包括后续我们要介绍的 OpenFeign 也是基于它实现的。
在任何一个系统中,负载均衡都是一个十分重要且不得不去实施的内容,它是系统处理高并发、缓解网络压力和服务端扩容的重要手段之一。
负载均衡(Load Balance) ,简单点说就是将用户的请求平摊分配到多个服务器上运行,以达到扩展服务器带宽、增强数据处理能力、增加吞吐量、提高网络的可用性和灵活性的目的。
常见的负载均衡方式有两种:
服务端负载均衡是最常见的负载均衡方式,其工作原理如下图。
图1:服务端负载均衡工作原理
服务端负载均衡是在客户端和服务端之间建立一个独立的负载均衡服务器,该服务器既可以是硬件设备(例如 F5),也可以是软件(例如 Nginx)。这个负载均衡服务器维护了一份可用服务端清单,然后通过心跳机制来删除故障的服务端节点,以保证清单中的所有服务节点都是可以正常访问的。
当客户端发送请求时,该请求不会直接发送到服务端进行处理,而是全部交给负载均衡服务器,由负载均衡服务器按照某种算法(例如轮询、随机等),从其维护的可用服务清单中选择一个服务端,然后进行转发。
服务端负载均衡具有以下特点:
相较于服务端负载均衡,客户端服务在均衡则是一个比较小众的概念。
客户端负载均衡的工作原理如下图。
图2:客户端负载均衡工作原理
客户端负载均衡是将负载均衡逻辑以代码的形式封装到客户端上,即负载均衡器位于客户端。客户端通过服务注册中心(例如 Eureka Server)获取到一份服务端提供的可用服务清单。有了服务清单后,负载均衡器会在客户端发送请求前通过负载均衡算法选择一个服务端实例再进行访问,以达到负载均衡的目的;
客户端负载均衡也需要心跳机制去维护服务端清单的有效性,这个过程需要配合服务注册中心一起完成。
客户端负载均衡具有以下特点:
Ribbon 就是一个基于 HTTP 和 TCP 的客户端负载均衡器,当我们将 Ribbon 和 Eureka 一起使用时,Ribbon 会从 Eureka Server(服务注册中心)中获取服务端列表,然后通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。
下面我们就来对比下,服务端负载均衡和客户端负载均衡到底有什么区别,如下表。
不同点 | 服务端负载均衡 | 客户端负载均衡 |
---|---|---|
是否需要建立负载均衡服务器 | 需要在客户端和服务端之间建立一个独立的负载均衡服务器。 | 将负载均衡的逻辑以代码的形式封装到客户端上,因此不需要单独建立负载均衡服务器。 |
是否需要服务注册中心 | 不需要服务注册中心。 | 需要服务注册中心。在客户端负载均衡中,所有的客户端和服务端都需要将其提供的服务注册到服务注册中心上。 |
可用服务清单存储的位置 | 可用服务清单存储在位于客户端与服务器之间的负载均衡服务器上。 所有的客户端都维护了一份可用服务清单,这些清单都是从服务注册中心获取的。负载均衡的时机 | 先将请求发送到负载均衡服务器,然后由负载均衡服务器通过负载均衡算法,在多个服务端之间选择一个进行访问;即在服务器端再进行负载均衡算法分配。简单点说就是,先发送请求,再进行负载均衡。 |
客户端是否了解服务提供方信息 | 由于负载均衡是在客户端发送请求后进行的,因此客户端并不知道到底是哪个服务端提供的服务。 | 负载均衡是在客户端发送请求前进行的,因此客户端清楚的知道是哪个服务端提供的服务。 |
Ribbon 可以与 RestTemplate(Rest 模板)配合使用,以实现微服务之间的调用。
RestTemplate 是 Spring 家族中的一个用于消费第三方 REST 服务的请求框架。RestTemplate 实现了对 HTTP 请求的封装,提供了一套模板化的服务调用方法。通过它,Spring 应用可以很方便地对各种类型的 HTTP 请求进行访问。
RestTemplate 针对各种类型的 HTTP 请求都提供了相应的方法进行处理,例如 HEAD、GET、POST、PUT、DELETE 等类型的 HTTP 请求,分别对应 RestTemplate 中的 headForHeaders()、getForObject()、postForObject()、put() 以及 delete() 方法。
下面我们通过一个简单的实例,来演示 Ribbon 是如何实现服务调用的。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>spring-cloud-demo2artifactId>
<groupId>net.demo.cgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<groupId>net.demo.cgroupId>
<artifactId>micro-service-cloud-consumer-dept-80artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>micro-service-cloud-consumer-dept-80name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>net.demo.cgroupId>
<artifactId>micro-service-cloud-apiartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
server:
port: 80 #端口号
############################################# Spring Cloud Ribbon 负载均衡配置##########################
eureka:
client:
register-with-eureka: false #本微服务为服务消费者,不需要将自己注册到服务注册中心
fetch-registry: true #本微服务为服务消费者,需要到服务注册中心搜索服务
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #服务注册中心集群
package net.demo.c.config;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RetryRule;
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 ConfigBean {
@Bean //将 RestTemplate 注入到容器中
@LoadBalanced //在客户端使用 RestTemplate 请求服务端时,开启负载均衡(Ribbon)
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
4. 在 net.demo.c.controller 包下,创建一个名为 DeptController_Consumer 的 Controller,该 Controller 中定义的请求用于调用服务端提供的服务,代码如下。
package net.demo.c.controller;
import net.demo.c.entity.Dept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
public class DeptController_Consumer {
//private static final String REST_URL_PROVIDER_PREFIX = "http://localhost:8001/"; 这种方式是直调用服务方的方法,根本没有用到 Spring Cloud
//面向微服务编程,即通过微服务的名称来获取调用地址
private static final String REST_URL_PROVIDER_PREFIX = "http://MICROSERVICECLOUDPROVIDERDEPT"; // 使用注册到 Spring Cloud Eureka 服务注册中心中的服务,即 application.name
@Autowired
private RestTemplate restTemplate; //RestTemplate 是一种简单便捷的访问 restful 服务模板类,是 Spring 提供的用于访问 Rest 服务的客户端模板工具集,提供了多种便捷访问远程 HTTP 服务的方法
//获取指定部门信息
@RequestMapping(value = "/consumer/dept/get/{id}")
public Dept get(@PathVariable("id") Integer id) {
return restTemplate.getForObject(REST_URL_PROVIDER_PREFIX + "/dept/get/" + id, Dept.class);
}
//获取部门列表
@RequestMapping(value = "/consumer/dept/list")
public List<Dept> list() {
return restTemplate.getForObject(REST_URL_PROVIDER_PREFIX + "/dept/list", List.class);
}
}
在 micro-service-cloud-consumer-dept-80 的主启动类上,使用 @EnableEurekaClient 注解来开启 Eureka 客户端功能,代码如下。
package net.demo.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class MicroServiceCloudConsumerDept80Application {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudConsumerDept80Application.class, args);
}
}
依次启动服务注册中心 micro-service-cloud-eureka-7001、服务提供者 micro-service-cloud-provider-dept-8001 以及服务消费者 micro-service-cloud-consumer-dept-80。
使用浏览器访问“http://eureka7001.com:80/consumer/dept/list”,结果如下图。
图3:Ribbon 实现服务调用
Ribbon 是一个客户端的负载均衡器,它可以与 Eureka 配合使用轻松地实现客户端的负载均衡。Ribbon 会先从 Eureka Server(服务注册中心)去获取服务端列表,然后通过负载均衡策略将请求分摊给多个服务端,从而达到负载均衡的目的。
Spring Cloud Ribbon 提供了一个 IRule 接口,该接口主要用来定义负载均衡策略,它有 7 个默认实现类,每一个实现类都是一种负载均衡策略。
序号 | 实现类 | 负载均衡策略 |
---|---|---|
1 | RoundRobinRule | 按照线性轮询策略,即按照一定的顺序依次选取服务实例 |
2 | RandomRule | 随机选取一个服务实例 |
3 | RetryRule | 按照 RoundRobinRule(轮询)的策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试(重试时获取服务的策略还是 RoundRobinRule 中定义的策略),如果超过指定时间依然没获取到服务实例则返回 null 。 |
4 | WeightedResponseTimeRule | WeightedResponseTimeRule 是 RoundRobinRule 的一个子类,它对 RoundRobinRule 的功能进行了扩展。根据平均响应时间,来计算所有服务实例的权重,响应时间越短的服务实例权重越高,被选中的概率越大。刚启动时,如果统计信息不足,则使用线性轮询策略,等信息足够时,再切换到 WeightedResponseTimeRule。 |
5 | BestAvailableRule | 继承自 ClientConfigEnabledRoundRobinRule。先过滤点故障或失效的服务实例,然后再选择并发量最小的服务实例。 |
6 | AvailabilityFilteringRule | 先过滤掉故障或失效的服务实例,然后再选择并发量较小的服务实例。 |
7 | ZoneAvoidanceRule | 默认的负载均衡策略,综合判断服务所在区域(zone)的性能和服务(server)的可用性,来选择服务实例。在没有区域的环境下,该策略与轮询(RandomRule)策略类似。 |
下面我们就来通过一个实例来验证下,Ribbon 默认是使用什么策略选取服务实例的。
DROP DATABASE IF EXISTS spring_cloud_db2;
CREATE DATABASE spring_cloud_db2 CHARACTER SET UTF8;
USE spring_cloud_db2;
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`dept_no` int NOT NULL AUTO_INCREMENT,
`dept_name` varchar(255) DEFAULT NULL,
`db_source` varchar(255) DEFAULT NULL,
PRIMARY KEY (`dept_no`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `dept` VALUES ('1', '开发部', DATABASE());
INSERT INTO `dept` VALUES ('2', '人事部', DATABASE());
INSERT INTO `dept` VALUES ('3', '财务部', DATABASE());
INSERT INTO `dept` VALUES ('4', '市场部', DATABASE());
INSERT INTO `dept` VALUES ('5', '运维部', DATABASE());
#############################################################################################
DROP DATABASE IF EXISTS spring_cloud_db3;
CREATE DATABASE spring_cloud_db3 CHARACTER SET UTF8;
USE spring_cloud_db3;
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`dept_no` int NOT NULL AUTO_INCREMENT,
`dept_name` varchar(255) DEFAULT NULL,
`db_source` varchar(255) DEFAULT NULL,
PRIMARY KEY (`dept_no`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `dept` VALUES ('1', '开发部', DATABASE());
INSERT INTO `dept` VALUES ('2', '人事部', DATABASE());
INSERT INTO `dept` VALUES ('3', '财务部', DATABASE());
INSERT INTO `dept` VALUES ('4', '市场部', DATABASE());
INSERT INTO `dept` VALUES ('5', '运维部', DATABASE());
参考 micro-service-cloud-provider-dept-8001,再创建两个微服务 Moudle :micro-service-cloud-provider-dept-8002 和 micro-service-cloud-provider-dept-8003。
在 micro-service-cloud-provider-dept-8002 中 application.yml 中,修改端口号、数据库连接信息以及自定义服务名称信息(eureka.instance.instance-id),修改的配置如下。
server:
port: 8002 #端口号修改为 8002
spring:
application:
name: microServiceCloudProviderDept #微服务名称,不做修改,与 micro-service-cloud-provider-dept-8001 的配置保持一致
datasource:
username: root #数据库登陆用户名
password: root #数据库登陆密码
url: jdbc:mysql://127.0.0.1:3306/spring_cloud_db2 #数据库url
driver-class-name: com.mysql.jdbc.Driver #数据库驱动
eureka:
client: #将客户端注册到 eureka 服务列表内
service-url:
#defaultZone: http://eureka7001:7001/eureka #这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版)
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #将服务注册到 Eureka 集群
instance:
instance-id: spring-cloud-provider-8002 #修改自定义的服务名称信息
prefer-ip-address: true #显示访问路径的 ip 地址
server:
port: 8003 #端口号修改为 8003
spring:
application:
name: microServiceCloudProviderDept #微服务名称,不做修改,与 micro-service-cloud-provider-dept-8001 的配置保持一致
datasource:
username: root #数据库登陆用户名
password: root #数据库登陆密码
url: jdbc:mysql://127.0.0.1:3306/spring_cloud_db3 #数据库url
driver-class-name: com.mysql.jdbc.Driver #数据库驱动
eureka:
client: #将客户端注册到 eureka 服务列表内
service-url:
#defaultZone: http://eureka7001:7001/eureka #这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版)
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #将服务注册到 Eureka 集群
instance:
instance-id: spring-cloud-provider-8003 #自定义服务名称信息
prefer-ip-address: true #显示访问路径的 ip 地址
依次启动 micro-service-cloud-eureka-7001/7002/7003(服务注册中心集群)、micro-service-cloud-provider-dept-8001/8002/8003(服务提供者集群)以及 micro-service-cloud-consumer-dept-80(服务消费者)。
使用浏览器连续访问“http://eureka7001.com/consumer/dept/get/1”,结果如下图。
图4:Ribbon 默认负载均衡策略
通过图 4 中 dbSource 字段取值的变化可以看出,Spring Cloud Ribbon 默认使用轮询策略进行负载均衡。
Spring Cloud Ribbon 默认使用轮询策略选取服务实例,我们也可以根据自身的需求切换负载均衡策略。
切换负载均衡策略的方法很简单,我们只需要在服务消费者(客户端)的配置类中,将 IRule 的其他实现类注入到容器中即可。
下面我们就通过一个实例,来演示下如何切换负载均衡的策略。
@Bean
public IRule myRule() {
// RandomRule 为随机策略
return new RandomRule();
}
图5:切换负载均衡策略为随机
通过图 5 中 dbSource 字段取值的变化可以看出,我们已经将负载均衡策略切换为 RandomRule(随机)。
通常情况下,Ribbon 提供的这些默认负载均衡策略是可以满足我们的需求的,如果有特殊的要求,我们还可以根据自身需求定制负载均衡策略。
下面我们就来演示下如何定制负载均衡策略。
package net.demo.myrule;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;
/**
* 定制 Ribbon 负载均衡策略
*/
public class MyRandomRule extends AbstractLoadBalancerRule {
private int total = 0; // 总共被调用的次数,目前要求每台被调用5次
private int currentIndex = 0; // 当前提供服务的机器号
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
//获取所有有效的服务实例列表
List<Server> upList = lb.getReachableServers();
//获取所有的服务实例的列表
List<Server> allList = lb.getAllServers();
//如果没有任何的服务实例则返回 null
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
//与随机策略相似,但每个服务实例只有在调用 3 次之后,才会调用其他的服务实例
if (total < 3) {
server = upList.get(currentIndex);
total++;
} else {
total = 0;
currentIndex++;
if (currentIndex >= upList.size()) {
currentIndex = 0;
}
}
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
server = null;
Thread.yield();
}
return server;
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
// TODO Auto-generated method stub
}
}
package net.demo.myrule;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 定制 Ribbon 负载均衡策略的配置类
* 该自定义 Ribbon 负载均衡策略配置类 不能在 net.demo.c 包及其子包下
* 否则所有的 Ribbon 客户端都会采用该策略,无法达到特殊化定制的目的
*/
@Configuration
public class MySelfRibbonRuleConfig {
@Bean
public IRule myRule() {
//自定义 Ribbon 负载均衡策略
return new MyRandomRule(); //自定义,随机选择某一个微服务,执行五次
}
}
package net.demo.c;
import net.demo.myrule.MySelfRibbonRuleConfig;
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
//自定义 Ribbon 负载均衡策略在主启动类上使用 RibbonClient 注解,在该微服务启动时,就能自动去加载我们自定义的 Ribbon 配置类,从而是配置生效
// name 为需要定制负载均衡策略的微服务名称(application name)
// configuration 为定制的负载均衡策略的配置类,
// 且官方文档中明确提出,该配置类不能在 ComponentScan 注解(SpringBootApplication 注解中包含了该注解)下的包或其子包中,即自定义负载均衡配置类不能在 net.demo.c 包及其子包下
@RibbonClient(name = "MICROSERVICECLOUDPROVIDERDEPT", configuration = MySelfRibbonRuleConfig.class)
public class MicroServiceCloudConsumerDept80Application {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudConsumerDept80Application.class, args);
}
}
图6:定制负载均衡策略
通过图 6 中 dbSource 字段取值的变化可以看出,我们定制的负载均衡策略已经生效。
Netflix Feign 是 Netflix 公司发布的一种实现负载均衡和服务调用的开源组件。Spring Cloud 将其与 Netflix 中的其他开源服务组件(例如 Eureka、Ribbon 以及 Hystrix 等)一起整合进 Spring Cloud Netflix 模块中,整合后全称为 Spring Cloud Netflix Feign。
Feign 对 Ribbon 进行了集成,利用 Ribbon 维护了一份可用服务清单,并通过 Ribbon 实现了客户端的负载均衡。
Feign 是一种声明式服务调用组件,它在 RestTemplate 的基础上做了进一步的封装。通过 Feign,我们只需要声明一个接口并通过注解进行简单的配置(类似于 Dao 接口上面的 Mapper 注解一样)即可实现对 HTTP 接口的绑定。
通过 Feign,我们可以像调用本地方法一样来调用远程服务,而完全感觉不到这是在进行远程调用。
Feign 支持多种注解,例如 Feign 自带的注解以及 JAX-RS 注解等,但遗憾的是 Feign 本身并不支持 Spring MVC 注解,这无疑会给广大 Spring 用户带来不便。
2019 年 Netflix 公司宣布 Feign 组件正式进入停更维护状态,于是 Spring 官方便推出了一个名为 OpenFeign 的组件作为 Feign 的替代方案。
OpenFeign 全称 Spring Cloud OpenFeign,它是 Spring 官方推出的一种声明式服务调用与负载均衡组件,它的出现就是为了替代进入停更维护状态的 Feign。
OpenFeign 是 Spring Cloud 对 Feign 的二次封装,它具有 Feign 的所有功能,并在 Feign 的基础上增加了对 Spring MVC 注解的支持,例如 @RequestMapping、@GetMapping 和 @PostMapping 等。
OpenFeign 常用注解
使用 OpenFegin 进行远程服务调用时,常用注解如下表。
注解 | 说明 |
---|---|
@FeignClient | 该注解用于通知 OpenFeign 组件对 @RequestMapping 注解下的接口进行解析,并通过动态代理的方式产生实现类,实现负载均衡和服务调用。 |
@EnableFeignClients | 该注解用于开启 OpenFeign 功能,当 Spring Cloud 应用启动时,OpenFeign 会扫描标有 @FeignClient 注解的接口,生成代理并注册到 Spring 容器中。 |
@RequestMapping | Spring MVC 注解,在 Spring MVC 中使用该注解映射请求,通过它来指定控制器(Controller)可以处理哪些 URL 请求,相当于 Servlet 中 web.xml 的配置。 |
@GetMapping | Spring MVC 注解,用来映射 GET 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.GET) 。 |
@PostMapping | Spring MVC 注解,用来映射 POST 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.POST) 。 |
Spring Cloud Finchley 及以上版本一般使用 OpenFeign 作为其服务调用组件。由于 OpenFeign 是在 2019 年 Feign 停更进入维护后推出的,因此大多数 2019 年及以后的新项目使用的都是 OpenFeign,而 2018 年以前的项目一般使用 Feign。
Feign VS OpenFeign
下面我们就来对比下 Feign 和 OpenFeign 的异同。
相同点
Feign 和 OpenFegin 具有以下相同点:
不同点
Feign 和 OpenFeign 具有以下不同:
下面我们就通过一个实例,来演示下通过 OpenFeign 是如何实现远程服务调用的。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>spring-cloud-demo2artifactId>
<groupId>net.biancheng.cgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>micro-service-cloud-consumer-dept-feignartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>micro-service-cloud-consumer-dept-feignname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>net.biancheng.cgroupId>
<artifactId>micro-service-cloud-apiartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
server:
port: 80
eureka:
client:
register-with-eureka: false #服务消费者可以不向服务注册中心注册服务
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
fetch-registry: true #服务消费者客户端需要去检索服务
package net.biancheng.c.service;
import net.biancheng.c.entity.Dept;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.List;
//添加为容器内的一个组件
@Component
// 服务提供者提供的服务名称,即 application.name
@FeignClient(value = "MICROSERVICECLOUDPROVIDERDEPT")
public interface DeptFeignService {
//对应服务提供者(8001、8002、8003)Controller 中定义的方法
@RequestMapping(value = "/dept/get/{id}", method = RequestMethod.GET)
public Dept get(@PathVariable("id") int id);
@RequestMapping(value = "/dept/list", method = RequestMethod.GET)
public List<Dept> list();
}
在编写服务绑定接口时,需要注意以下 2 点:
在 @FeignClient 注解中,value 属性的取值为:服务提供者的服务名,即服务提供者配置文件(application.yml)中 spring.application.name 的取值。
接口中定义的每个方法都与服务提供者(即 micro-service-cloud-provider-dept-8001 等)中 Controller 定义的服务方法对应。
package net.biancheng.c.controller;
import net.biancheng.c.entity.Dept;
import net.biancheng.c.service.DeptFeignService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
public class DeptController_Consumer {
@Resource
private DeptFeignService deptFeignService;
@RequestMapping(value = "/consumer/dept/get/{id}")
public Dept get(@PathVariable("id") Integer id) {
return deptFeignService.get(id);
}
@RequestMapping(value = "/consumer/dept/list")
public List<Dept> list() {
return deptFeignService.list();
}
}
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients //开启 OpenFeign 功能
public class MicroServiceCloudConsumerDeptFeignApplication {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudConsumerDeptFeignApplication.class, args);
}
}
Spring Cloud 应用在启动时,OpenFeign 会扫描标有 @FeignClient 注解的接口生成代理,并注人到 Spring 容器中。
图1:OpenFeign 实现远程服务调用
图2:OpenFeign 负载均衡
从图 2 可以看出,由于 OpenFeign 集成了 Ribbon,因此它也实现了客户端的负载均衡,其默认负载均衡策略为轮询策略。
OpenFeign 客户端的默认超时时间为 1 秒钟,如果服务端处理请求的时间超过 1 秒就会报错。为了避免这样的情况,我们需要对 OpenFeign 客户端的超时时间进行控制。
下面我们就通过一个实例,来演示 OpenFeign 是如何进行超时控制的。
//超时测试,该服务的响应时间为 5 秒
@RequestMapping(value = "/dept/feign/timeout")
public String DeptFeignTimeout() {
//暂停 5 秒
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return serverPort;
}
@RequestMapping(value = "/dept/feign/timeout")
public String DeptFeignTimeout();
@RequestMapping(value = "/consumer/dept/feign/timeout")
public String DeptFeignTimeout() {
// openFeign-ribbon 客户端一般默认等待一秒钟,超过该时间就会报错
return deptFeignService.DeptFeignTimeout();
}
图3:服务提供者的超时服务
图4:OpenFeign 超时报错
ribbon:
ReadTimeout: 6000 #建立连接所用的时间,适用于网络状况正常的情况下,两端两端连接所用的时间
ConnectionTimeout: 6000 #建立连接后,服务器读取到可用资源的时间
注:由于 OpenFeign 集成了 Ribbon ,其服务调用以及负载均衡在底层都是依靠 Ribbon 实现的,因此 OpenFeign 超时控制也是通过 Ribbon 来实现的。
图5:OpenFeign 超时控制
OpenFeign 提供了日志打印功能,我们可以通过配置调整日志级别,来了解请求的细节。
Feign 为每一个 FeignClient 都提供了一个 feign.Logger 实例,通过它可以对 OpenFeign 服务绑定接口的调用情况进行监控。
OpenFeign 日志打印功能的开启方式比较简单,下面我们就通过一个实例进行演示。
logging:
level:
#feign 日志以什么样的级别监控该接口
net.biancheng.c.service.DeptFeignService: debug
以上配置说明如下:
以上配置的含义就是,OpenFeign 以 debug 级别监控 net.biancheng.c.service.DeptFeignService 接口。
package net.biancheng.c.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfigBean {
/**
* OpenFeign 日志增强
* 配置 OpenFeign 记录哪些内容
*/
@Bean
Logger.Level feginLoggerLevel() {
return Logger.Level.FULL;
}
}
该配置的作用是通过配置的 Logger.Level 对象告诉 OpenFeign 记录哪些日志内容。
Logger.Level 的具体级别如下:
2021-10-12 14:33:07.408 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] ---> GET http://MICROSERVICECLOUDPROVIDERDEPT/dept/list HTTP/1.1
2021-10-12 14:33:07.408 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] ---> END HTTP (0-byte body)
2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] <--- HTTP/1.1 200 (574ms)
2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] connection: keep-alive
2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] content-type: application/json
2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] date: Tue, 12 Oct 2021 06:33:07 GMT
2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] keep-alive: timeout=60
2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] transfer-encoding: chunked
2021-10-12 14:33:07.983 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list]
2021-10-12 14:33:07.991 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] [{"deptNo":1,"deptName":"开发部","dbSource":"bianchengbang_jdbc"},{"deptNo":2,"deptName":"人事部","dbSource":"bianchengbang_jdbc"},{"deptNo":3,"deptName":"财务部","dbSource":"bianchengbang_jdbc"},{"deptNo":4,"deptName":"市场部","dbSource":"bianchengbang_jdbc"},{"deptNo":5,"deptName":"运维部","dbSource":"bianchengbang_jdbc"}]
2021-10-12 14:33:07.991 DEBUG 13388 --- [p-nio-80-exec-2] n.biancheng.c.service.DeptFeignService : [DeptFeignService#list] <--- END HTTP (341-byte body)
在微服务架构中,一个应用往往由多个服务组成,这些服务之间相互依赖,依赖关系错综复杂。
例如一个微服务系统中存在 A、B、C、D、E、F 等多个服务,它们的依赖关系如下图。
图1:服务依赖关系
通常情况下,一个用户请求往往需要多个服务配合才能完成。如图 1 所示,在所有服务都处于可用状态时,请求 1 需要调用 A、D、E、F 四个服务才能完成,请求 2 需要调用 B、E、D 三个服务才能完成,请求 3 需要调用服务 C、F、E、D 四个服务才能完成。
当服务 E 发生故障或网络延迟时,会出现以下情况:
即使其他所有服务都可用,由于服务 E 的不可用,那么用户请求 1、2、3 都会处于阻塞状态,等待服务 E 的响应。在高并发的场景下,会导致整个服务器的线程资源在短时间内迅速消耗殆尽。
所有依赖于服务 E 的其他服务,例如服务 B、D 以及 F 也都会处于线程阻塞状态,等待服务 E 的响应,导致这些服务的不可用。
所有依赖服务B、D 和 F 的服务,例如服务 A 和服务 C 也会处于线程阻塞状态,以等待服务 D 和服务 F 的响应,导致服务 A 和服务 C 也不可用。
从以上过程可以看出,当微服务系统的一个服务出现故障时,故障会沿着服务的调用链路在系统中疯狂蔓延,最终导致整个微服务系统的瘫痪,这就是“雪崩效应”。为了防止此类事件的发生,微服务架构引入了“熔断器”的一系列服务容错和保护机制。
熔断器(Circuit Breaker)一词来源物理学中的电路知识,它的作用是当线路出现故障时,迅速切断电源以保护电路的安全。
在微服务领域,熔断器最早是由 Martin Fowler 在他发表的 《Circuit Breaker》一文中提出。与物理学中的熔断器作用相似,微服务架构中的熔断器能够在某个服务发生故障后,向服务调用方返回一个符合预期的、可处理的降级响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常。这样就保证了服务调用方的线程不会被长时间、不必要地占用,避免故障在微服务系统中的蔓延,防止系统雪崩效应的发生。
Spring Cloud Hystrix 是一款优秀的服务容错与保护组件,也是 Spring Cloud 中最重要的组件之一。
Spring Cloud Hystrix 是基于 Netflix 公司的开源组件 Hystrix 实现的,它提供了熔断器功能,能够有效地阻止分布式微服务系统中出现联动故障,以提高微服务系统的弹性。Spring Cloud Hystrix 具有服务降级、服务熔断、线程隔离、请求缓存、请求合并以及实时故障监控等强大功能。
Hystrix [hɪst’rɪks],中文含义是豪猪,豪猪的背上长满了棘刺,使它拥有了强大的自我保护能力。而 Spring Cloud Hystrix 作为一个服务容错与保护组件,也可以让服务拥有自我保护的能力,因此也有人将其戏称为“豪猪哥”。
在微服务系统中,Hystrix 能够帮助我们实现以下目标:
监控功能:提供熔断器故障监控组件 Hystrix Dashboard,随时监控熔断器的状态。
Hystrix 提供了服务降级功能,能够保证当前服务不受其他服务故障的影响,提高服务的健壮性。
服务降级的使用场景有以下 2 种:
我们可以通过重写 HystrixCommand 的 getFallBack() 方法或 HystrixObservableCommand 的 resumeWithFallback() 方法,使服务支持服务降级。
Hystrix 服务降级 FallBack 既可以放在服务端进行,也可以放在客户端进行。
Hystrix 会在以下场景下进行服务降级处理:
下面我们就通过一个案例,分别演示下 Hystrix 服务端服务降级和客户端服务降级。
服务端服务降级
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>spring-cloud-demo2artifactId>
<groupId>net.biancheng.cgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>micro-service-cloud-provider-dept-hystrix-8004artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>micro-service-cloud-provider-dept-hystrix-8004name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
spring:
application:
name: microServiceCloudProviderDeptHystrix #微服务名称,对外暴漏的微服务名称,十分重要
server:
port: 8004
########################################### Spring cloud 自定义服务名称和 ip 地址###############################################
eureka:
client: #将客户端注册到 eureka 服务列表内
service-url:
#defaultZone: http://eureka7001:7001/eureka #这个地址是 7001注册中心在 application.yml 中暴露出来额注册地址 (单机版)
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #将服务注册到 Eureka 集群
instance:
instance-id: spring-cloud-provider-8004 #自定义服务名称信息
prefer-ip-address: true #显示访问路径的 ip 地址
#####################spring cloud 使用 Spring Boot actuator 监控完善信息###########################################
# Spring Boot 2.50对 actuator 监控屏蔽了大多数的节点,只暴露了 heath 节点,本段配置(*)就是为了开启所有的节点
management:
endpoints:
web:
exposure:
include: "*" # * 在yaml 文件属于关键字,所以需要加引号
info:
app.name: micro-service-cloud-provider-dept-hystrix
company.name: c.biancheng.net
build.aetifactId: @project.artifactId@
build.version: @project.version@
package net.biancheng.c.service;
public interface DeptService {
// hystrix 熔断器示例 ok
public String deptInfo_Ok(Integer id);
//hystrix 熔断器超时案例
public String deptInfo_Timeout(Integer id);
}
package net.biancheng.c.service.impl;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import net.biancheng.c.service.DeptService;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service("deptService")
public class DeptServiceImpl implements DeptService {
@Override
public String deptInfo_Ok(Integer id) {
return "线程池:" + Thread.currentThread().getName() + " deptInfo_Ok,id: " + id;
}
//一旦该方法失败并抛出了异常信息后,会自动调用 @HystrixCommand 注解标注的 fallbackMethod 指定的方法
@HystrixCommand(fallbackMethod = "dept_TimeoutHandler",
commandProperties =
//规定 5 秒钟以内就不报错,正常运行,超过 5 秒就报错,调用指定的方法
{@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")})
@Override
public String deptInfo_Timeout(Integer id) {
int outTime = 6;
try {
TimeUnit.SECONDS.sleep(outTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + " deptInfo_Timeout,id: " + id + " 耗时: " + outTime;
}
// 当服务出现故障后,调用该方法给出友好提示
public String dept_TimeoutHandler(Integer id) {
return "提醒您,系统繁忙请稍后再试!"+"线程池:" + Thread.currentThread().getName() + " deptInfo_Timeout,id: " + id;
}
}
我们可以看到 deptInfo_Timeout() 方法上使用 @HystrixCommand 注解,该注解说明如下:
参数 fallbackMethod 属性用于指定降级方法。
参数 execution.isolation.thread.timeoutInMilliseconds 用于设置自身调用超时时间的峰值,峰值内可以正常运行,否则执行降级方法
package net.biancheng.c.controller;
import lombok.extern.slf4j.Slf4j;
import net.biancheng.c.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
@RestController
@Slf4j
public class DeptController {
@Autowired
private DeptService deptService;
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "/dept/hystrix/ok/{id}")
public String deptInfo_Ok(@PathVariable("id") Integer id) {
String result = deptService.deptInfo_Ok(id);
log.info("端口号:" + serverPort + " result:" + result);
return result + ", 端口号:" + serverPort;
}
// Hystrix 服务超时降级
@RequestMapping(value = "/dept/hystrix/timeout/{id}")
public String deptInfo_Timeout(@PathVariable("id") Integer id) {
String result = deptService.deptInfo_Timeout(id);
log.info("端口号:" + serverPort + " result:" + result);
return result + ", 端口号:" + serverPort;
}
}
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient //开启 Eureka 客户端功能
@EnableCircuitBreaker //激活熔断器功能
public class MicroServiceCloudProviderDeptHystrix8004Application {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudProviderDeptHystrix8004Application.class, args);
}
}
图2:Hystrix 正常服务案例
图3:Hystrix 服务端服务降级
客户端服务降级
通常情况下,我们都会在客户端进行服务降级,当客户端调用的服务端的服务不可用时,客户端直接进行服务降级处理,避免其线程被长时间、不必要地占用。
客户端服务降级步骤如下。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
feign:
hystrix:
enabled: true #开启客户端 hystrix
package net.biancheng.c.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Component
@FeignClient(value = "MICROSERVICECLOUDPROVIDERDEPTHYSTRIX")
public interface DeptHystrixService {
@RequestMapping(value = "/dept/hystrix/ok/{id}")
public String deptInfo_Ok(@PathVariable("id") Integer id);
@RequestMapping(value = "/dept/hystrix/timeout/{id}")
public String deptInfo_Timeout(@PathVariable("id") Integer id);
}
package net.biancheng.c.controller;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;;
import lombok.extern.slf4j.Slf4j;
import net.biancheng.c.service.DeptHystrixService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Slf4j
@RestController
public class HystrixController_Consumer {
@Resource
private DeptHystrixService deptHystrixService;
@RequestMapping(value = "/consumer/dept/hystrix/ok/{id}")
public String deptInfo_Ok(@PathVariable("id") Integer id) {
return deptHystrixService.deptInfo_Ok(id);
}
//在客户端进行降级
@RequestMapping(value = "/consumer/dept/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "dept_TimeoutHandler") //为该请求指定专属的回退方法
public String deptInfo_Timeout(@PathVariable("id") Integer id) {
String s = deptHystrixService.deptInfo_Timeout(id);
log.info(s);
return s;
}
// deptInfo_Timeout方法的 专用 fallback 方法
public String dept_TimeoutHandler(@PathVariable("id") Integer id) {
log.info("deptInfo_Timeout 出错,服务已被降级!");
return "提醒您:服务端系统繁忙,请稍后再试!(客户端 deptInfo_Timeout 专属的回退方法触发)";
}
}
######################### Ribbon 客户端超时控制 ###################################
ribbon:
ReadTimeout: 6000 #建立连接所用的时间,适用于网络状况正常的情况下,两端两端连接所用的时间
ConnectionTimeout: 6000 #建立连接后,服务器读取到可用资源的时间
######################配置请求超时时间##########################
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 7000
####################配置具体方法超时时间 为 3 秒########################
DeptHystrixService#deptInfo_Timeout(Integer):
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
在配置文件中设计请求的超时时间时,需要注意以下 2 点:
1)Hystrix 可以来为所有请求(方法)设置超时时间(单位为毫秒),若请求超时则触发全局的回退方法进行处理。
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=mmm
2)Hystrix 还可以为某个特定的服务请求(方法)设置超时时间,格式如下:
hystrix.command.xxx#yyy(zzz).execution.isolation.thread.timeoutInMilliseconds=mmm
格式说明如下:
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients //开启 OpenFeign 功能
@EnableHystrix //启用 Hystrix
public class MicroServiceCloudConsumerDeptFeignApplication {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudConsumerDeptFeignApplication.class, args);
}
}
@HystrixCommand(fallbackMethod = "dept_TimeoutHandler",
commandProperties =
//规定 5 秒钟以内就不报错,正常运行,超过 5 秒就报错,调用指定的方法
{@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")})
@Override
public String deptInfo_Timeout(Integer id) {
int outTime = 4;
try {
TimeUnit.SECONDS.sleep(outTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + " deptInfo_Timeout,id: " + id + " 耗时: " + outTime;
}
图4:Hystrix 服务端请求正常
图5:Hystrix 客户端服务降级
由图 5 可以看出,由于服务请求的耗时为 4 秒,超过了客户端为该请求指定的超时时间 (3 秒 ),因此该服务被降级处理,触发了其指定的回退方法。
全局降级方法
通过上面的方式实现服务降级时,需要针对所有业务方法都配置降级方法,这极有可能会造成代码的急剧膨胀。为了解决该问题,我们还可以为所有业务方法指定一个全局的回退方法,具体步骤如下。
@Slf4j
@RestController
@DefaultProperties(defaultFallback = "dept_Global_FallbackMethod") //全局的服务降级方法
public class HystrixController_Consumer {
……
}
/**
* 全局的 fallback 方法,
* 回退方法必须和 hystrix 的执行方法在相同类中
* @DefaultProperties(defaultFallback = "dept_Global_FallbackMethod") 类上注解,请求方法上使用 @HystrixCommand 注解
*/
public String dept_Global_FallbackMethod() {
return "提醒您,运行出错或服务端系统繁忙,请稍后再试!(客户端全局回退方法触发,)";
}
注意:降级(FallBack)方法必须与其对应的业务方法在同一个类中,否则无法生效。
//在客户端进行降级
@RequestMapping(value = "/consumer/dept/hystrix/timeout/{id}")
@HystrixCommand
public String deptInfo_Timeout(@PathVariable("id") Integer id) {
String s = deptHystrixService.deptInfo_Timeout(id);
log.info(s);
return s;
}
注意:全局降级方法的优先级较低,只有业务方法没有指定其降级方法时,服务降级时才会触发全局回退方法。若业务方法指定它自己的回退方法,那么在服务降级时,就只会直接触发它自己的回退方法,而非全局回退方法。
图6:全局回退方法
不管是业务方法指定的降级方法还是全局降级方法,它们都必须和业务方法在同一个类中才能生效,业务逻辑与降级逻辑耦合度极高。
下面我们对业务逻辑与降级逻辑进行解耦,操作步骤如下。
package net.biancheng.c.service;
import org.springframework.stereotype.Component;
/**
* Hystrix 服务降级
* 解耦回退逻辑
*/
@Component
public class DeptHystrixFallBackService implements DeptHystrixService {
@Override
public String deptInfo_Ok(Integer id) {
return "--------------------提醒您,系统繁忙,请稍后重试!(解耦回退方法触发)-----------------------";
}
@Override
public String deptInfo_Timeout(Integer id) {
return "--------------------提醒您,系统繁忙,请稍后重试!(解耦回退方法触发)-----------------------";
}
}
注意:该类必须以组件的形式添加 Spring 容器中才能生效,最常用的方式就是在类上标注 @Component 注解。
package net.biancheng.c.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Component
@FeignClient(value = "MICROSERVICECLOUDPROVIDERDEPTHYSTRIX", fallback = DeptHystrixFallBackService.class)
public interface DeptHystrixService {
@RequestMapping(value = "/dept/hystrix/ok/{id}")
public String deptInfo_Ok(@PathVariable("id") Integer id);
@RequestMapping(value = "/dept/hystrix/timeout/{id}")
public String deptInfo_Timeout(@PathVariable("id") Integer id);
}
图7:Hystrix 解耦回退方法
熔断机制是为了应对雪崩效应而出现的一种微服务链路保护机制。
当微服务系统中的某个微服务不可用或响应时间太长时,为了保护系统的整体可用性,熔断器会暂时切断请求对该服务的调用,并快速返回一个友好的错误响应。这种熔断状态不是永久的,在经历了一定的时间后,熔断器会再次检测该微服务是否恢复正常,若服务恢复正常则恢复其调用链路。
熔断状态
在熔断机制中涉及了三种熔断状态:
三种熔断状态之间的转化关系如下图:
图8:三种熔断状态转换
在 Spring Cloud 中,熔断机制是通过 Hystrix 实现的。Hystrix 会监控微服务间调用的状况,当失败调用到一定比例时(例如 5 秒内失败 20 次),就会启动熔断机制。
Hystrix 实现服务熔断的步骤如下:
当服务的调用出错率达到或超过 Hystix 规定的比率(默认为 50%)后,熔断器进入熔断开启状态。
熔断器进入熔断开启状态后,Hystrix 会启动一个休眠时间窗,在这个时间窗内,该服务的降级逻辑会临时充当业务主逻辑,而原来的业务主逻辑不可用。
当有请求再次调用该服务时,会直接调用降级逻辑快速地返回失败响应,以避免系统雪崩。
当休眠时间窗到期后,Hystrix 会进入半熔断转态,允许部分请求对服务原来的主业务逻辑进行调用,并监控其调用成功率。
如果调用成功率达到预期,则说明服务已恢复正常,Hystrix 进入熔断关闭状态,服务原来的主业务逻辑恢复;否则 Hystrix 重新进入熔断开启状态,休眠时间窗口重新计时,继续重复第 2 到第 5 步。
示例
下面我们就通过一个实例来验证下 Hystrix 是如何实现熔断机制的。
在 micro-service-cloud-provider-dept-hystrix-8004 中的 DeptService 接口中添加一个 deptCircuitBreaker() 方法,代码如下。
package net.biancheng.c.service;
public interface DeptService {
// hystrix 熔断器示例 ok
public String deptInfo_Ok(Integer id);
//hystrix 熔断器超时案例
public String deptInfo_Timeout(Integer id);
// Hystrix 熔断机制案例
public String deptCircuitBreaker(Integer id);
}
//Hystrix 熔断案例
@Override
@HystrixCommand(fallbackMethod = "deptCircuitBreaker_fallback", commandProperties = {
//以下参数在 HystrixCommandProperties 类中有默认配置
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), //是否开启熔断器
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value = "1000"), //统计时间窗
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), //统计时间窗内请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), //休眠时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), //在统计时间窗口期以内,请求失败率达到 60% 时进入熔断状态
})
public String deptCircuitBreaker(Integer id) {
if (id < 0) {
//当传入的 id 为负数时,抛出异常,调用降级方法
throw new RuntimeException("提醒您,id 不能是负数!");
}
String serialNum = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号为:" + serialNum;
}
//deptCircuitBreaker 的降级方法
public String deptCircuitBreaker_fallback(Integer id) {
return "提醒您,id 不能是负数,请稍后重试!\t id:" + id;
}
在以上代码中,共涉及到了 4 个与 Hystrix 熔断机制相关的重要参数,这 4 个参数的含义如下表。
参数 | 描述 |
---|---|
metrics.rollingStats.timeInMilliseconds | 统计时间窗。 |
circuitBreaker.sleepWindowInMilliseconds | 休眠时间窗,熔断开启状态持续一段时间后,熔断器会自动进入半熔断状态,这段时间就被称为休眠窗口期。 |
circuitBreaker.requestVolumeThreshold | 请求总数阀值。 |
在统计时间窗内,请求总数必须到达一定的数量级,Hystrix 才可能会将熔断器打开进入熔断开启转态,而这个请求数量级就是 请求总数阀值。Hystrix 请求总数阈值默认为 20,这就意味着在统计时间窗内,如果服务调用次数不足 20 次,即使所有的请求都调用出错,熔断器也不会打开。
circuitBreaker.errorThresholdPercentage 错误百分比阈值。
当请求总数在统计时间窗内超过了请求总数阀值,且请求调用出错率超过一定的比例,熔断器才会打开进入熔断开启转态,而这个比例就是错误百分比阈值。错误百分比阈值设置为 50,就表示错误百分比为 50%,如果服务发生了 30 次调用,其中有 15 次发生了错误,即超过了 50% 的错误百分比,这时候将熔断器就会打开。
3. 在 DeptController 中添加一个 deptCircuitBreaker() 方法对外提供服务,代码如下。
// Hystrix 服务熔断
@RequestMapping(value = "/dept/hystrix/circuit/{id}")
public String deptCircuitBreaker(@PathVariable("id") Integer id){
String result = deptService.deptCircuitBreaker(id);
log.info("result:"+result);
return result;
}
图9:Hystrix 实现熔断机制 调用正确示例
图10:Hystrix 实现熔断机制 错误调用
图11:Hystrix 熔断开启状态传入正数
通过图 11 可以看到,在熔断开启状态下,即使我们传入的参数已经是正数,调用的依然降级逻辑。
图12:Hystrix 进入熔断关闭状态
通过图 12 可以看出,当服务调用正确率上升到一定的利率后,Hystrix 进入熔断关闭状态。
Hystrix 还提供了准实时的调用监控(Hystrix Dashboard)功能,Hystrix 会持续地记录所有通过 Hystrix 发起的请求的执行信息,并以统计报表的形式展示给用户,包括每秒执行请求的数量、成功请求的数量和失败请求的数量等。
下面我们就通过一个实例来搭建 Hystrix Dashboard,监控 micro-service-cloud-provider-dept-hystrix-8004 的运行情况。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>spring-cloud-demo2artifactId>
<groupId>net.biancheng.cgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>micro-service-cloud-consumer-dept-hystrix-dashboard-9002artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>micro-service-cloud-consumer-dept-hystrix-dashboard-9002name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
server:
port: 9002 #端口号
#http://eureka7001.com:9002/hystrix 熔断器监控页面
# localhost:8004//actuator/hystrix.stream 监控地址
hystrix:
dashboard:
proxy-stream-allow-list:
- "localhost"
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@SpringBootApplication
@EnableHystrixDashboard
public class MicroServiceCloudConsumerDeptHystrixDashboard9002Application {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudConsumerDeptHystrixDashboard9002Application.class, args);
}
}
package net.biancheng.c.config;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HystrixDashboardConfig {
/**
* Hystrix dashboard 监控界面必须配置
* @return
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/actuator/hystrix.stream");//访问路径
registrationBean.setName("hystrix.stream");
return registrationBean;
}
}
图13:Hystrix 监控页面
图14:Hystrix 监控信息
图15:Hystrix 监控微服务运行情况
图16:Hystrix 监控服务运行情况
在微服务架构中,一个系统往往由多个微服务组成,而这些服务可能部署在不同机房、不同地区、不同域名下。这种情况下,客户端(例如浏览器、手机、软件工具等)想要直接请求这些服务,就需要知道它们具体的地址信息,例如 IP 地址、端口号等。
这种客户端直接请求服务的方式存在以下问题:
我们可以通过 API 网关来解决这些问题,下面就让我们来看看什么是 API 网关。
API 网关是一个搭建在客户端和微服务之间的服务,我们可以在 API 网关中处理一些非业务功能的逻辑,例如权限验证、监控、缓存、请求路由等。
API 网关就像整个微服务系统的门面一样,是系统对外的唯一入口。有了它,客户端会先将请求发送到 API 网关,然后由 API 网关根据请求的标识信息将请求转发到微服务实例。
图1:两种服务访问方式对比
对于服务数量众多、复杂度较高、规模比较大的系统来说,使用 API 网关具有以下好处:
常见的 API 网关实现方案主要有以下 5 种:
本节,我们就对 Spring Cloud Gateway 进行详细介绍。
Spring Cloud Gateway 是 Spring Cloud 团队基于 Spring 5.0、Spring Boot 2.0 和 Project Reactor 等技术开发的高性能 API 网关组件。
Spring Cloud Gateway 旨在提供一种简单而有效的途径来发送 API,并为它们提供横切关注点,例如:安全性,监控/指标和弹性。
Spring Cloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。
Spring Cloud GateWay 最主要的功能就是路由转发,而在定义转发规则时主要涉及了以下三个核心概念,如下表。
核心概念 | 描述 |
---|---|
Route(路由) | 网关最基本的模块。它由一个 ID、一个目标 URI、一组断言(Predicate)和一组过滤器(Filter)组成。 |
Predicate(断言) | 路由转发的判断条件,我们可以通过 Predicate 对 HTTP 请求进行匹配,例如请求方式、请求路径、请求头、参数等,如果请求与断言匹配成功,则将请求转发到相应的服务。 |
Filter(过滤器) | 过滤器,我们可以使用它对请求进行拦截和修改,还可以使用它对上文的响应进行再处理。 |
注意:其中 Route 和 Predicate 必须同时声明。
Spring Cloud Gateway 具有以下特性:
Spring Cloud Gateway 工作流程如下图。
图2:Spring Cloud Gateway 工作流程
Spring Cloud Gateway 工作流程说明如下:
总而言之,客户端发送到 Spring Cloud Gateway 的请求需要通过一定的匹配条件,才能定位到真正的服务节点。在将请求转发到服务进行处理的过程前后(pre 和 post),我们还可以对请求和响应进行一些精细化控制。
Predicate 就是路由的匹配条件,而 Filter 就是对请求和响应进行精细化控制的工具。有了这两个元素,再加上目标 URI,就可以实现一个具体的路由了。
Spring Cloud Gateway 通过 Predicate 断言来实现 Route 路由的匹配规则。简单点说,Predicate 是路由转发的判断条件,请求只有满足了 Predicate 的条件,才会被转发到指定的服务上进行处理。
使用 Predicate 断言需要注意以下 3 点:
图3:Predicate 断言匹配
常见的 Predicate 断言如下表(假设转发的 URI 为 http://localhost:8001)。
断言 | 示例 | 说明 |
---|---|---|
Path | - Path=/dept/list/** | 当请求路径与 /dept/list/** 匹配时,该请求才能被转发到 http://localhost:8001 上。 |
Before | - Before=2021-10-20T11:47:34.255+08:00[Asia/Shanghai] | 在 2021 年 10 月 20 日 11 时 47 分 34.255 秒之前的请求,才会被转发到 http://localhost:8001 上。 |
After | - After=2021-10-20T11:47:34.255+08:00[Asia/Shanghai] | 在 2021 年 10 月 20 日 11 时 47 分 34.255 秒之后的请求,才会被转发到 http://localhost:8001 上。 |
Between | - Between=2021-10-20T15:18:33.226+08:00[Asia/Shanghai],2021-1020T15:23:33.226+08:00[Asia/Shanghai] | 在 2021 年 10 月 20 日 15 时 18 分 33.226 秒 到 2021 年 10 月 20 日 15 时 23 分 33.226 秒之间的请求,才会被转发到 http://localhost:8001 服务器上。 |
Cookie | - Cookie=name,c.biancheng.net | 携带 Cookie 且 Cookie 的内容为 name=c.biancheng.net 的请求,才会被转发到 http://localhost:8001 上。 |
Header | - Header=X-Request-Id,\d+ | 请求头上携带属性 X-Request-Id 且属性值为整数的请求,才会被转发到 http://localhost:8001 上。 |
Method | - Method=GET 只有 GET 请求才会被转发到 http://localhost:8001 上。 |
下面我们就通过一个实例,来演示下 Predicate 是如何使用的。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>spring-cloud-demo2artifactId>
<groupId>net.biancheng.cgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>micro-service-cloud-gateway-9527artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>micro-service-cloud-gateway-9527name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
server:
port: 9527 #端口号
spring:
application:
name: microServiceCloudGateway
cloud:
gateway: #网关路由配置
routes:
#将 micro-service-cloud-provider-dept-8001 提供的服务隐藏起来,不暴露给客户端,只给客户端暴露 API 网关的地址 9527
- id: provider_dept_list_routh #路由 id,没有固定规则,但唯一,建议与服务名对应
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
#以下是断言条件,必选全部符合条件
- Path=/dept/list/** #断言,路径匹配 注意:Path 中 P 为大写
- Method=GET #只能时 GET 请求时,才能访问
eureka:
instance:
instance-id: micro-service-cloud-gateway-9527
hostname: micro-service-cloud-gateway
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
以上配置中,我们在 spring.cloud.gateway.routes 下使用 predicates 属性,定义了以下两个断言条件:
只有当外部(客户端)发送到 micro-service-cloud-gateway-9527 的 HTTP 请求同时满足以上所有的断言时,该请求才会被转发到指定的服务端中(即 http://localhost:8001)。
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class MicroServiceCloudGateway9527Application {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudGateway9527Application.class, args);
}
}
图4:Spring Cloud Gateway 路由转发
默认情况下,Spring Cloud Gateway 会根据服务注册中心(例如 Eureka Server)中维护的服务列表,以服务名(spring.application.name)作为路径创建动态路由进行转发,从而实现动态路由功能。
我们可以在配置文件中,将 Route 的 uri 地址修改为以下形式。
lb://service-name
以上配置说明如下:
下面我们就通过一个实例,来展示下 Spring Cloud Gateway 是如何实现动态路由的。
server:
port: 9527 #端口号
spring:
application:
name: microServiceCloudGateway #服务注册中心注册的服务名
cloud:
gateway: #网关路由配置
discovery:
locator:
enabled: true #默认值为 true,即默认开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
#将 micro-service-cloud-provider-dept-8001 提供的服务隐藏起来,不暴露给客户端,只给客户端暴露 API 网关的地址 9527
- id: provider_dept_list_routh #路由 id,没有固定规则,但唯一,建议与服务名对应
uri: lb://MICROSERVICECLOUDPROVIDERDEPT #动态路由,使用服务名代替上面的具体带端口 http://eureka7001.com:9527/dept/list
predicates:
#以下是断言条件,必选全部符合条件
- Path=/dept/list/** #断言,路径匹配 注意:Path 中 P 为大写
- Method=GET #只能时 GET 请求时,才能访问
eureka:
instance:
instance-id: micro-service-cloud-gateway-9527
hostname: micro-service-cloud-gateway
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
依次启动 Eureka 服务注册中心(集群)、服务提供者集群(micro-service-cloud-provider-dept-8001/8002/8003)以及 micro-service-cloud-gateway-9527。
在浏览器中访问“http://localhost:9527/dept/list”,结果如下图。
图5:Spring Cloud 实现动态路由
通常情况下,出于安全方面的考虑,服务端提供的服务往往都会有一定的校验逻辑,例如用户登陆状态校验、签名校验等。
在微服务架构中,系统由多个微服务组成,所有这些服务都需要这些校验逻辑,此时我们就可以将这些校验逻辑写到 Spring Cloud Gateway 的 Filter 过滤器中。
Filter 的分类
Spring Cloud Gateway 提供了以下两种类型的过滤器,可以对请求和响应进行精细化控制。
过滤器类型 | 说明 |
---|---|
Pre 类型 | 这种过滤器在请求被转发到微服务之前可以对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。 |
Post 类型 | 这种过滤器在微服务对请求做出响应后可以对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等。 |
按照作用范围划分,Spring Cloud gateway 的 Filter 可以分为 2 类:
GatewayFilter 是 Spring Cloud Gateway 网关中提供的一种应用在单个或一组路由上的过滤器。它可以对单个路由或者一组路由上传入的请求和传出响应进行拦截,并实现一些与业务无关的功能,比如登陆状态校验、签名校验、权限校验、日志输出、流量监控等。
GatewayFilter 在配置文件(例如 application.yml)中的写法与 Predicate 类似,格式如下。
spring:
cloud:
gateway:
routes:
- id: xxxx
uri: xxxx
predicates:
- Path=xxxx
filters:
- AddRequestParameter=X-Request-Id,1024 #过滤器工厂会在匹配的请求头加上一对请求头,名称为 X-Request-Id 值为 1024
- PrefixPath=/dept #在请求路径前面加上 /dept
……
Spring Cloud Gateway 内置了多达 31 种 GatewayFilter,下表中列举了几种常用的网关过滤器及其使用示例。
路由过 | 滤器 | 描述 参数 | 使用示例 |
---|---|---|---|
AddRequestHeader | 拦截传入的请求,并在请求上添加一个指定的请求头参数。 | name:需要添加的请求头参数的 key;value:需要添加的请求头参数的 value。 | - AddRequestHeader=my-request-header,1024 |
AddRequestParameter | 拦截传入的请求,并在请求上添加一个指定的请求参数。 | name:需要添加的请求参数的 key;value:需要添加的请求参数的 value。 | - AddRequestParameter=my-request-param,c.biancheng.net |
AddResponseHeader | 拦截响应,并在响应上添加一个指定的响应头参数。 | name:需要添加的响应头的 key;value:需要添加的响应头的 value。 | - AddResponseHeader=my-response-header,c.biancheng.net |
PrefixPath | 拦截传入的请求,并在请求路径增加一个指定的前缀。 | prefix:需要增加的路径前缀。 | - PrefixPath=/consumer |
PreserveHostHeader | 转发请求时,保持客户端的 Host 信息不变,然后将它传递到提供具体服务的微服务中。 | 无 | - PreserveHostHeader |
RemoveRequestHeader | 移除请求头中指定的参数。 | name:需要移除的请求头的 key。 | - RemoveRequestHeader=my-request-header |
RemoveResponseHeader | 移除响应头中指定的参数。 | name:需要移除的响应头。 | - RemoveResponseHeader=my-response-header |
RemoveRequestParameter | 移除指定的请求参数。 | name:需要移除的请求参数。 | - RemoveRequestParameter=my-request-param |
RequestSize | 配置请求体的大小,当请求体过大时,将会返回 413 Payload Too Large。 | maxSize:请求体的大小。 | - name: RequestSizeargs: maxSize: 5000000 |
示例
下面我们通过一个实例来演示 GatewayFilter 的配置,步骤如下。
- id: provider_dept_get_routh
uri: lb://MICROSERVICECLOUDPROVIDERDEPT #使用服务名代替上面的具体带端口
predicates:
- Path=/get/**
filters:
- PrefixPath=/dept #在请求路径上增加一个前缀 /dept
图6:路由过滤器示例
GlobalFilter 是一种作用于所有的路由上的全局过滤器,通过它,我们可以实现一些统一化的业务功能,例如权限认证、IP 访问限制等。当某个请求被路由匹配时,那么所有的 GlobalFilter 会和该路由自身配置的 GatewayFilter 组合成一个过滤器链。
Spring Cloud Gateway 为我们提供了多种默认的 GlobalFilter,例如与转发、路由、负载均衡等相关的全局过滤器。但在实际的项目开发中,通常我们都会自定义一些自己的 GlobalFilter 全局过滤器以满足我们自身的业务需求,而很少直接使用 Spring Cloud Config 提供这些默认的 GlobalFilter。
关于默认的全局过滤器的详细内容,请参考 Spring Cloud 官网。
下面我们就通过一个实例来演示下,如何自定义 GlobalFilter 全局过滤器。
package net.biancheng.c.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Date;
/**
* 自定义全局网关过滤器(GlobalFilter)
*/
@Component
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("进入自定义的全局过滤器 MyGlobalFilter" + new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (uname == null) {
log.info("参数 uname 不能为 null!");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
//过滤器的顺序,0 表示第一个
return 0;
}
}
2021-10-21 16:25:39.450 INFO 19116 --- [ctor-http-nio-4] net.biancheng.c.filter.MyGlobalFilter : Thu Oct 21 16:25:39 CST 2021进入自定义的全局过滤器 MyGlobalFilter
2021-10-21 16:25:39.451 INFO 19116 --- [ctor-http-nio-4] net.biancheng.c.filter.MyGlobalFilter : 参数 uname 不能为 null!
图7:自定义全局网关过滤器
在分布式微服务系统中,几乎所有服务的运行都离不开配置文件的支持,这些配置文件通常由各个服务自行管理,以 properties 或 yml 格式保存在各个微服务的类路径下,例如 application.properties 或 application.yml 等。
这种将配置文件散落在各个服务中的管理方式,存在以下问题:
为了解决这些问题,通常我们都会使用配置中心对配置进行统一管理。市面上开源的配置中心有很多,例如百度的 Disconf、淘宝的 diamond、360 的 QConf、携程的 Apollo 等都是解决这类问题的。Spring Cloud 也有自己的分布式配置中心,那就是 Spring Cloud Config。
Spring Cloud Config 是由 Spring Cloud 团队开发的项目,它可以为微服务架构中各个微服务提供集中化的外部配置支持。
简单点说就是,Spring Cloud Config 可以将各个微服务的配置文件集中存储在一个外部的存储仓库或系统(例如 Git 、SVN 等)中,对配置的统一管理,以支持各个微服务的运行。
Spring Cloud Config 包含以下两个部分:
Config Server:也被称为分布式配置中心,它是一个独立运行的微服务应用,用来连接配置仓库并为客户端提供获取配置信息、加密信息和解密信息的访问接口。
Config Client:指的是微服务架构中的各个微服务,它们通过 Config Server 对配置进行管理,并从 Config Sever 中获取和加载配置信息。
Spring Cloud Config 默认使用 Git 存储配置信息,因此使用 Spirng Cloud Config 构建的配置服务器天然就支持对微服务配置的版本管理。我们可以使用 Git 客户端工具方便地对配置内容进行管理和访问。除了 Git 外,Spring Cloud Config 还提供了对其他存储方式的支持,例如 SVN、本地化文件系统等。
图1:Spring Cloud Config 工作原理
Spring Cloud Config 工作流程如下:
Spring Cloud Config 具有以下特点:
下面我们就通过几个实例来演示 Spring Cloud Config 的使用。
在 Github 上创建一个名为 springcloud-config 的仓库(Repository)并获取该仓库的地址。由于 Github 站点对于国内用户来说不够稳定,很可能存在加载缓慢等问题,因此我们可以在码云上执行该操作。
在父工程 spring-cloud-demo2 下,创建一个名为 micro-service-cloud-config-center-3344 的 Spring Boot 模块,并在 pom.xml 中添加 Spring Cloud Config 服务端依赖,配置如下。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-cloud-demo2</artifactId>
<groupId>net.biancheng.c</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>net.biancheng.c</groupId>
<artifactId>micro-service-cloud-config-center-3344</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>micro-service-cloud-config-center-3344</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--配置中心服务器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
server:
port: 3344 #端口号
spring:
application:
name: spring-cloud-config-center #服务名
cloud:
config:
server:
git:
## Git 地址,https://gitee.com/java-mohan/springcloud-config.git
## 码云(gitee)地址 uri: https://github.com/javmohan/springcloud-config.git (github 站点访问较慢,因此这里我们使用 gitee)
uri: https://gitee.com/java-mohan/springcloud-config.git
#仓库名
search-paths:
- springcloud-config
force-pull: true
## 如果Git仓库为公开仓库,可以不填写用户名和密码,如果是私有仓库需要填写
## username: ********
## password: ********
#分支名
label: master
eureka:
client: #将客户端注册到 eureka 服务列表内
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #将服务注册到 Eureka 集群
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class MicroServiceCloudConfigCenter3344Application {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudConfigCenter3344Application.class, args);
}
}
config:
info: c.biancheng.net
version: 1.0
图2:访问配置文件
Spring Cloud Config 规定了一套配置文件访问规则,如下表。
访问规则 | 示例 |
---|---|
/{application}/{profile}[/{label}] | /config/dev/master |
/{application}-{profile}.{suffix} | /config-dev.yml |
/{label}/{application}-{profile}.{suffix} | /master/config-dev.yml |
访问规则内各参数说明如下。
通过这套规则,我们在浏览器上就直接对配置文件进行访问。
图3:Spring Cloud Config 访问配置文件
{"name":"config","profiles":["dev"],"label":"master","version":"9caafcc3498e04147463482f8b29e925e8afcc3a","state":null,"propertySources":[{"name":"https://gitee.com/java-mohan/springcloud-config.git/config-dev.yml","source":{"config.info":"c.biancheng.net","config.version":1.0}}]}
自此我们就完成了对 Spring Cloud Config 服务端的搭建和测试。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>spring-cloud-demo2artifactId>
<groupId>net.biancheng.cgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>micro-service-cloud-config-client-3355artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>micro-service-cloud-config-client-3355name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
server:
port: 3355 #端口号
spring:
application:
name: spring-cloud-config-client #服务名
cloud:
config:
label: master #分支名称
name: config #配置文件名称,config-dev.yml 中的 config
profile: dev #环境名 config-dev.yml 中的 dev
#这里不要忘记添加 http:// 否则无法读取
uri: http://localhost:3344 #Spring Cloud Config 服务端(配置中心)地址
eureka:
client: #将客户端注册到 eureka 服务列表内
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #将服务注册到 Eureka 集群
package net.biancheng.c.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
//读取配置中心指定配置文件的内容,并展示到页面
@RestController
public class ConfigClientController {
@Value("${server.port}")
private String serverPort;
@Value("${config.info}")
private String configInfo;
@Value("${config.version}")
private String configVersion;
@GetMapping(value = "/getConfig")
public String getConfig() {
return "info:" + configInfo + "
version:" + configVersion + "
port:" + serverPort;
}
}
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class MicroServiceCloudConfigClient3355Application {
public static void main(String[] args) {
SpringApplication.run(MicroServiceCloudConfigClient3355Application.class, args);
}
}
图4:Spring Cloud Config 客户端获取配置信息
config:
info: c.biancheng.net
version: 2.0
图5:配置中心获取修改后的配置文件
从图 6 可以看出,配置中心已经成功地获取到了修改后的配置。
图6:Spring Cloud Config 客户端获取修改后的配置信息
图7:重启 Spring Cloud Config 客户端获取配置
通过该实例,我们可以得到以下 2 点结论,
为了解决不重启 Config 客户端无法获取最新配置的问题,接下来,我们就对 micro-service-cloud-config-client-3355 进行改造,改造步骤如下。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
## Spring Boot 2.50对 actuator 监控屏蔽了大多数的节点,只暴露了 health 节点,本段配置(*)就是为了开启所有的节点
management:
endpoints:
web:
exposure:
include: "*" ## * 在yaml 文件属于关键字,所以需要加引号
package net.biancheng.c.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
// 读取配置中心指定配置文件的内容,并展示到页面
@RefreshScope //为了让动态(手动)的获取最新的git 配置,在添加 actuator 监控加载 RefreshScope,
@RestController
public class ConfigClientController {
@Value("${server.port}")
private String serverPort;
@Value("${config.info}")
private String configInfo;
@Value("${config.version}")
private String configVersion;
@GetMapping(value = "/getConfig")
public String getConfig() {
return "info:" + configInfo + "
version:" + configVersion + "
port:" + serverPort;
}
}
config:
info: c.biancheng.net
version: 3.0
图8:重启 Spring Cloud Config 客户端后获取配置
从图 9 可以看出,即使我们对 Spring Cloud Config 客户端进行了改造,也依然无法直接获取到最新配置。
curl -X POST "http://localhost:3355/actuator/refresh"
图9:手动刷新客户端
在上面的实例中,我们通过在 Config 客户端(端口号:3355)中引入 Spring Boot actuator 监控组件来监控配置的变化,使我们可以在不重启 Config 客户端的情况下获取到了最新配置,原理如下图。
图10:Spring Cloud Congfig 手动刷新
这种方式虽然解决了重启 Config 客户端才能获取最新配置的问题,但另一个问题却也接踵而至,那就是只要配置仓库中的配置发生改变,就需要我们挨个向 Config 客户端手动发送 POST 请求,通知它们重新拉取配置。
我们知道,所谓的 Config 客户端其实就是一个一个的服务。在微服务架构中,一个系统往往包含十几甚至几十个服务,如果因为某一个配置文件的修改而向几十个微服务发送 POST 请求,这显然是不合理的。
那么有没有“一次通知,处处生效”的方式呢?答案是肯定的。Spring Cloud Config 配合 Bus 就可以实现配置的动态刷新。
Spring Cloud Bus 又被称为消息总线,它能够通过轻量级的消息代理(例如 RabbitMQ、Kafka 等)将微服务架构中的各个服务连接起来,实现广播状态更改、事件推送等功能,还可以实现微服务之间的通信功能。
目前 Spring Cloud Bus 支持两种消息代理:RabbitMQ 和 Kafka。
Spring Cloud Bus 会使用一个轻量级的消息代理来构建一个公共的消息主题 Topic(默认为“springCloudBus”),这个 Topic 中的消息会被所有服务实例监听和消费。当其中的一个服务刷新数据时,Spring Cloud Bus 会把信息保存到 Topic 中,这样监听这个 Topic 的服务就收到消息并自动消费。
利用 Spring Cloud Bus 的特殊机制可以实现很多功能,其中配合 Spring Cloud Config 实现配置的动态刷新就是最典型的应用场景之一。
当 Git 仓库中的配置发生了改变,我们只需要向某一个服务(既可以是 Config 服务端,也可以是 Config 客户端)发送一个 POST 请求,Spring Cloud Bus 就可以通过消息代理通知其他服务重新拉取最新配置,以实现配置的动态刷新。
Spring Cloud Bus 动态刷新配置的工作原理,如下图所示。
图11:Bus+Config 实现配置的动态刷新
根据图 12,利用 Spring Cloud Bus 实现配置的动态刷新需要以下步骤:
下面我们以 RabbitMQ 为例,来演示如何使用 Config+Bus 实现配置的动态刷新。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
###### RabbitMQ 相关配置,15672 是web 管理界面的端口,5672 是 MQ 的访问端口###########
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
## Spring Boot 2.50对 actuator 监控屏蔽了大多数的节点,只暴露了 heath 节点,本段配置(*)就是为了开启所有的节点
management:
endpoints:
web:
exposure:
include: 'bus-refresh'
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
###### RabbitMQ 相关配置,15672 是web 管理界面的端口,5672 是 MQ 的访问端口###########
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
#bootstrap.yml 是系统级别的,加载优先级高于 application.yml ,负责从外部加载配置并解析
server:
port: 3366 #端口号为 3366
spring:
application:
name: spring-cloud-config-client-bus
cloud:
config:
label: master #分支名称
name: config #配置文件名称,config-dev.yml 中的 config
profile: dev #配置文件的后缀名 config-dev.yml 中的 dev
#这里不要忘记添加 http:// 否则无法读取
uri: http://localhost:3344 #spring cloud 配置中心地址
###### RabbitMQ 相关配置,15672 是web 管理界面的端口,5672 是 MQ 的访问端口###########
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
####################### eureka 配置 ####################
eureka:
client: #将客户端注册到 eureka 服务列表内
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ #将服务注册到 Eureka 集群
## Spring Boot 2.50对 actuator 监控屏蔽了大多数的节点,只暴露了 heath 节点,本段配置(*)就是为了开启所有的节点
management:
endpoints:
web:
exposure:
include: "*" ## * 在yaml 文件属于关键字,所以需要加引号
图12:Spring Cloud Bus 动态刷新配置
图13:Spring Cloud Bus 动态刷新配置
config:
info: c.biancheng.net
version: 4.0
curl -X POST "http://localhost:3344/actuator/bus-refresh"
图14:Spring Cloud Bus 动态刷新
图15:Spring Cloud Bus 动态刷新
注意:在使用 Spring Cloud Bus 时,必须保证 Bus 所连接的消息代理软件(例如 RabbitMQ)已经正确安装且运行正常。
所谓定点通知,就是不再通知所有的 Config 客户端,而是根据需求只通知其中某一个 Config 客户端。
使用 Spring Cloud Bus 实现定点通知的方法十分简单,只要我们在发送 POST 请求时使用以下格式即可。
http://{hostname}:{port}/actuator/bus-refresh/{destination}
参数说明如下:
下面我们就通过一个简单的实例,来演示下 Spring Cloud Bus 动态刷新的定点通知。
config:
info: c.biancheng.net
version: 5.0
curl -X POST "http://localhost:3344/actuator/bus-refresh/spring-cloud-config-client:3355"
图16:Spring Cloud Bus 定点通知
Spring Cloud 本身并不是一个拿来即可用的框架,它是一套微服务规范,这套规范共有两代实现。
2018 年 12 月12 日,Netflix 公司宣布 Spring Cloud Netflix 系列大部分组件都进入维护模式,不再添加新特性。这严重地限制了 Spring Cloud 的高速发展,于是各大互联网公司和组织开始把目光转向 Spring Cloud 的第二代实现:Spring Cloud Alibaba。
Spring Cloud Alibaba 是阿里巴巴结合自身丰富的微服务实践而推出的微服务开发的一站式解决方案,是 Spring Cloud 第二代实现的主要组成部分。
Spring Cloud Alibaba 吸收了 Spring Cloud Netflix 的核心架构思想,并进行了高性能改进。自 Spring Cloud Netflix 进入停更维护后,Spring Cloud Alibaba 逐渐代替它成为主流的微服务框架。
Spring Cloud Alibaba 是国内首个进入 Spring 社区的开源项目。2018 年 7 月,Spring Cloud Alibaba 正式开源,并进入 Spring Cloud 孵化器中孵化;2019 年 7 月,Spring Cloud 官方宣布 Spring Cloud Alibaba 毕业,并将仓库迁移到 Alibaba Github OSS 下。
虽然 Spring Cloud Alibaba 诞生时间不久,但俗话说的好“大树底下好乘凉”,依赖于阿里巴巴强大的技术影响力,Spring Cloud Alibaba 在业界得到了广泛的使用,成功案例也越来越多。
Spring Cloud Alibaba 包含了多种开发分布式微服务系统的必需组件
通过 Spring Cloud Alibaba 的这些组件,我们只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
Spring Cloud Alibaba 的应用场景如下:
下表展示了 Spring Cloud 两代实现的组件对比情况。
Spring Cloud 第一代实现(Netflix) | 状态 | Spring Cloud 第二代实现(Alibaba) | 状态 |
---|---|---|---|
Ereka | 2.0 孵化失败 | Nacos Discovery 性能更好,感知力更强 | |
Ribbon | 停更进维 | Spring Cloud Loadbalancer Spring Cloud 原生组件,用于代替 Ribbon | |
Hystrix | 停更进维 | Sentinel 可视化配置,上手简单 | |
Zuul | 停更进维 | Spring Cloud Gateway 性能为 Zuul 的 1.6 倍 | |
Spring Cloud Config | 搭建过程复杂,约定过多,无可视化界面,上手难点大 | Nacos Config | 搭建过程简单,有可视化界面,配置管理更简单,容易上手 |
Spring Cloud、Spring Cloud Alibaba 以及 Spring Boot 之间版本依赖关系如下。
Spring Cloud 版本 | Spring Cloud Alibaba 版本 | Spring Boot 版本 |
---|---|---|
Spring Cloud 2020.0.1 | 2021.1 | 2.4.2 |
Spring Cloud Hoxton.SR12 | 2.2.7.RELEASE | 2.3.12.RELEASE |
Spring Cloud Hoxton.SR9 | 2.2.6.RELEASE | 2.3.2.RELEASE |
Spring Cloud Greenwich.SR6 | 2.1.4.RELEASE | 2.1.13.RELEASE |
Spring Cloud Hoxton.SR3 | 2.2.1.RELEASE | 2.2.5.RELEASE |
Spring Cloud Hoxton.RELEASE | 2.2.0.RELEASE | 2.2.X.RELEASE |
Spring Cloud Greenwich | 2.1.2.RELEASE | 2.1.X.RELEASE |
Spring Cloud Finchley | 2.0.4.RELEASE(停止维护,建议升级) | 2.0.X.RELEASE |
Spring Cloud Edgware | 1.5.1.RELEASE(停止维护,建议升级) | 1.5.X.RELEASE |
Spring Cloud Alibaba 下各组件版本关系如下表。
Spring Cloud Alibaba 版本 | Sentinel 版本 | Nacos 版本 | RocketMQ 版本 | Dubbo 版本 | Seata 版本 |
---|---|---|---|---|---|
2.2.7.RELEASE | 1.8.1 | 2.0.3 | 4.6.1 | 2.7.13 | 1.3.0 |
2.2.6.RELEASE | 1.8.1 | 1.4.2 | 4.4.0 | 2.7.8 | 1.3.0 |
2021.1 or 2.2.5.RELEASE or 2.1.4.RELEASE or 2.0.4.RELEASE | 1.8.0 | 1.4.1 | 4.4.0 | 2.7.8 | 1.3.0 |
2.2.3.RELEASE or 2.1.3.RELEASE or 2.0.3.RELEASE | 1.8.0 | 1.3.3 | 4.4.0 | 2.7.8 | 1.3.0 |
2.2.1.RELEASE or 2.1.2.RELEASE or 2.0.2.RELEASE | 1.7.1 | 1.2.1 | 4.4.0 | 2.7.6 | 1.2.0 |
2.2.0.RELEASE | 1.7.1 | 1.1.4 | 4.4.0 | 2.7.4.1 | 1.0.0 |
2.1.1.RELEASE or 2.0.1.RELEASE or 1.5.1.RELEASE | 1.7.0 | 1.1.4 | 4.4.0 | 2.7.3 | 0.9.0 |
2.1.0.RELEASE or 2.0.0.RELEASE or 1.5.0.RELEASE | 1.6.3 | 1.1.1 | 4.4.0 | 2.7.3 | 0.7.1 |
Nacos 英文全称为 Dynamic Naming and Configuration Service,是一个由阿里巴巴团队使用 Java 语言开发的开源项目。
Nacos 是一个更易于帮助构建云原生应用的动态服务发现、配置和服务管理平台(参考自 Nacos 官网)。
Nacos 的命名是由 3 部分组成:
组成部分 | 全称 | 描述 |
---|---|---|
Na | naming/nameServer | 即服务注册中心,与 Spring Cloud Eureka 的功能类似。 |
co | configuration | 即配置中心,与 Spring Cloud Config+Spring Cloud Bus 的功能类似。 |
s | service | 即服务,表示 Nacos 实现的服务注册中心和配置中心都是以服务为核心的。 |
我们可以将 Nacos 理解成服务注册中心和配置中心的组合体,它可以替换 Eureka 作为服务注册中心,实现服务的注册与发现;还可以替换 Spring Cloud Config 作为配置中心,实现配置的动态刷新。
Nacos 作为服务注册中心经历了十年“双十一”的洪峰考验,具有简单易用、稳定可靠、性能卓越等优点,可以帮助用户更敏捷、容易地构建和管理微服务应用。
Nacos 支持几乎所有主流类型“服务”的发现、配置和管理:
Nacos 提供了一系列简单易用的特性,能够帮助我们快速地实现动态服务发现、服务配置等功能。
Nacos 支持基于 DNS 和 RPC 的服务发现。当服务提供者使用原生 SDK、OpenAPI 或一个独立的 Agent TODO 向 Nacos 注册服务后,服务消费者可以在 Nacos 上通过 DNS TODO 或 HTTP&API 查找、发现服务。
Nacos 提供对服务的实时健康检查,能够阻止请求发送到不健康主机或服务实例上。Nacos 还提供了一个健康检查仪表盘,能够帮助我们根据健康状态管理服务的可用性及流量。
动态配置服务可以让我们以中心化、外部化和动态化的方式,管理所有环境的应用配置和服务配置。
动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效、敏捷。
配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。
Nacos 提供了一个简洁易用的 UI 帮助我们管理所有服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助我们更安全地在生产环境中管理配置变更和降低配置变更带来的风险。
Nacos 提供了动态 DNS 服务,能够让我们更容易地实现负载均衡、流量控制以及数据中心内网的简单 DNS 解析服务。
Nacos 提供了一些简单的 DNS APIs TODO,可以帮助我们管理服务的关联域名和可用的 IP:PORT 列表。
Nacos 能让我们从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及 metrics 统计数据。
与 Eureka 类似,Nacos 也采用 CS(Client/Server,客户端/服务器)架构,它包含两大组件,如下表。
组件 | 描述 | 功能 |
---|---|---|
Nacos Server | Nacos 服务端,与 Eureka Server 不同,Nacos Server 由阿里巴巴团队使用 Java 语言编写并将 Nacos Server 的下载地址给用户,用户只需要直接下载并运行即可。 | Nacos Server 可以作为服务注册中心,帮助 Nacos Client 实现服务的注册与发现。Nacos Server 可以作为配置中心,帮助 Nacos Client 在不重启的情况下,实现配置的动态刷新。 |
Nacos Client | Nacos 客户端,通常指的是微服务架构中的各个服务,由用户自己搭建,可以使用多种语言编写。 | Nacos Client 通过添加依赖 spring-cloud-starter-alibaba-nacos-discovery,在服务注册中心(Nacos Server)中实现服务的注册与发现。 |
Nacos Client 通过添加依赖 spring-cloud-starter-alibaba-nacos-config,在配置中心(Nacos Server)中实现配置的动态刷新。 |
Nacos 作为服务注册中心可以实现服务的注册与发现,流程如下图。
图1:Nacos 服务注册与发现
在图 1 中共涉及到以下 3 个角色:
Nacos 实现服务注册与发现的流程如下:
下面我们以 Nacos 2.0.3 为例,演示下如何安装和运行 Nacos Server,步骤如下。
图2:Nacos Server 下载
图3:Nacos Server 目录结构
Nacos Server 下各目录说明如下:
startup.cmd -m standalone
"nacos is starting with standalone"
,--.
,--.'|
,--,: : | Nacos 2.0.3
,`--.'`| ' : ,---. Running in stand alone mode, All function modules
| : : | | ' ,'\ .--.--. Port: 8848
: | \ | : ,--.--. ,---. / / | / / ' Pid: 27512
| : ' '; | / \ / \. ; ,. :| : /`./ Console: http://192.168.3.138:8848/nacos/index.html
' ' ;. ;.--. .-. | / / '' | |: :| : ;_
| | | \ | \__\/: . .. ' / ' | .; : \ \ `. https://nacos.io
' : | ; .' ," .--.; |' ; :__| : | `----. \
| | '`--' / / ,. |' | '.'|\ \ / / /`--' /
' : | ; : .' \ : : `----' '--'. /
; |.' | , .-./\ \ / `--'---'
'---' `--`---' `----'
2021-11-08 16:16:38,877 INFO Bean 'org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler@5ab9b447' of type [org.springframework.security.access.expression.method
.DefaultMethodSecurityExpressionHandler] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-11-08 16:16:38,884 INFO Bean 'methodSecurityMetadataSource' of type [org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource] is not eligible for getting processed by al
l BeanPostProcessors (for example: not eligible for auto-proxying)
2021-11-08 16:16:40,001 INFO Tomcat initialized with port(s): 8848 (http)
2021-11-08 16:16:40,713 INFO Root WebApplicationContext: initialization completed in 14868 ms
2021-11-08 16:16:52,351 INFO Initializing ExecutorService 'applicationTaskExecutor'
2021-11-08 16:16:52,560 INFO Adding welcome page: class path resource [static/index.html]
2021-11-08 16:16:54,239 INFO Creating filter chain: Ant [pattern='/**'], []
2021-11-08 16:16:54,344 INFO Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7dd611c8, org.springframework.security.web.con
text.SecurityContextPersistenceFilter@5c7668ba, org.springframework.security.web.header.HeaderWriterFilter@fb713e7, org.springframework.security.web.csrf.CsrfFilter@6ec7bce0, org.springframework.secur
ity.web.authentication.logout.LogoutFilter@7d9ba6c, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@158f4cfe, org.springframework.security.web.servletapi.SecurityContextHolderAwa
reRequestFilter@6c6333cd, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5d425813, org.springframework.security.web.session.SessionManagementFilter@13741d5a, org.springf
ramework.security.web.access.ExceptionTranslationFilter@3727f0ee]
2021-11-08 16:16:54,948 INFO Initializing ExecutorService 'taskScheduler'
2021-11-08 16:16:54,977 INFO Exposing 16 endpoint(s) beneath base path '/actuator'
2021-11-08 16:16:55,309 INFO Tomcat started on port(s): 8848 (http) with context path '/nacos'
2021-11-08 16:16:55,319 INFO Nacos started successfully in stand alone mode. use embedded storage
图4:Nacos Server 登陆页面
图5:Nacos Server 控制台
自此,我们就完成了 Nacos Server 的下载、安装和运行工作。
接下来,我们来搭建一个服务提供者,步骤如下。
<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.0modelVersion>
<packaging>pompackaging>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.5.6version>
<relativePath/>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>spring-cloud-alibaba-demoartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<junit.version>4.12junit.version>
<log4j.version>1.2.17log4j.version>
<lombok.version>1.16.18lombok.version>
<spring-cloud.version>2020.0.4spring-cloud.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2021.1version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
project>
在该工程的 pom.xml 中,我们通过 dependencyManagement 对 Spring Cloud Alibaba 的版本信息进行管理,该工程下的各个子模块在引入 Spring Cloud Alibaba 的各个组件时就不要单独指定版本号了。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>net.biancheng.cgroupId>
<version>1.0-SNAPSHOTversion>
<artifactId>spring-cloud-alibaba-demoartifactId>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>spring-cloud-alibaba-provider-8001artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>spring-cloud-alibaba-provider-8001name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
#端口号
server.port=8001
#服务名
spring.application.name=spring-cloud-alibaba-provider
#Nacos Server 的地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
management.endpoints.web.exposure.include=*
package net.biancheng.c.controller;
import lombok.extern.slf4j.Slf4j;
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.RestController;
@RestController
@Slf4j
public class DeptController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/dept/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "提醒您,服务访问成功!
服务名:spring-cloud-alibaba-provider
端口号: " + serverPort + "
传入的参数:" + id;
}
}
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient //开启服务发现功能
public class SpringCloudAlibabaProvider8001Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudAlibabaProvider8001Application.class, args);
}
}
启动 spring-cloud-alibaba-provider-8001,使用浏览器访问“http://localhost:8001/dept/nacos/1”。
使用浏览器访问“http://localhost:8848/nacos”,查看“服务管理”下的“服务列表”,如下图。
图7:服务注册列表
从图 7 可以看到,我们搭建的服务提供者 spring-cloud-alibaba-provider-8001 所提供的服务已经被注册到了 Nacos Server 上了。
下面,我们就来搭建一个服务消费者来消费 spring-cloud-alibaba-provider-8001 提供的服务,步骤如下。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>net.biancheng.cgroupId>
<version>1.0-SNAPSHOTversion>
<artifactId>spring-cloud-alibaba-demoartifactId>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>spring-cloud-alibaba-consumer-nacos-8081artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>spring-cloud-alibaba-consumer-nacos-8081name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-loadbalancerartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
注意:由于 Netflix Ribbon 已经进入停更维护状态,Nacos Discovery 已经停了对 Ribbon 的支持,因此我们需要在服务消费者的 pom.xml 中引入 spring-cloud-loadbalancer 才能调用服务提供者提供的服务。
server:
port: 8801 #端口号
spring:
application:
name: spring-cloud-alibaba-consumer #服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos server 的地址
#以下配置信息并不是默认配置,而是我们自定义的配置,目的是不在 Controller 内硬编码服务提供者的服务名
service-url:
nacos-user-service: http://spring-cloud-alibaba-provider #服务提供者的服务名
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient // 开启服务注册与发现功能
public class SpringCloudAlibabaConsumerNacos8801Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudAlibabaConsumerNacos8081Application.class, args);
}
}
package net.biancheng.c.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 ApplicationContextBean {
@Bean
@LoadBalanced //与 Ribbon 集成,并开启负载均衡功能
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
package net.biancheng.c.controller;
import lombok.extern.slf4j.Slf4j;
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.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
@Slf4j
public class DeptController_Consumer {
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serverURL; //服务提供者的服务名
@GetMapping("/consumer/dept/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(serverURL + "/dept/nacos/" + id, String.class);
}
}
图8:服务消费者
Nacos Server 还可以作为配置中心,对 Spring Cloud 应用的外部配置进行统一地集中化管理。而我们只需要在应用的 POM 文件中引入 spring-cloud-starter-alibaba-nacos-config 即可实现配置的获取与动态刷新。
从配置管理的角度看,Nacos 可以说是 Spring Cloud Config 的替代方案,但相比后者 Nacos 的使用更简单,操作步骤也更少。
接下来,我们通过一个实例来演示下 Nacos 是如何实现配置的统一管理和动态刷新的。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>net.biancheng.cgroupId>
<version>1.0-SNAPSHOTversion>
<artifactId>spring-cloud-alibaba-demoartifactId>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>spring-cloud-alibaba-config-client-3377artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>spring-cloud-alibaba-nacos-config-client-3377name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bootstrapartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
注意:我们使用的 Spring Cloud 2020 版本默认不启用 bootstrap,若想要在应用启动时加载 bootstrap 配置(例如 bootstrap.yml 或 bootstrap.properties),就需要我们在 pom.xml 中显式的引入 spring-cloud-starter-bootstrap 依赖。
server:
port: 3377 #端口号
spring:
application:
name: config-client #服务名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #Nacos服务注册中心地址
config:
server-addr: 127.0.0.1:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
spring:
profiles:
active: dev #激活 dev 的配置
package net.biancheng.c.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String ConfigInfo;
@GetMapping("/config/info")
public String getConfigInfo(){
return ConfigInfo;
}
}
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudAlibabaNacosConfigClient3377Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudAlibabaNacosConfigClient3377Application.class, args);
}
}
Data ID: config-client-dev.yaml
Group : DEFAULT_GROUP
配置格式: YAML
配置内容: config:
info: c.biancheng.net
在 Nacos Server 中,配置的 dataId(即 Data ID)的完整格式如下:
${prefix}-${spring.profiles.active}.${file-extension}
dataId 格式中各参数说明如下:
图10:Nacos Config
config:
info: this is c.biancheng.net
图11:Nacos Cofig
在实际的项目开发中,一个微服务系统往往由十几,几十个甚至几百个微服务组成。 这些服务若全部注册到同一台 Nacos Server,就极有可能导致 Nacos Server 因为不堪重负而崩溃,最终导致整个微服务系统瘫痪。解决这个问题最直接的办法就是使用 Nacos Server 集群。
Nacos Server 的集群化部署有一个十分明显的优点,那就是可以保障系统的高可用性。在集群化部署中,只要不是所有的 Nacos Server 都停止工作,Nacos Client 就还可以从集群中正常的 Nacos Server 上获取服务信息及配置,而不会导致系统的整体瘫痪,这就是 Nacos Server 集群化部署的高可用性。
下图展示了 Nacos Server 集群化部署的基本架构。
图12:Nacos Server 集群架构
下面我们以 Windows 系统为例,演示如何部署 Nacos Server 集群。
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info */
/******************************************/
CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) DEFAULT NULL,
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL,
`c_use` varchar(64) DEFAULT NULL,
`effect` varchar(64) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
`c_schema` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
`content` longtext NOT NULL COMMENT '内容',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(64) unsigned NOT NULL,
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) NOT NULL,
`group_id` varchar(128) NOT NULL,
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL,
`md5` varchar(32) DEFAULT NULL,
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` text,
`src_ip` varchar(50) DEFAULT NULL,
`op_type` char(10) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);
CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL,
`resource` varchar(255) NOT NULL,
`action` varchar(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
192.168.3.138:3333
192.168.3.138:4444
192.168.3.138:5555
配置说明如下:
192.168.138 为本地电脑主机的 IP 地址,这里最好不要写成 localhost 或 127.0.0.1,否则 Nacos Server 集群可能会搭建失败!
本次搭建的 Nacos Server 集群的端口分别为:3333、4444、5555。
server.port=3333
################ MySQL 数据库配置##################
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
db.user=root
db.password=root
4. 将该 Nacos Server 目录复制到另外两台机器上,并将它们的端口号分别修改为: 4444 和 5555。
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream cluster{
server 127.0.0.1:3333;
server 127.0.0.1:4444;
server 127.0.0.1:5555;
}
server {
listen 1111;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
#root html;
#index index.html index.htm;
proxy_pass http://cluster;
}
}
}
"nacos is starting with cluster"
,--.
,--.'|
,--,: : | Nacos 2.0.3
,`--.'`| ' : ,---. Running in cluster mode, All function modules
| : : | | ' ,'\ .--.--. Port: ****
: | \ | : ,--.--. ,---. / / | / / ' Pid: 21592
| : ' '; | / \ / \. ; ,. :| : /`./ Console: http://192.168.3.138:3333/nacos/index.html
' ' ;. ;.--. .-. | / / '' | |: :| : ;_
| | | \ | \__\/: . .. ' / ' | .; : \ \ `. https://nacos.io
' : | ; .' ," .--.; |' ; :__| : | `----. \
| | '`--' / / ,. |' | '.'|\ \ / / /`--' /
' : | ; : .' \ : : `----' '--'. /
; |.' | , .-./\ \ / `--'---'
'---' `--`---' `----'
2021-11-09 16:25:00,993 INFO The server IP list of Nacos is [192.168.3.138:3333, 192.168.3.138:4444, 192.168.3.138:5555]
2021-11-09 16:27:07,318 INFO Nacos is starting...
2021-11-09 16:27:08,325 INFO Nacos is starting...
2021-11-09 16:27:09,340 INFO Nacos is starting...
2021-11-09 16:27:10,343 INFO Nacos is starting...
2021-11-09 16:27:10,742 INFO Nacos started successfully in cluster mode. use external storage
图13:Nginx 启动脚本
图14:Nacos 集群
server:
port: 8801 #端口号
spring:
application:
name: spring-cloud-alibaba-consumer #服务名
cloud:
nacos:
discovery:
#server-addr: localhost:8848 #单机版 Nacos Server 的地址
server-addr: localhost:1111 #集群版 Nacos Server 的地址
#以下配置信息并不是默认配置,而是我们自定义的配置,目的是不在 Controller 内硬编码服务提供者的服务名
service-url:
nacos-user-service: http://spring-cloud-alibaba-provider #服务提供者的服务名
图15:服务注册到 Nacos Server 集群上
Sentinel 是由阿里巴巴中间件团队开发的开源项目,是一种面向分布式微服务架构的轻量级高可用流量控制组件。
Sentinel 主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度帮助用户保护服务的稳定性。
Sentinel 具有以下优势:
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它可以在 ClassPath 路径下的 META-INF/services 文件夹查找文件,并自动加载文件中定义的类。
从功能上来说,Sentinel 与 Spring Cloud Netfilx Hystrix 类似,但 Sentinel 要比 Hystrix 更加强大,例如 Sentinel 提供了流量控制功能、比 Hystrix 更加完善的实时监控功能等等。
Sentinel 主要由以下两个部分组成:
Sentinel 核心库不依赖 Sentinel Dashboard,但两者结合使用可以有效的提高效率,让 Sentinel 发挥它最大的作用。
Sentinel 的基本概念有两个,它们分别是:资源和规则。
基本概念 | 描述 |
---|---|
资源 | 资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如由应用程序提供的服务或者是服务里的方法,甚至可以是一段代码。我们可以通过 Sentinel 提供的 API 来定义一个资源,使其能够被 Sentinel 保护起来。通常情况下,我们可以使用方法名、URL 甚至是服务名来作为资源名来描述某个资源。 |
规则 | 围绕资源而设定的规则。Sentinel 支持流量控制、熔断降级、系统保护、来源访问控制和热点参数等多种规则,所有这些规则都可以动态实时调整。 |
@SentinelResource 注解是 Sentinel 提供的最重要的注解之一,它还包含了多个属性,如下表。
属性 | 说明 | 必填与否 | 使用要求 |
---|---|---|---|
value | 用于指定资源的名称 | 必填 | - |
entryType | entry 类型 | 可选项(默认为 EntryType.OUT) | - |
blockHandler | 服务限流后会抛出 BlockException 异常,而 blockHandler 则是用来指定一个函数来处理 BlockException 异常的。简单点说,该属性用于指定服务限流后的后续处理逻辑。 | 可选项 | blockHandler 函数访问范围需要是 public;返回类型需要与原方法相匹配;参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException;blockHandler 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 blockHandler 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 |
blockHandlerClass | 若 blockHandler 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。 | 可选项 | 不能单独使用,必须与 blockHandler 属性配合使用;该属性指定的类中的 blockHandler 函数必须为 static 函数,否则无法解析。 |
fallback | 用于在抛出异常(包括 BlockException)时,提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。 | 可选项 | 返回值类型必须与原函数返回值类型一致;方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;fallback 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。 |
fallbackClass | 若 fallback 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。 | 可选项 | 不能单独使用,必须与 fallback 或 defaultFallback 属性配合使用;该属性指定的类中的 fallback 函数必须为 static 函数,否则无法解析。 |
defaultFallback | 默认的 fallback 函数名称,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所以类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。 | 可选项 | 返回值类型必须与原函数返回值类型一致;方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;defaultFallback |
exceptionsToIgnore | 用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。 | 可选项 | - |
注:在 Sentinel 1.6.0 之前,fallback 函数只针对降级异常(DegradeException)进行处理,不能处理业务异常。
Sentinel 提供了一个轻量级的开源控制台 Sentinel Dashboard,它提供了机器发现与健康情况管理、监控(单机和集群)、规则管理与推送等多种功能。
Sentinel 控制台提供的功能如下:
Sentinel Dashboard 是我们配置和管理规则(例如流控规则、熔断降级规则等)的重要入口之一。通过它,我们不仅可以对规则进行配置和管理,还能实时查看规则的效果。
下面我们就来演示下,如何下载和安装 Sentinel 控制台,具体步骤如下。
图1:Sentinel 控制台下载
java -jar sentinel-dashboard-1.8.2.jar
图2:Sentinel 控制台登录页
图3:Sentinel 控制台主页
Sentinel 的开发流程如下:
为了减少开发的复杂程度,Sentinel 对大部分的主流框架都进行了适配,例如 Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux 和 Reactor 等。以 Spring Cloud 为例,我们只需要引入 spring-cloud-starter-alibaba-sentinel 的依赖,就可以方便地将 Sentinel 整合到项目中。
下面我们就通过一个简单的实例,演示如何将 Sentinel 整合到 Spring Cloud 项目中,步骤如下。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>net.biancheng.cgroupId>
<version>1.0-SNAPSHOTversion>
<artifactId>spring-cloud-alibaba-demoartifactId>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>spring-cloud-alibaba-sentinel-service-8401artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>spring-cloud-alibaba-sentinel-service-8401name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
server:
port: 8401 #端口
spring:
application:
name: sentinel-service #服务名
cloud:
nacos:
discovery:
#Nacos服务注册中心(集群)地址
server-addr: localhost:1111
sentinel:
transport:
#配置 Sentinel dashboard 地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
package net.biancheng.c.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class SentinelFlowLimitController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/testA")
public String testA() {
return "提醒您,服务访问成功------testA";
}
@GetMapping("/testB")
public String testB() {
return "提醒您,服务访问成功------testB"
}
}
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudAlibabaSentinelService8401Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudAlibabaSentinelService8401Application.class, args);
}
}
依次启动 Nacos Server 集群、 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testA”.
使用浏览器访问 Sentinel 控制台主页,我们发现在“首页”下方新增了一个“sentinel-servcie”的菜单,而这正是 spring-cloud-alibaba-sentinel-service-8401 的服务名(spring.application.name),说明 Sentinel 已经监控到这个服务,如下图。
图5:Sentinel 控制台主页
图6:Sentinel 实时监控
资源是 Sentinel 中的核心概念之一。在项目开发时,我们只需要考虑这个服务、方法或代码是否需要保护,如果需要保护,就可以将它定义为一个资源。
Sentinel 为我们提供了多种定义资源的方式:
Sentinel 对大部分的主流框架都进行了适配,我们只要引入相关的适配模块(例如 spring-cloud-starter-alibaba-sentinel),Snetinel 就会自动将项目中的服务(包括调用端和服务端)定义为资源,资源名就是服务的请求路径。此时,我们只要再定义一些规则,这些资源就可以享受到 Sentinel 的保护。
我们可以在 Sentinel 控制台的“簇点链路”中,直接查看被 Sentinel 监控的资源,如下图。
图7:Sentinel 控制台-簇点链路
通过 SphU 手动定义资源
Sentinel 提供了一个名为 SphU 的类,它包含的 try-catch 风格的 API ,可以帮助我们手动定义资源。
下面我们就通过一个实例,来演示下如何通过 SphU 定义资源。
package net.biancheng.c.controller;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphO;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@Slf4j
public class SentinelFlowLimitController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/testA")
public String testA() {
return testAbySphU();
}
@GetMapping("/testB")
public String testB() {
return "提醒您,服务访问成功------testB";
}
/**
* 通过 SphU 手动定义资源
* @return
*/
public String testAbySphU() {
Entry entry = null;
try {
entry = SphU.entry("testAbySphU");
//您的业务逻辑 - 开始
log.info("提醒您,服务访问成功------testA:"+serverPort);
return "提醒您,服务访问成功------testA:"+serverPort;
//您的业务逻辑 - 结束
} catch (BlockException e1) {
//流控逻辑处理 - 开始
log.info("提醒您,testA 服务被限流");
return "提醒您,testA 服务被限流";
//流控逻辑处理 - 结束
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testA”,结果如下。
提醒您,服务访问成功------testA:8401
访问 Sentinel 控制台主页,点击 sentinel-service 下的“簇点链路”,结果如下图。
图8:Sentinel 通过 SphU 定义资源
通过 SphO 手动定义资源
Sentinel 还提供了一个名为 SphO 的类,它包含了 if-else 风格的 API,能帮助我们手动定义资源。通过这种方式定义的资源,发生了限流之后会返回 false,此时我们可以根据返回值,进行限流之后的逻辑处理。
下面我们就通过一个实例,来演示下如何通过 SphO 定义资源。
package net.biancheng.c.controller;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphO;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@Slf4j
public class SentinelFlowLimitController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/testA")
public String testA() {
return testAbySphU();
}
@GetMapping("/testB")
public String testB() {
return testBbySphO();
}
/**
* 通过 SphU 手动定义资源
*
* @return
*/
public String testAbySphU() {
Entry entry = null;
try {
entry = SphU.entry("testAbySphU");
//您的业务逻辑 - 开始
log.info("提醒您,服务访问成功------testA:" + serverPort);
return "提醒您,服务访问成功------testA:" + serverPort;
//您的业务逻辑 - 结束
} catch (BlockException e1) {
//流控逻辑处理 - 开始
log.info("提醒您,testA 服务被限流");
return "提醒您,testA 服务被限流";
//流控逻辑处理 - 结束
} finally {
if (entry != null) {
entry.exit();
}
}
}
/**
* 通过 SphO 手动定义资源
*
* @return
*/
public String testBbySphO() {
if (SphO.entry("testBbySphO")) {
// 务必保证finally会被执行
try {
log.info("提醒您,服务访问成功------testB:" + serverPort);
return "提醒您,服务访问成功------testB:" + serverPort;
} finally {
SphO.exit();
}
} else {
// 资源访问阻止,被限流或被降级
//流控逻辑处理 - 开始
log.info("提醒您,testB 服务被限流");
return "提醒您,testB 服务被限流";
//流控逻辑处理 - 结束
}
}
}
重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testB”,结果如下。
提醒您,服务访问成功------testB:8401
访问 Sentinel 控制台主页,点击 sentinel-service 下的“簇点链路”,结果如下图。
Sentinel SphO
图9:Sentinel 通过 SphO 定义资源
注解方式定义资源(推荐)
除了以上两种方式外,我们还可以通过 Sentinel 提供的 @SentinelResource 注解定义资源。
下面我们就通过一个实例,来演示下如何通过 @SentinelResource 注解定义资源。
@GetMapping("/testC")
@SentinelResource(value = "testCbyAnnotation") //通过注解定义资源
public String testC() {
log.info("提醒您,服务访问成功------testC:" + serverPort);
return "提醒您,服务访问成功------testC:" + serverPort;
}
重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testC”,结果如下。
服务访问成功------testC:8401
访问 Sentinel 控制台主页,点击 sentinel-service 下的“簇点链路”,结果如下图。
图10:Sentinel 注解方式定义资源
Sentinel 流量控制
任何系统处理请求的能力都是有限的,但任意时间内到达系统的请求量往往是随机且不可控的,如果在某一个瞬时时刻请求量急剧增,那么系统就很有可能被瞬时的流量高峰冲垮。为了避免此类情况发生,我们都需要根据系统的处理能力对请求流量进行控制,这就是我们常说的“流量控制”,简称“流控”。
Sentinel 作为一种轻量级高可用流量控制组件,流量控制是它最主要的工作之一。
我们可以针对资源定义流控规则,Sentinel 会根据这些规则对流量相关的各项指标进行监控。当这些指标当达到或超过流控规则规定的阈值时,Sentinel 会对请求的流量进行限制(即“限流”),以避免系统被瞬时的流量高峰冲垮,保障系统的高可用性。
一条流量规则主要由下表中的属性组成,我们可以通过组合这些属性来实现不同的限流效果。
属性 | 说明 | 默认值 |
---|---|---|
资源名 | 的作用对象。 | - |
阈值 | 值。 | - |
阈值类型 | 流控阈值的类型,包括 QPS 或并发线程数。 | QPS |
针对来源 | 流控针对的调用来源。 | default,表示不区分调用来源 |
流控模式 | 调用关系限流策略,包括直接、链路和关联。 | 直接 |
流控效果 | 流控效果(直接拒绝、Warm Up、匀速排队),不支持按调用关系限流。 | 直接拒绝 |
注:QPS 表示并发请求数,换句话说就是,每秒钟最多通过的请求数。
同一个资源可以创建多条流控规则,Sentinel 会遍历这些规则,直到有规则触发限流或者所有规则遍历完毕为止。
Sentinel 触发限流时,资源会抛出 BlockException 异常,此时我们可以捕捉 BlockException 来自定义被限流之后的处理逻辑。
注意:这里我们主要讲解 Sentinel 流控规则的定义与使用,至于详细的流控规则配置,请参考 Sentinel 官方流控文档。
通过 Sentinel 控制台定义流控规则
我们可以通过 Sentinel 控制台,直接对资源定义流控规则,操作步骤如下。
/**
* 通过 Sentinel 控制台定义流控规则
*
* @return
*/
@GetMapping("/testD")
public String testD() {
log.info("提醒您,服务访问成功------testD:" + serverPort);
return "提醒您,服务访问成功------testD:" + serverPort;
}
重启 spring-cloud-alibaba-sentinel-service-8401,使用浏览器访问“http://localhost:8401/testD”,结果如下。
提醒您,服务访问成功------testD:8401
使用浏览器访问“http://localhost:8080”,登陆 Sentinel 控制台主页,点击 sentinel-sevice 下的“簇点链路”,结果如下图。
图11:Sentinel 控制台定义流控规则
图12:Sentinel 控制台定义流控规则
图13:Sentinel 控制台流控规则列表
若页面中出现以上信息,则说明该服务已被限流,但这种提示是 Sentinel 系统自动生成的,用户体验不好。
/**
* 通过 Sentinel 控制台定义流控规则
*
*/
@GetMapping("/testD")
@SentinelResource(value = "testD-resource", blockHandler = "blockHandlerTestD") //通过注解定义资源
public String testD() {
log.info("提醒您,服务访问成功------testD:" + serverPort);
return "提醒您,服务访问成功------testD:" + serverPort;
}
/**
* 限流之后的逻辑
* @param exception
* @return
*/
public String blockHandlerTestD(BlockException exception) {
log.info(Thread.currentThread().getName() + "提醒您,TestD服务访问失败! 您已被限流,请稍后重试");
return "提醒您,TestD服务访问失败! 您已被限流,请稍后重试";
}
在以上代码中,我们通过 @SentinelResource 注解的 blockHandler 属性指定了一个 blockHandler 函数,进行限流之后的后续处理。
使用 @SentinelResource 注解的 blockHandler 属性时,需要注意以下事项:
blockHandler 函数访问范围需要是 public;
返回类型需要与原方法相匹配;
参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException;
blockHandler 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 blockHandler 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
请务必添加 blockHandler 属性来指定自定义的限流处理方法,若不指定,则会跳转到错误页(用户体验不好)。
图14:Sentinel 控制台-簇点链路
图15:Sentinel 控制台新增流控规则
提醒您,TestD服务访问失败! 您已被限流,请稍后重试
通过代码定义流控规则
我们还可以在服务代码中,调用 FlowRuleManager 类的 loadRules() 方法来定义流控规则,该方法需要一个 FlowRule 类型的 List 集合作为其参数,示例代码如下。
public static void loadRules(List<FlowRule> rules) {
currentProperty.updateValue(rules);
}
FlowRule 可以通过以下属性定义流控规则,如下表。
属性 | 说明 | 默认值 |
---|---|---|
resource | 资源名,即流控规则的作用对象 | - |
count | 限流的阈值。 | - |
grade | 流控阈值的类型,包括 QPS 或并发线程数 | QPS |
limitApp | 流控针对的调用来源 | default,表示不区分调用来源 |
strategy | 调用关系限流策略,包括直接、链路和关联 | 直接 |
controlBehavior | 流控效果(直接拒绝、Warm Up、匀速排队),不支持按调用关系限流 | 直接拒绝 |
下面我们就通过一个简单的实例,来演示下如何通过代码定义流控规则,步骤如下。
/**
* 通过代码定义流量控制规则
*/
private static void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
//定义一个限流规则对象
FlowRule rule = new FlowRule();
//资源名称
rule.setResource("testD-resource");
//限流阈值的类型
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置 QPS 的阈值为 2
rule.setCount(2);
rules.add(rule);
//定义限流规则
FlowRuleManager.loadRules(rules);
}
@GetMapping("/testD")
@SentinelResource(value = "testD-resource", blockHandler = "blockHandlerTestD") //通过注解定义资源
public String testD() {
initFlowRules(); //调用初始化流控规则的方法
log.info("提醒您,服务访问成功------testD:" + serverPort);
return "提醒您,服务访问成功------testD:" + serverPort;
}
重启 spring-cloud-alibaba-sentinel-service-8401,并使用浏览器访问“http://localhost:8401/testD”,结果如下。
提醒您,服务访问成功------testD:8401
快速连续(频率大于每秒钟 2 次)访问“http://localhost:8401/testD”,结果如下。
提醒您,TestD服务访问失败! 您已被限流,请稍后重试
打开命令行窗口,执行以下命令查看资源的实时统计信息。
curl http://localhost:8719/cnode?id=testD-resource
控制台输出内容如下。
idx id thread pass blocked success total aRt 1m-pass 1m-block 1m-all exceptio
2 testD-resource 0 0.0 0.0 0.0 0.0 0.0 10 16 26 0.0
实时统计信息各列名说明如下:
除了流量控制以外,对调用链路中不稳定资源的熔断降级,也是保障服务高可用的重要措施之一。
在分布式微服务架构中,一个系统往往由多个服务组成,不同服务之间相互调用,组成复杂的调用链路。如果链路上的某一个服务出现故障,那么故障就会沿着调用链路在系统中蔓延,最终导致整个系统瘫痪。Sentinel 提供了熔断降级机制就可以解决这个问题。
Sentinel 的熔断将机制会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),暂时切断对这个资源的调用,以避免局部不稳定因素导致整个系统的雪崩。
熔断降级作为服务保护自身的手段,通常在客户端(调用端)进行配置,资源被熔断降级最直接的表现就是抛出 DegradeException 异常。
Sentinel 熔断策略
Sentinel 提供了 3 种熔断策略,如下表所示。
熔断策略 | 说明 |
---|---|
慢调用比例(SLOW_REQUEST_RATIO) | 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大响应时间),若请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则再次被熔断。 |
异常比例 (ERROR_RATIO) | 当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目且异常的比例大于阈值,则在接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。 |
异常数 (ERROR_COUNT) | 当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。 |
注意:Sentinel 1.8.0 版本对熔断降级特性进行了全新的改进升级,以上熔断策略针对的是 Sentinel 1.8.0 及以上版本。
Sentinel 熔断降级中共涉及 3 种状态,熔断状态的之间的转换过程如下图。
图16:Sentinel 熔断状态转换
Sentinel 熔断降级中共涉及 3 种状态,如下表。
状态 | 说明 | 触发条件 |
---|---|---|
熔断关闭状态(CLOSED) | 处于关闭状态时,请求可以正常调用资源。 满足以下任意条件,Sentinel 熔断器进入熔断关闭状态:全部请求访问成功。单位统计时长(statIntervalMs)内请求数目小于设置的最小请求数目。未达到熔断标准,例如服务超时比例、异常数、异常比例未达到阈值。处于探测恢复状态时,下一个请求访问成功。 | |
熔断开启状态(OPEN) | 处于熔断开启状态时,熔断器会一定的时间(规定的熔断时长)内,暂时切断所有请求对该资源的调用,并调用相应的降级逻辑使请求快速失败避免系统崩溃。 满足以下任意条件,Sentinel 熔断器进入熔断开启状态:单位统计时长内请求数目大于设置的最小请求数目,且已达到熔断标准,例如请求超时比例、异常数、异常比例达到阈值。处于探测恢复状态时,下一个请求访问失败。 | |
探测恢复状态(HALF-OPEN) | 处于探测恢复状态时,Sentinel 熔断器会允许一个请求调用资源。则若接下来的一个请求成功完成(没有错误)则结束熔断,熔断器进入熔断关闭(CLOSED)状态;否则会再次被熔断,熔断器进入熔断开启(OPEN)状态。 在熔断开启一段时间(降级窗口时间或熔断时长,单位为 s)后,Sentinel 熔断器自动会进入探测恢复状态。 |
Sentinel 熔断降级规则包含多个重要属性,如下表所示。
属性 | 说明 | 默认值 | 使用范围 |
---|---|---|---|
资源名 | 规则的作用对象。 | - | 所有熔断策略 |
熔断策略 | Sentinel 支持3 中熔断策略:慢调用比例、异常比例、异常数策略。 | 慢调用比例 | 所有熔断策略 |
最大 RT | 请求的最大相应时间,请求的响应时间大于该值则统计为慢调用。 | - | 慢调用比例 |
熔断时长 | 熔断开启状态持续的时间,超过该时间熔断器会切换为探测恢复状态(HALF-OPEN),单位为 s。 | - | 所有熔断策略 |
最小请求数 | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入)。 | 5 | 所有熔断策略 |
统计时长 | 熔断触发需要统计的时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入)。 | 1000 ms | 所有熔断策略 |
比例阈值 | 分为慢调用比例阈值和异常比例阈值,即慢调用或异常调用占所有请求的百分比,取值范围 [0.0,1.0]。 | - | 慢调用比例 、异常比例 |
异常数 | 请求或调用发生的异常的数量。 | - | 异常数 |
Sentinel 实现熔断降级的步骤如下:
我们可以通过 Sentinel 控制台直接对资源定义熔断降级规则。
下面我们通过一个实例,来演示如何通过 Sentinel 控制台,对资源定义降级规则。
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`dept_no` int NOT NULL AUTO_INCREMENT,
`dept_name` varchar(255) DEFAULT NULL,
`db_source` varchar(255) DEFAULT NULL,
PRIMARY KEY (`dept_no`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `dept` VALUES ('1', '开发部', 'bianchengbang_jdbc');
INSERT INTO `dept` VALUES ('2', '人事部', 'bianchengbang_jdbc');
INSERT INTO `dept` VALUES ('3', '财务部', 'bianchengbang_jdbc');
INSERT INTO `dept` VALUES ('4', '市场部', 'bianchengbang_jdbc');
INSERT INTO `dept` VALUES ('5', '运维部', 'bianchengbang_jdbc');
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>net.biancheng.cgroupId>
<version>1.0-SNAPSHOTversion>
<artifactId>spring-cloud-alibaba-demoartifactId>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>spring-cloud-alibaba-provider-mysql-8003artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>spring-cloud-alibaba-provider-mysql-8003name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>net.biancheng.cgroupId>
<artifactId>spring-cloud-alibaba-apiartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-coreartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
server:
port: 8003 #端口
spring:
application:
name: spring-cloud-alibaba-provider-mysql
cloud:
nacos:
discovery:
server-addr: localhost:1111
######################### 数据库连接 #################################
datasource:
username: root #数据库登陆用户名
password: root #数据库登陆密码
url: jdbc:mysql://127.0.0.1:3306/spring_cloud_db2 #数据库url
driver-class-name: com.mysql.jdbc.Driver
management:
endpoints:
web:
exposure:
include: "*" # * 在yaml 文件属于关键字,所以需要加引号
###################################### MyBatis 配置 ######################################
mybatis:
# 指定 mapper.xml 的位置
mapper-locations: classpath:mybatis/mapper/*.xml
#扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
type-aliases-package: net.biancheng.c.entity
configuration:
#默认开启驼峰命名法,可以不用设置该属性
map-underscore-to-camel-case: true
package net.biancheng.c.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor //无参构造函数
@Data // 提供类的get、set、equals、hashCode、canEqual、toString 方法
@Accessors(chain = true)
public class Dept implements Serializable {
private Integer deptNo;
private String deptName;
private String dbSource;
}
package net.biancheng.c.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this(code, message, null);
}
}
package net.biancheng.c.mapper;
import net.biancheng.c.entity.Dept;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface DeptMapper {
//根据主键获取数据
Dept selectByPrimaryKey(Integer deptNo);
//获取表中的全部数据
List<Dept> GetAll();
}
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.biancheng.c.mapper.DeptMapper">
<resultMap id="BaseResultMap" type="net.biancheng.c.entity.Dept">
<id column="dept_no" jdbcType="INTEGER" property="deptNo"/>
<result column="dept_name" jdbcType="VARCHAR" property="deptName"/>
<result column="db_source" jdbcType="VARCHAR" property="dbSource"/>
resultMap>
<sql id="Base_Column_List">
dept_no
, dept_name, db_source
sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from dept
where dept_no = #{deptNo,jdbcType=INTEGER}
select>
<select id="GetAll" resultType="net.biancheng.c.entity.Dept">
select *
from dept;
select>
mapper>
package net.biancheng.c.service;
import net.biancheng.c.entity.Dept;
import java.util.List;
public interface DeptService {
Dept get(Integer deptNo);
List<Dept> selectAll();
}
package net.biancheng.c.service.impl;
import net.biancheng.c.entity.Dept;
import net.biancheng.c.mapper.DeptMapper;
import net.biancheng.c.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("deptService")
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public Dept get(Integer deptNo) {
return deptMapper.selectByPrimaryKey(deptNo);
}
@Override
public List<Dept> selectAll() {
return deptMapper.GetAll();
}
}
package net.biancheng.c.controller;
import lombok.extern.slf4j.Slf4j;
import net.biancheng.c.entity.CommonResult;
import net.biancheng.c.entity.Dept;
import net.biancheng.c.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.concurrent.TimeUnit;
@RestController
@Slf4j
public class DeptController {
@Autowired
private DeptService deptService;
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "/dept/get/{id}", method = RequestMethod.GET)
public CommonResult<Dept> get(@PathVariable("id") int id) {
log.info("端口:" + serverPort + "\t+ dept/get/");
try {
TimeUnit.SECONDS.sleep(1);
log.info("休眠 1秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
Dept dept = deptService.get(id);
CommonResult<Dept> result = new CommonResult(200, "from mysql,serverPort: " + serverPort, dept);
return result;
}
@RequestMapping(value = "/dept/list", method = RequestMethod.GET)
public CommonResult<List<Dept>> list() {
log.info("端口:" + serverPort + "\t+ dept/list/");
List<Dept> depts = deptService.selectAll();
CommonResult<List<Dept>> result = new CommonResult(200, "from mysql,serverPort: " + serverPort, depts);
return result;
}
}
package net.biancheng.c;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudAlibabaProviderMysql8003Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudAlibabaProviderMysql8003Application.class, args);
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>net.biancheng.cgroupId>
<version>1.0-SNAPSHOTversion>
<artifactId>spring-cloud-alibaba-demoartifactId>
parent>
<groupId>net.biancheng.cgroupId>
<artifactId>spring-cloud-alibaba-consumer-mysql-8803artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>spring-cloud-alibaba-consumer-mysql-8803name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloud