文章目录
一.微服务
1.1 什么是微服务
简单来说,微服务就是一种将一个单一应用程序拆分为一组小型服务的方法,拆分完成后,每一个服务都运行在独立的进程中,服务于服务之间采用轻量级的通信机制来进行沟通(Spring Cloud 中采用基于HTTP 的 RESTful API)。每一个服务,都是围绕具体的业务进行构建,例如一个电商系统,订单服务、支付服务、物流服务、会
员服务等等,这些拆分后的应用都是独立的应用,都可以独立的部署到生产环境中。就是在采用微服务之后,我们的项目不再拘泥于一种语言,可以 Java、Go、Python、PHP 等等,混合使用,这在传统的应用开发中,是无法想象的。而使用了微服务之后,我们可以根据业务上下文来选择合适的语言和构建工具进行构建。微服务可以理解为是 SOA 的一个传承,一个本质的区别是微服务是一个真正分布式、去中心化的,微服务的拆分比 SOA 更加彻底。
1.2 微服务与SOA的区别

- 微服务剔除SOA中复杂的ESB企业服务总线,所有的业务智能逻辑在服务内部处理,使用Http(Rest API)进行轻量化通讯
- SOA强调按水平架构划分为:前、后端、数据库、测试等,微服务强调按垂直架构划分,按业务能力划分,每个服务完成一种特定的功能,服务即产品
- SOA将组件以library的方式和应用部署在同一个进程中运行,微服务则是各个服务独立运行。
- 传统应用倾向于使用统一的技术平台来解决所有问题,微服务可以针对不同业务特征选择不同技术平台,去中心统一化,发挥各种技术平台的特长。
- SOA架构强调的是异构系统之间的通信和解耦合;(一种粗粒度、松耦合的服务架构)
- 微服务架构强调的是系统按业务边界做细粒度的拆分和部署。
1.3 微服务的优点
- 由于开发周期缩短,微服务架构有助于实现更加敏捷的部署和更新。
- 高度可扩展
- 出色的弹性 只要确保正确构建,这些独立的服务就不会彼此影响。这意味着,一个服务出现故障不会导致整个应用下线,这一点与单体式应用模型不同。
- 易于部署
- 易于访问 较好的容错性
- 技术选型灵活
二. Spring Cloud
2.1 介绍
Spring Cloud是分布式微服务架构下的一站式解决方案,是各个微服务架构落地技术的集合体,俗称微服务全家桶
Spring Cloud 是一系列框架的集合,Spring Cloud 内部包含了许多框架,这些框架互相协作,共同来构建分布式系统。利用这些组件,可以非常方便的构建一个分布式系统。
2.2 spring boot与spring cloud的关系
SpringBoot专注于快速方便的开发单个个体微服务。
SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务
SpringBoot可以离开SpringCloud独立使用开发项目,但是SpringCloud离不开SpringBoot,属于依赖的关系.
2.3 cloud的优势
Spring Cloud 可以理解为微服务这种思想在 Java 领域的一个具体落地。Spring Cloud 在发展之初,就借鉴了微服务的思想,同时结合 Spring Boot,Spring Cloud 提供了组件的一键式启动和部署的能力,极大的简化了微服务架构的落地。
Spring Cloud 这种框架,从设计之初,就充分考虑了分布式架构演化所需要的功能,例如服务注册、配置中心、消息总线以及负载均衡等。这些功能都是以可插拔的形式提供出来的,这样,在分布式系统不断演化的过程中,我们的 Spring Cloud 也可以非常方便的进化。
2.4 版本名称
不同于其他的框架,Spring Cloud 版本名称是通过 A(Angel)、B(Brixton)、C(Camden)、D(Dalston)、E(Edgware)、F(Finchley)… 这样来命名的,这些名字使用了伦敦地铁站的名字,目前最新版是 H (Hoxton)版。
Spring Cloud 中,除了大的版本之外,还有一些小版本,小版本命名方式如下:

- M ,M 版是 milestone 的缩写,所以我们会看到一些版本叫 M1、M2
- RC,RC 是 Release Candidate,表示该项目处于候选状态,这是正式发版之前的一个状态,所以我们会看到 RC1、RC2
- SR,SR 是 Service Release ,表示项目正式发布的稳定版,其实相当于 GA(GenerallyAvailable) 版。所以,我们会看到 SR1、SR2
- SNAPSHOT,这个表示快照版
2.5 相关组件
- Spring Cloud Netflix
- Spring Cloud Config,分布式配置中心,利用 Git/Svn 来集中管理项目的配置文件
- Spring Cloud Bus,消息总线,可以构建消息驱动的微服务,也可以用来做一些状态管理等
- Spring Cloud Consul,服务注册发现
- Spring Cloud Stream,基于 Redis、RabbitMQ、Kafka 实现的消息微服务
- Spring Cloud OpenFeign,提供 OpenFeign 集成到 Spring Boot 应用中的方式,主要解决微服务之间的调用问题
- Spring Cloud Gateway,Spring Cloud 官方推出的网关服务
- Spring Cloud Cloudfoundry,利用 Cloudfoundry 集成我们的应用程序
- Spring Cloud Security,在 Zuul 代理中,为 OAuth2 客户端认证提供支持
- Spring Cloud AWS ,快速集成亚马逊云服务
- Spring Cloud Contract,一个消费者驱动的、面向 Java 的契约框架
- Spring Cloud Zookeeper,基于 Apache Zookeeper 的服务注册和发现
- Spring Cloud Data Flow,在一个结构化的平台上,组成数据微服务
- Spring Cloud Kubernetes,Spring Cloud 提供的针对 Kubernetes 的支持
- Spring Cloud Function
- Spring Cloud Task,短生命周期的微服务
2.6 cloud与boot的版本对应
在构建spring cloud项目时,要注意与使用的boot版本一一对应起来

2.7 cloud与dubbo的对比

很明显,Spring Cloud的功能比DUBBO更加强大,涵盖面更广,而且作为Spring的拳头项目,它也能够与Spring Framework、Spring Boot、Spring Data、Spring Batch等其他Spring项目完美融合,这些对于微服务而言是至关重要的。使用Dubbo构建的微服务架构就像组装电脑,各环节我们的选择自由度很高,但是最终结果很有可能因为一条内存质量不行就点不亮了,总是让人不怎么放心,但是如果你是一名高手,那这些都不是问题;而Spring Cloud就像品牌机,在Spring Source的整合下,做了大量的兼容性测试,保证了机器拥有更高的稳定性,但是如果要在使用非原装组件外的东西,就需要对其基础有足够的了解。
三. Eureka
3.1 注册中心
Eureka 是 Spring Cloud 中的注册中心,类似于 Dubbo 中的 Zookeeper。那么到底什么是注册中心,我们为什么需要注册中心?
单体应用

在单体应用中,所有的业务都集中在一个项目中,当用户从浏览器发起请求时,直接由前端发起请求给后端,后端调用业务逻辑,给前端请求做出响应,完成一次调用。整个调用过程是一条直线,不需要服务之间的中转,所以没有必要引入注册中心
随着公司项目越来越大,我们会将系统进行拆分,例如一个电商项目,可以拆分为订单模块、物流模块、支付模块、CMS 模块等等。这样,当用户发起请求时,就需要各个模块之间进行协作,这样不可避免的要进行模块之间的调用。此时,我们的系统架构就会发生变化:

在这里,可以看到,模块之间的调用,变得越来越复杂,而且模块之间还存在强耦合。例如 A 调用B,那么就要在 A 中写上 B 的地址,也意味着 B 的部署位置要固定,同时,如果以后 B 要进行集群化部署,A 也需要修改。
3.2 Eureka
Eureka 由两部分:服务端和客户端,服务端就是注册中心,用来接收其他服务的注册,客户端则是一个 Java 客户端,用来注册,并可以实现负载均衡等功能。

从图中,我们可以看出,Eureka 中,有三个角色:
- Eureka Server:注册中心
- Eureka Provider:服务提供者
- Eureka Consumer:服务消费者
3.3 Eureka 搭建
-
引入依赖

-
开启eureka配置
@SpringBootApplication @EnableEurekaServer //表示开启 Eureka 的功能。 public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } } -
配置application.yml
spring: application: name: eureka server: port: 1111 eureka: client: #默认情况下,Eureka Server 也是一个普通的微服务,所以当它还是一个注册中心的时候, #他会有两层身份:1.注册中心;2.普通服务,即当前服务会自己把自己注册到自己上面来 因此要关闭它 register-with-eureka: false #表示是否从 Eureka Server 上获取注册信息 fetch-registry: false
配置完成后启动项目即可
注意:
如果在项目启动时,遇到 java.lang.TypeNotPresentException: Type
javax.xml.bind.JAXBContext not present 异常,这是因为 JDK9 以上,移除了 JAXB,这个时候,只需要我们手动引入 JAXB 即可。
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
-
访问 localhost:1111 后台管理页面

3.4 集群搭建
使用了注册中心之后,所有的服务都要通过服务注册中心来进行信息交换。服务注册中心的稳定性就非常重要了,一旦服务注册中心掉线,会影响到整个系统的稳定性。所以,在实际开发中,Eureka 一般都是以集群的形式出现的。
Eureka 集群,实际上就是启动多个 Eureka 实例,多个 Eureka 实例之间,互相注册,互相同步数据,共同组成一个 Eureka 集群。
-
准备工作
修改本机hosts文件
C:\Windows\System32\drivers\etc添加本地解析映射配置
127.0.0.1 eurekaA eurekaB -
将此模块名改为eurekaA并修改yml配置文件
spring: application: name: eureka server: port: 1111 eureka: client: #搭建集群 因此要将它注册到eureka 开启 register-with-eureka: true #表示是否从 Eureka Server 上获取注册信息 fetch-registry: true service-url: defaultZone: http://eurekaB:1112/eureka #将此实例注册到另一个注册中心上 instance: hostname: eurekaA #给该节点实例起名 -
复制模块eurekaA 命名为eurekaB 修改B的yml文件
spring: application: name: eureka server: port: 1112 eureka: client: #搭建集群 因此要将它注册到eureka 开启 register-with-eureka: true #表示是否从 Eureka Server 上获取注册信息 fetch-registry: true service-url: defaultZone: http://eurekaA:1111/eureka #将此实例注册到另一个注册中心上 instance: hostname: eurekaB #给该节点实例起名
分别启动2个实例即可 观察2个后台管理界面


此时,集群就算搭建成功了
3.5 工作原理
Eureka 本身可以分为两大部分,Eureka Server 和 Eureka Client
3.5.1 Eureka Server
Eureka Server 主要对外提供了三个功能:
- 服务注册,所有的服务都注册到 Eureka Server 上面来
- 提供注册表,注册表就是所有注册上来服务的一个列表,Eureka Client 在调用服务时,需要获取这个注册表,一般来说,这个注册表会缓存下来,如果缓存失效,则直接获取最新的注册表
- 同步状态,Eureka Client 通过注册、心跳等机制,和 Eureka Server 同步当前客户端的状态
3.5.2 Eureka Client
Eureka Client 主要是用来简化每一个服务和 Eureka Server 之间的交互。Eureka Client 会自动拉取、更新以及缓存 Eureka Server 中的信息,这样,即使 Eureka Server 所有节点都宕机,Eureka Client依然能够获取到想要调用服务的地址(但是地址可能不准确)。
-
服务注册
服务提供者将自己注册到服务注册中心(Eureka Server),需要注意,所谓的服务提供者,只是一个业务上上的划分,本质上他就是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,他需要提供自身的一些元数据信息,例如 IP 地址、端口、名称、运行状态等等
-
服务续约
Eureka Client 注册到 Eureka Server 上之后,注册成功后,默认情况下,Eureka CLient 每隔 30 秒就要向 Eureka Server 发送一条心跳消息,来告诉 Eureka Server 我还在运行。如果 Eureka Server 连续 90 秒都有没有收到 Eureka Client 的续约消息(连续三次没发送),它会认为 Eureka Client 已经掉线了,会将掉线的 Eureka Client 从当前的服务注册列表中剔除。
服务续约,有两个相关的属性(一般不建议修改):
eureka.instance.lease-renewal-interval-in-seconds=30 #表示服务的续约时间,默认是 30 秒 eureka.instance.lease-expiration-duration-in-seconds=90 #服务失效时间,默认是 90 秒 -
服务下线
当 Eureka Client 下线时,它会主动发送一条消息,告诉 Eureka Server ,我下线啦。
-
获取注册表信息
Eureka Client 从 Eureka Server 上获取服务的注册信息,并将其缓存在本地。本地客户端,在需要调用远程服务时,会从该信息中查找远程服务所对应的 IP 地址、端口等信息。Eureka Client 上缓存的服务注册信息会定期更新(30 秒),如果 Eureka Server 返回的注册表信息与本地缓存的注册表信息不同的话,Eureka Client 会自动处理。
这里,也涉及到两个属性
eureka.client.fetch-registry=true #是否允许获取注册表信息 eureka.client.registry-fetch-interval-seconds=30 #Eureka Client 上缓存的服注册信息,定期更新的时间间隔,默认 30 秒:
3.6 自我保护机制
Eureka在运行期间会统计心跳失败的比例,在15分钟内是否低于85%,如果出现了低于的情况,则会开启自我保护机制
默认情况下,当eureka server在一定时间内没有收到实例的心跳,便会把该实例从注册表中删除(默认是90秒),但是,如果短时间内丢失大量的实例心跳,便会触发eureka server的自我保护机制,比如在开发测试时,需要频繁地重启微服务实例,但是我们很少会把eureka server一起重启(因为在开发过程中不会修改eureka注册中心),当一分钟内收到的心跳数大量减少时,会触发该保护机制。可以在eureka管理界面看到Renews threshold和Renews(last min),当后者(最后一分钟收到的心跳数)小于前者(心跳阈值)的时候,触发保护机制,会出现红色的警告:
EMERGENCY!EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT.RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEGING EXPIRED JUST TO BE SAFE.

从警告中可以看到,eureka认为虽然收不到实例的心跳,但它认为实例还是健康的,eureka会保护这些实例,不会把它们从注册表中删掉。
该保护机制的目的是避免网络连接故障,在发生网络故障时,微服务和注册中心之间无法正常通信,但服务本身是健康的,不应该注销该服务,如果eureka因网络故障而把微服务误删了,那即使网络恢复了,该微服务也不会重新注册到eureka server了,因为只有在微服务启动的时候才会发起注册请求,后面只会发送心跳和服务列表请求,这样的话,该实例虽然是运行着,但永远不会被其它服务所感知。所以,eureka server在短时间内丢失过多的客户端心跳时,会进入自我保护模式,该模式下,eureka会保护注册表中的信息,不在注销任何微服务,当网络故障恢复后,eureka会自动退出保护模式。自我保护模式可以让集群更加健壮。
但是我们在开发测试阶段,需要频繁地重启发布,如果触发了保护机制,则旧的服务实例没有被删除,这时请求有可能跑到旧的实例中,而该实例已经关闭了,这就导致请求错误,影响开发测试。所以,在开发测试阶段,我们可以把自我保护模式关闭,只需在eureka server配置文件中加上如下配置即可:
eureka.server.enable-self-preservation=false
自我保护模式阈值计算:
每个instance的预期心跳数目 = 60/每个instance的心跳间隔秒数(默认30s)
阈值 = 所有注册到服务的instance的数量的预期心跳之和 *自我保护系数(默认为0.85)
以上的参数都可配置的:
eureka.server.renewal-percent-threshold=0.85 #自我保护系数
3.7 Eureka 集群原理

在这个集群架构中,Eureka Server 之间通过 Replicate 进行数据同步,不同的 Eureka Server 之间不区分主从节点,所有节点都是平等的。节点之间,通过指定 serviceUrl 来互相注册,形成一个集群,进而提高节点的可用性。
在 Eureka Server 集群中,如果有某一个节点宕机,Eureka Client 会自动切换到新的 Eureka Server上。每一个 Eureka Server 节点,都会互相同步数据。Eureka Server 的连接方式,可以是单线的,就是 A–>B–>C ,此时,A 的数据也会和 C 之间互相同步。但是一般不建议这种写法,在我们配置serviceUrl 时,可以指定多个注册地址,即 A 可以即注册到 B 上,也可以同时注册到 C 上。
Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。
Eureka 分区:
- region:地理上的不同区域
- zone:具体的机房
在同一个分区里的client和server会优先进行心跳同步
3.8 服务注册
服务注册就是把一个微服务注册到 Eureka Server 上,这样,当其他服务需要调用该服务时,只需要从Eureka Server 上查询该服务的信息即可。
-
provider项目构建

-
配置服务注册
server: port: 1113 spring: application: name: provider eureka: client: service-url: defaultZone: http://eurekaA:1111/eureka #这里注册到A后 A会自动同步到B
注册成功
3.9 服务消费
-
consumer consumer模块搭建

-
配置
server: port: 1114 spring: application: name: consumer eureka: client: service-url: defaultZone: http://eurekaA:1111/eureka #这里注册到A后 A会自动同步到B -
在provider中添加提供服务的测试接口
@RestController public class ProviderController{ @GetMapping("/provider") public String test() { return "hello provider"; } } -
服务消费
配置完成后,假设我们现在想在 consumer 中调用 provider 提供的服务,我们可以直接将调用写死,就是说,整个调用过程不会涉及到 Eureka Server。
@GetMapping("/test1") public String test1() { HttpURLConnection connection = null; try { URL url = new URL("http://127.0.0.1:1113/provider"); connection = (HttpURLConnection) url.openConnection(); if (connection.getResponseCode() == 200) { BufferedReader bf = new BufferedReader(new InputStreamReader(connection.getInputStream())); String s = bf.readLine(); bf.close(); return s; } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return "error"; }这是一段利用了 HttpUrlConnection 来发起的请求,请求中 provider 的地址写死了,意味着 provider和 consumer 高度绑定在一起,这个不符合微服务的思想。
要改造它,我们可以借助 Eureka Client 提供的 DiscoveryClient 工具,利用这个工具,我们可以根据服务名从 Eureka Server 上查询到一个服务的详细信息,改造后的代码如下:
@Autowired DiscoveryClient discoveryClient; @GetMapping("/test2") public String test2() { List<ServiceInstance> provider = discoveryClient.getInstances("provider"); ServiceInstance instance = provider.get(0); // String host = instance.getHost(); // int port = instance.getPort(); // String instanceId = instance.getInstanceId(); // Map<String, String> metadata = instance.getMetadata(); // String scheme = instance.getScheme(); // URI uri = instance.getUri(); // String serviceId = instance.getServiceId(); // System.out.println("host = " + host); // System.out.println("port = " + port); // System.out.println("instanceId = " + instanceId); // System.out.println("metadata = " + metadata); // System.out.println("scheme = " + scheme); // System.out.println("uri = " + uri); // System.out.println("serviceId = " + serviceId); StringBuilder sb = new StringBuilder(); sb.append("http://") .append(instance.getUri()) .append("/provider"); HttpURLConnection connection = null; try { URL url = new URL(sb.toString()); connection = (HttpURLConnection) url.openConnection(); if (connection.getResponseCode() == 200) { BufferedReader bf = new BufferedReader(new InputStreamReader(connection.getInputStream())); String s = bf.readLine(); bf.close(); return s; } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return "error"; }注意,DiscoveryClient 查询到的服务列表是一个集合,因为服务在部署的过程中,可能是集群化部署,集合中的每一项就是一个实例。
模拟演示一下集群化部署。
为了效果明显,首先,修改 provider 中的 hello 接口:
@Value("${server.port}") private int port; @GetMapping("/provider") public String test() { return "hello provider:" + port; }修改完成后,对 provider 进行打包。provider 打包成功之后,我们在命令行启动两个 provider 实例:
java -jar provider-0.0.1-SNAPSHOT.jar --server.port=1113 java -jar provider-0.0.1-SNAPSHOT.jar --server.port=1116启动完成后,检查 Eureka Server 上,这两个 provider 是否成功注册上来。

注册成功后,在 consumer 中再去调用 provider,DiscoveryClient 集合中,获取到的就不是一个实例了,而是两个实例。这里我们可以手动实现一个负载均衡:int count = 0; @GetMapping("/test3") public String test3() { List<ServiceInstance> provider = discoveryClient.getInstances("provider"); ServiceInstance instance = provider.get((count++) % provider.size()); StringBuilder sb = new StringBuilder(); sb.append(instance.getUri()) .append("/provider"); HttpURLConnection connection = null; try { URL url = new URL(sb.toString()); connection = (HttpURLConnection) url.openConnection(); if (connection.getResponseCode() == 200) { BufferedReader bf = new BufferedReader(new InputStreamReader(connection.getInputStream())); String s = bf.readLine(); bf.close(); return s; } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return "error"; }测试后发现每次请求都会调用不同的提供者服务

3.9.1 代码简化
从两个方面进行改造:
- Http 调用
- 负载均衡
Http 调用,我们使用 Spring 提供的 RestTemplate 来实现
首先,在启动类里提供一个 RestTemplate 的实例:
@Bean
RestTemplate restTemplateOne(){
return new RestTemplate();
}
代码简化
@Autowired
RestTemplate restTemplateOne;
@GetMapping("/test2")
public String test2() {
List<ServiceInstance> provider = discoveryClient.getInstances("provider");
ServiceInstance instance = provider.get(0);
StringBuilder sb = new StringBuilder();
sb.append(instance.getUri())
.append("/provider");
String result = restTemplateOne.getForObject(sb.toString(), String.class);
return result;
}
实现负载均衡
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
@GetMapping("/test4")
public String test4() {
String result = restTemplate.getForObject("http://provider/provider", String.class);
return result;
}
注意,此时在写请求地址时,直接写的是服务提供者的服务名,也就是写的模糊的地址,因为在RestTemplate上加了@LoadBalanced注解后,调用时,它会帮你自动的去实现负载均衡,找到相应的具体服务地址拼接后发送出去,如果在这里直接写具体服务地址的话 会报错
3.10 RestTemplate
RestTemplate 是从 Spring3.0 开始支持的一个 Http 请求工具,这个请求工具和 Spring Boot 无关,更和 Spring Cloud 无关。RestTemplate 提供了常见的 REST 请求方法模板,例如 GET、POST、PUT、DELETE 请求以及一些通用的请求执行方法 exchange 和 execute 方法。
RestTemplate 本身实现了 RestOperations 接口,而在RestOperations 接口中,定义了常见的RESTful 操作,这些操作在 RestTemplate 中都得到了很好的实现。
3.10.1 GET
测试:
在provider中提供测试接口
@GetMapping("/provider1")
public String test1(String name) {
return "hello" + name;
}
在consumer中消费
@GetMapping("/test5")
public void test5() {
String result = restTemplate.getForObject("http://provider/provider1?name={1}", String.class, "zs");
System.out.println("result = " + result);
Map<String, Object> map = new HashMap<>();
map.put("name", "zs");
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://provider/provider1?name={name}",
String.class, map);
String body = responseEntity.getBody();
System.out.println("body = " + body);
HttpStatus statusCode = responseEntity.getStatusCode();
System.out.println("statusCode = " + statusCode);
int codeValue = responseEntity.getStatusCodeValue();
System.out.println("codeValue = " + codeValue);
HttpHeaders headers = responseEntity.getHeaders();
System.out.println("headers:----------------------");
headers.forEach((x,y)-> {
System.out.println(x+":"+y);
});
}

在 RestTemplate 中,关于 GET 请求,一共有如下两大类方法:.

这两大类方法实际上是重载的,唯一不同的,就是返回值类型。
getForObject 返回的是一个对象,这个对象就是服务端返回的具体值。getForEntity 返回的是一个ResponseEntity,这个ResponseEntity 中除了服务端返回的具体数据外,还保留了 Http 响应头的数据
看清楚两者的区别之后,接下来看下两个各自的重载方法,getForObject 和 getForEntity 分别有三个重载方法,两者的三个重载方法基本都是一致的。所以,这里,我们主要看其中一种。三个重载方法,其实代表了三种不同的传参方式。
@GetMapping("/test6")
public void test6() throws UnsupportedEncodingException {
//第一种
ResponseEntity<String> entity = restTemplate.getForEntity("http://provider/provider1?name={1}", String.class,
"李四");
System.out.println("body1:" + entity.getBody());
//第二种
URI uri = URI.create("http://provider/provider1?name="+URLEncoder.encode("李四", "UTF-8"));
ResponseEntity<String> forEntity = restTemplate.getForEntity(uri, String.class);
System.out.println("body2:" + forEntity.getBody());
//第三种
Map<String, Object> map = new HashMap<>();
map.put("name", "zs");
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://provider/provider1?name={name}",
String.class, map);
System.out.println("body3:" + responseEntity.getBody());
}
3.10.2 POST
首先在 provider 中提供两个 POST 接口,同时,因为 POST 请求可能需要传递 JSON,所以,这里我们创建一个普通的 Maven 项目作为 commons 模块,然后这个 commons 模块被 provider 和consumer 共同引用,这样我们就可以方便的传递 JSON 了。
commons 模块创建成功后,首先在 commons 模块中添加 User 对象,然后该模块分别被 provider 和consumer 引用。
然后,我们在 provider 中,提供和两个 POST 接口:
/**
* 以key-value方式传参
* @param user
* @return
*/
@PostMapping("/user")
public User test1(User user) {
return user;
}
/**
* 以json方式传参
* @param user
* @return
*/
@PostMapping("/user1")
public User test2(@RequestBody User user) {
return user;
}
在consumer中调用
@GetMapping("/test7")
public void test7() {
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("id", 1);
map.add("username", "zhangsan");
map.add("password", "1230");
User user = restTemplate.postForObject("http://provider/user", map, User.class);
System.out.println("user = " + user);
user.setId(2);
User user1 = restTemplate.postForObject("http://provider/user1", user, User.class);
System.out.println("user1 = " + user1);
}
post 参数到底是 key/value 形式还是 json 形式,主要看第二个参数,如果第二个参数是MultiValueMap ,则参数是以 key/value 形式来传递的,如果是一个普通对象,则参数是以 json 形式来传递的。
有的时候,当我执行完一个 post 请求之后,立马要进行重定向,一个非常常见的场景就是注册,注册是一个 post 请求,注册完成之后,立马重定向到登录页面去登录。对于这种场景,我们就可以使用 postForLocation。
首先我们在 provider 上提供一个用户注册接口:
@Controller
public class RegisterController {
@PostMapping("/register")
public String register(@RequestBody User user) {
return "redirect:http://provider/login?username=" + user.getUsername();
}
@GetMapping("/login")
@ResponseBody
public String login(String username) {
System.out.println("1111");
return "username"+username;
}
}
注意,这里的 post 接口,响应一定是 302,否则postForLocation 无效重定向的地址,一定要写成绝对路径,不要写相对路径,否则在 consumer 中调用时会出问题
@GetMapping("/test8")
public String test8() {
User user = new User();
user.setId(3);
user.setUsername("lisi");
user.setPassword("123");
//调用该方法返回的是一个 Uri,这个 Uri 就是重定向的地址(里边也包含了重定向的参数),拿到 Uri 之后,就可以直接发送新的请求了。
URI uri = restTemplate.postForLocation("http://provider/register", user);
String s = restTemplate.getForObject(uri, String.class);
System.out.println("uri = " + uri);
System.out.println("s = " + s);
return s;
}
3.10.3 PUT
首先在 provider 中提供一个 PUT 接口:
/**
* 以key-value方式传参
* @param user
* @return
*/
@PutMapping("/user3")
public void test3(User user) {
System.out.println("user = " + user);
}
/**
* 以json方式传参
* @param user
* @return
*/
@PutMapping("/user4")
public void test4(@RequestBody User user) {
System.out.println("user = " + user);
}
在consumer中测试
@GetMapping("/test9")
public void test9() {
User user = new User();
user.setId(3);
user.setUsername("lisi");
user.setPassword("123");
restTemplate.put("http://provider/user4", user);
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("id", 1);
map.add("username", "zhangsan");
map.add("password", "1230");
restTemplate.put("http://provider/user3",map);
}
3.10.4 DELETE
首先在 provider 中提供一个 DELETE接口:
/**
* 以地址栏路径传参
* @param id
* @return
*/
@DeleteMapping("/delete/{id}")
public void delete1(@PathVariable Integer id) {
System.out.println("id = " + id);
}
/**
* 以key-value方式传参
* @param id
* @return
*/
@DeleteMapping("/delete")
public void delete2(Integer id) {
System.out.println("id = " + id);
}
在consumer中测试
@GetMapping("/test10")
public void test10() {
restTemplate.delete("http://provider/delete/{1}",1);
restTemplate.delete("http://provider/delete?id={1}",2);
}
3.11 客户端负载均衡
客户端负载均衡就是相对服务端负载均衡而言的。
服务端负载均衡,就是传统的 Nginx 的方式,用 Nginx 做负载均衡,我们称之为服务端负载均衡:

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

客户端负载均衡,就是调用的客户端本身是知道所有 Server 的详细信息的,当需要调用 Server 上的接口的时候,客户端从自身所维护的 Server 列表中,根据提前配置好的负载均衡策略,自己挑选一个
Server 来调用,此时,客户端知道它所调用的是哪一个 Server。
在 RestTemplate 中,要想使用负载均衡功能,只需要给
RestTemplate 实例上添加一个@LoadBalanced 注解即可,此时,RestTemplate 就会自动具备负载均衡功能,这个负载均衡就是客户端负载均衡。
3.12 负载均衡原理
在 Spring Cloud 中,实现负载均衡非常容易,只需要添加 @LoadBalanced 注解即可。只要添加了该注解,一个原本普普通通做 Rest 请求的工具 RestTemplate 就会自动具备负载均衡功能,这个是怎么实现的呢?
整体上来说,这个功能的实现就是三个核心点:
- 从 Eureka Client 本地缓存的服务注册信息中,选择一个可以调用的服务
- 根据 1 中所选择的服务,重构请求 URL 地址
- 将 1、2 步的功能嵌入到 RestTemplate 中
四. Consul
在 Spring Cloud 中,大部分组件都有备选方案,例如注册中心,除了常见 Eureka 之外,像zookeeper 我们也可以直接使用在 Spring Cloud 中,还有另外一个比较重要的方案,就是 Consul。
Consul 是 HashiCorp 公司推出来的开源产品。主要提供了:服务发现、服务隔离、服务配置等功能。
相比于 Eureka 和 zookeeper,Consul 配置更加一站式,因为它内置了很多微服务常见的需求:服务发现与注册、分布式一致性协议实现、健康检查、键值对存储、多数据中心等,我们不再需要借助第三方组件来实现这些功能。
4.1 安装
不同于 Eureka ,Consul 使用 Go 语言开发,所以,使用 Consul ,我们需要先安装软件。
-
在 Linux 中,首先执行如下命令下载 Consul:
wget https://releases.hashicorp.com/consul/1.8.4/consul_1.8.4_linux_386.zip或者在官网直接下载zip包
https://www.consul.io/ -
解压
unzip consul_1.8.4_linux_386.zip -
解压完成后,我们在当前目录下就可以看到 consul 文件,然后执行如下命令,启动 Consul:
./consul agent -dev -ui -node=consul-dev -client=192.168.83.128启动成功后,在物理机中,我们可以直接访问 Consul 的后台管理页面(注意,这个访问要确保 8500端口可用,或者直接关闭防火墙):

4.2 Consul使用
4.2.1 provider工程搭建
-
引入依赖

-
添加如下配置
server.port=1200
spring.application.name=provider_consul
##consul相关配置
spring.cloud.consul.host=192.168.83.128
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.service-name=provider_consul
spring.cloud.consul.discovery.register=true
- 在项目启动类上开启服务发现的功能:
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderConsulApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderConsulApplication.class, args);
}
}
- 提供测试接口
@RestController
public class ProviderController {
@GetMapping("/hello")
public String test(String name) {
return "provider:" + name;
}
}
启动项目后,去 consul 后台管理页面看是否注册成功
4.2.2 consumer工程搭建
-
复制provider工程, 修改配置文件
server.port=1201 spring.application.name=consumer_consul spring.cloud.consul.host=192.168.83.128 spring.cloud.consul.port=8500 spring.cloud.consul.discovery.service-name=consumer_consul -
开启服务发现,并添加 RestTemplate:
@SpringBootApplication @EnableDiscoveryClient public class ConsumerConsulApplication { public static void main(String[] args) { SpringApplication.run(ConsumerConsulApplication.class, args); } @Bean RestTemplate restTemplate(){ return new RestTemplate(); } } -
测试
@RestController public class ConsumerController { @Autowired RestTemplate restTemplate; @Autowired LoadBalancerClient loadBalancerClient; @GetMapping("/test") public String test() { ServiceInstance instance = loadBalancerClient.choose("provider_consul"); System.out.println(instance.getUri()); String s = restTemplate.getForObject(instance.getUri() + "/hello?name={1}", String.class, "zhangsan"); return s; } }
五.Hystrix
5.1 基本介绍
Hystrix 叫做断路器/熔断器。微服务系统中,整个系统出错的概率非常高,因为在微服务系统中,涉及到的模块太多了,每一个模块出错,都有可能导致整个服务出错,当所有模块都稳定运行时,整个服务才算是稳定运行。
我们希望当整个系统中,某一个模块无法正常工作时,能够通过我们提前配置的一些东西,来使得整个系统正常运行,即单个模块出问题,不影响整个系统。
5.2 基本用法
-
搭建工程 引入依赖

-
项目创建成功后,添加如下配置,将 Hystrix 注册到 Eureka 上:
server: port: 1115 spring: application: name: hystrix eureka: client: service-url: defaultZone: http://eurekaA:1111/eureka #这里注册到A后 A会自动同步到B -
在项目启动类上添加如下注解,开启断路器
//@SpringBootApplication //@EnableCircuitBreaker @SpringCloudApplication //组合注解 包含SpringBootApplication EnableCircuitBreaker public class HystrixApplication { public static void main(String[] args) { SpringApplication.run(HystrixApplication.class, args); } @Bean @LoadBalanced RestTemplate restTemplate(){ return new RestTemplate(); } } -
提供测试接口
@Service public class HystrixService { @Autowired RestTemplate restTemplate; /** * 在这个方法中,我们将发起一个远程调用,去调用 provider 中提供的 /provider测试接口 * 但是,这个调用可能会失败。 * 我们在这个方法上添加 @HystrixCommand 注解,配置 fallbackMethod 属性,这个属性表示该方法调用失败时的临时替代方法 * @return */ @HystrixCommand(fallbackMethod = "error") public String hystrix(){ return restTemplate.getForObject("http://provider/provider", String.class); } /** * 注意,这个方法名字要和 fallbackMethod一致 方法返回值也要和对应的方法一致 * 当然,如果有需要,也可以在这降级的方法上在进行降级 * 添加 @HystrixCommand(fallbackMethod = "xxx") 然后准备xxx方法即可 以此类推 * @return */ public String error(){ return "服务降级了 预备页面"; } }@RestController public class HystrixController { @Autowired private HystrixService hystrixService; @GetMapping("/hystrix") public String test(){ return hystrixService.hystrix(); } }我们同时启动2个provider(端口号不同,集群)进行测试, 在启动consumer, 用之前写的带有负载均衡的方法进行测试, 发现可以进行正常的进行负载均衡, 然后手动关闭一个provider工程, 发现consumer在进行负载均衡时,当负载到被关闭的provider时, 会出现错误页面

然而我们不想在某个模块挂了之后 ,影响整个应用的正常访问,因此就需要使用到刚才写的带断路器的工程, 启动该工程, 同样的情况, 如果某服务挂掉后, 此时该工程刚好负载到挂掉的某服务时, 访问不通时, 就会直接去访问我们已经提前写好的降级后的接口中去了, 这样就不会因为某一个服务挂掉进而影响整个应用的正常使用
5.3 请求命令
请求命令就是以继承类的方式来替代前面的注解方式。
自定义一个 HelloCommand:
public class HelloCommand extends HystrixCommand<String> {
RestTemplate restTemplate;
public HelloCommand(Setter setter, RestTemplate restTemplate) {
super(setter);
this.restTemplate = restTemplate;
}
@Override
protected String run() throws Exception {
return restTemplate.getForObject("http://provider/provider", String.class);
}
/**
* 请求失败时的回调
* @return
*/
@Override
protected String getFallback() {
return "服务降级了 降级方法";
}
}
测试调用
@GetMapping("/hystrix1")
public String test1() {
HelloCommand helloCommand =
new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("test")),
restTemplate);
//String execute = helloCommand.execute();
//return execute;
Future<String> queue = helloCommand.queue(); //异步调用
String s = null;
try {
s = queue.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return s;
}
注意:
- 一个实例只能执行一次
- 可以直接执行,也可以先入队,后执行
也可以通过注解来实现异步调用
首先,定义如下方法,返回 Future :
@HystrixCommand(fallbackMethod = "error")
public Future<String> hystrix1() {
return new AsyncResult<String>() {
@Override
public String invoke() {
return restTemplate.getForObject("http://provider/provider", String.class);
}
};
}
调用测试
@GetMapping("/hystrix2")
public String test2() throws ExecutionException, InterruptedException {
Future<String> stringFuture = hystrixService.hystrix1();
return stringFuture.get();
}
5.4 异常处理
就是当发起服务调用时,如果不是 provider 的原因导致请求调用失败,而是 consumer 中本身代码有问题导致的请求失败,即 consumer 中抛出了异常,这个时候,也会自动进行服务降级,只不过这个时候降级,我们还需要知道到底是哪里出异常了。
如下示例代码,如果 hystrix方法中,执行时抛出异常,那么一样也会进行服务降级,进入到 error 方法中,在 error 方法中,我们可以获取到异常的详细信息
@HystrixCommand(fallbackMethod = "error")
public String hystrix() {
int i = 1 / 0;
return restTemplate.getForObject("http://provider/provider", String.class);
}
` public String error(Throwable t) {
System.out.println(t.getMessage());
return "服务降级了 预备页面";
}
当然也可以通过继承的方式:
public class HelloCommand extends HystrixCommand<String> {
RestTemplate restTemplate;
public HelloCommand(Setter setter, RestTemplate restTemplate) {
super(setter);
this.restTemplate = restTemplate;
}
@Override
protected String run() throws Exception {
int i = 1 / 0;
return restTemplate.getForObject("http://provider/provider", String.class);
}
/**
* 请求失败时的回调
* @return
*/
@Override
protected String getFallback() {
System.out.println(getExecutionException().getMessage());
return "服务降级了 降级方法";
}
}
如果是通过继承的方式来做 Hystrix,在 getFallback 方法中,我们可以通过 getExecutionException 方法来获取执行的异常信息。
另一种可能性(作为了解)。如果抛异常了,我们希望异常直接抛出,不要服务降级,那么只需要配置忽略某一个异常即可
@HystrixCommand(fallbackMethod = "error",ignoreExceptions = ArithmeticException.class)
public String hystrix() {
int i = 1 / 0;
return restTemplate.getForObject("http://provider/provider", String.class);
}
在注解里添加ignoreExceptions 属性 这个配置表示当 hystrix方法抛出 ArithmeticException 异常时,不要进行服务降级,直接将错误抛出。

5.5 请求缓存
请求缓存就是在 consumer 中调用同一个接口,如果参数相同,则可以使用之前缓存下来的数据。
首先 在 hystrix 的请求方法中,添加如下注解:
/**
* @CacheResult 这个注解表示该方法的请求结果会被缓存起来,默认情况下,缓存的 key 就是方法的参数,缓存的 value 就是方法的返回值。
* @param name
* @return
*/
@HystrixCommand(fallbackMethod = "error1")
@CacheResult
public String hystrix2(String name) {
return restTemplate.getForObject("http://provider/provider1?name={1}", String.class,name);
}
public String error1(String name,Throwable t) {
System.out.println(t.getMessage());
return "服务降级了 预备页面";
}
这个配置完成后,缓存并不会立即生效,一般来说,我们使用缓存,都有一个缓存生命周期这样一个概念。
这里也一样,我们需要初始化 HystrixRequestContext,初始化完成后,缓存开始生效,HystrixRequestContext close 之后,缓存失效。
@GetMapping("/hystrix3")
public String test3() {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
hystrixService.hystrix2("测试");
hystrixService.hystrix2("测试");
context.close();
return null;
}
在 context.close 之前,缓存是有效的,close 之后,缓存就失效了。也就是说,访问一次 hystrix2接口,provider 只会被调用一次(第二次使用的缓存),如果再次调用 hystrix2接口,之前缓存的数据是失效的。
测试发现,调用2次provider后 provider控制台只打印了一次日志,第二次调用其实consumer就直接拿的是缓存里的数据
默认情况下,缓存的 key 就是所调用方法的参数,如果参数有多个,就是多个参数组合起来作为缓存的key。
但是,如果有多个参数,但是又只想使用其中一个作为缓存的 key,那么我们可以通过 @CacheKey 注解来解决。
@HystrixCommand(fallbackMethod = "error1")
@CacheResult
public String hystrix2(@CacheKey String name, String word) {
return restTemplate.getForObject("http://provider/provider1?name={1}", String.class,name);
}
上面这个配置,虽然有两个参数,但是缓存时以 name 为准。也就是说,两次请求中,只要 name 一样,即使 word不一样,第二次请求也可以使用第一次请求缓存的结果。
另外还有一个注解叫做 @CacheRemove()。在做数据缓存时,如果有一个数据删除的方法,我们一般除了删除数据库中的数据,还希望能够顺带删除缓存中的数据,这个时候 @CacheRemove() 就派上用场了。
@CacheRemove() 在使用时,必须指定 commandKey 属性,commandKey 其实就是缓存方法的名字,指定了 commandKey,@CacheRemove 才能找到数据缓存在哪里了,进而才能成功删除掉数据。
@HystrixCommand(fallbackMethod = "error1")
@CacheResult
public String hystrix2(String name) {
return restTemplate.getForObject("http://provider/provider1?name={1}", String.class, name);
}
@HystrixCommand(fallbackMethod = "error1")
@CacheRemove(commandKey = "hystrix2")
public String deleteHystrix(String name) {
return null;
}
@GetMapping("/hystrix3")
public String test3() {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
//第一请求完,数据已经缓存下来了
hystrixService.hystrix2("测试");
//删除数据,同时缓存中的数据也会被删除
hystrixService.deleteHystrix("测试");
//第二次请求时,虽然参数还是 测试,但是缓存数据已经没了,所以这一次,provider 还是会收到请求
hystrixService.hystrix2("测试");
context.close();
return null;
}
如果是继承的方式使用 Hystrix ,只需要重写 getCacheKey 方法即可:
public class HelloCommand extends HystrixCommand<String> {
RestTemplate restTemplate;
private String name;
public HelloCommand(Setter setter, RestTemplate restTemplate,String name) {
super(setter);
this.restTemplate = restTemplate;
this.name=name;
}
@Override
protected String run() throws Exception {
return restTemplate.getForObject("http://provider/provider1?name={1}", String.class,name);
}
/**
* 请求失败时的回调
* @return
*/
@Override
protected String getFallback() {
System.out.println(getExecutionException().getMessage());
return "服务降级了 降级方法";
}
/**
* 这里缓存是用name做缓存的 所以返回时也直接返回name
* @return
*/
@Override
protected String getCacheKey() {
return name;
}
}
@GetMapping("/hystrix1")
public String test1() {
//初始化HystrixRequestContext
HystrixRequestContext context = HystrixRequestContext.initializeContext();
HelloCommand helloCommand =
new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("test")),
restTemplate,"测试");
String execute = helloCommand.execute();
HelloCommand helloCommand1 =
new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("test")),
restTemplate,"测试");
Future<String> queue = helloCommand1.queue(); //异步调用
String s = null;
try {
s = queue.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
context.close();
return s;
}
5.6 请求合并
如果 consumer 中,频繁的调用 provider 中的同一个接口,在调用时,只是参数不一样,那么这样情况下,我们就可以将多个请求合并成一个,这样可以有效提高请求发送的效率。
首先在 provider 中提供一个请求合并的接口
@RestController
public class UserController {
@GetMapping("/user/{ids}")//假设 consumer 传过来的多个 id 的格式是 1,2,3,4....
public List<User> getUsers(@PathVariable String ids) {
System.out.println(ids);
String[] split = ids.split(",");
ArrayList<User> list = new ArrayList<>(split.length + 1);
for (String s : split) {
User user = new User();
user.setId(Integer.parseInt(s));
list.add(user);
}
return list;
}
}
这个接口既可以处理合并之后的请求,也可以处理单个请求(单个请求的话,List 集合中就只有一项数据。)
然后,在 Hystrix 中,定义 UserService:
@Service
public class UserService {
@Autowired
RestTemplate restTemplate;
public List<User> getUsers(List<Integer> ids) {
User[] users = restTemplate.getForObject("http://provider/user/{1}", User[].class, StringUtils.join(ids,
","));
return Arrays.asList(users);
}
}
接下来定义 UserBatchCommand ,相当于之前的 HelloCommand:
public class UserBatchCommand extends HystrixCommand<List<User>> {
List<Integer> ids;
UserService userService;
public UserBatchCommand(List<Integer> ids, UserService userService) {
super(HystrixCommandGroupKey.Factory.asKey("batchCommand"));
this.ids = ids;
this.userService = userService;
}
@Override
protected List<User> run() throws Exception {
return userService.getUsers(ids);
}
}
最后,定义最最关键的请求合并方法:
public class UserCollapseCommand extends HystrixCollapser<List<User>, User, Integer> {
private UserService userService;
private Integer id;
public UserCollapseCommand(UserService userService, Integer id) {
super(HystrixCollapser.Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("userCollapseCommand"))
.andCollapserPropertiesDefaults(HystrixCollapserProperties.Setter()
.withTimerDelayInMilliseconds(200))); //定义超时时间,当超过此时间时的请求到来时,请求会进入到下一批请求中合并发出
this.userService = userService;
this.id = id;
}
/**
* 请求参数
* @return
*/
@Override
public Integer getRequestArgument() {
return id;
}
/**
* 请求合并方法
* @param collection
* @return
*/
@Override
protected HystrixCommand<List<User>> createCommand(Collection<CollapsedRequest<User, Integer>> collection) {
List<Integer> ids = new ArrayList<>();
for (CollapsedRequest<User, Integer> request : collection) {
ids.add(request.getArgument());
}
return new UserBatchCommand(ids, userService);
}
/**
* 请求结果分发
* @param users
* @param collection
*/
@Override
protected void mapResponseToRequests(List<User> users, Collection<CollapsedRequest<User, Integer>> collection) {
int count = 0;
for (CollapsedRequest<User, Integer> request : collection) {
request.setResponse(users.get(count++));
}
}
}
测试
@Autowired
UserService userService;
@GetMapping("/hystrix4")
public void test4() throws ExecutionException, InterruptedException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
UserCollapseCommand userCollapseCommand1=new UserCollapseCommand(userService,1);
UserCollapseCommand userCollapseCommand2=new UserCollapseCommand(userService,2);
UserCollapseCommand userCollapseCommand3=new UserCollapseCommand(userService,3);
Future<User> queue1 = userCollapseCommand1.queue();
Future<User> queue2 = userCollapseCommand2.queue();
Future<User> queue3 = userCollapseCommand3.queue();
User user1 = queue1.get();
User user2 = queue2.get();
User user3 = queue3.get();
System.out.println("user1 = " + user1);
System.out.println("user2 = " + user2);
System.out.println("user3 = " + user3);
Thread.sleep(200);
UserCollapseCommand userCollapseCommand4=new UserCollapseCommand(userService,4);
Future<User> queue4 = userCollapseCommand4.queue();
User user4 = queue4.get();
System.out.println("user4 = " + user4);
context.close();
}
}
经测试发现在consumer中虽然一次发送了4次请求 但在provider中其实只接收到合并后的一次请求

通过注解实现请求合并
@Service
public class UserService {
@Autowired
RestTemplate restTemplate;
@HystrixCommand
public List<User> getUsers(List<Integer> ids) {
User[] users = restTemplate.getForObject("http://provider/user/{1}", User[].class, StringUtils.join(ids,
","));
return Arrays.asList(users);
}
@HystrixCollapser(batchMethod = "getUsers",collapserProperties = {@HystrixProperty(name="timerDelayInMilliseconds",value = "200")})
public Future<User> getUser(Integer id){
return null;
}
}
这里的核心是 @HystrixCollapser 注解。在这个注解中,指定批处理的方法即可。
测试代码如下:
@GetMapping("/hystrix5")
public void test5() throws ExecutionException, InterruptedException {
Future<User> future1 = userService.getUser(1);
Future<User> future2 = userService.getUser(2);
Future<User> future3 = userService.getUser(3);
Future<User> future4 = userService.getUser(4);
User user1 = future1.get();
User user2 = future2.get();
User user3 = future3.get();
User user4 = future4.get();
System.out.println("user1 = " + user1);
System.out.println("user2 = " + user2);
System.out.println("user3 = " + user3);
System.out.println("user4 = " + user4);
}
六. OpenFegin
前面无论是基本调用,还是 Hystrix,我们实际上都是通过手动调用 RestTemplate 来实现远程调用的。使用 RestTemplate 存在一个问题:繁琐,每一个请求,参数不同,请求地址不同,返回数据类型
不同,其他都是一样的,所以我们希望能够对请求进行简化。我们希望对请求进行简化,简化方案就是 OpenFeign。
一开始这个组件不叫这个名字,一开始就叫 Feign,Netflix Feign,但是 Netflix 中的组件现在已经停止开源工作,OpenFeign 是 Spring Cloud 团队在 Netflix Feign 的基础上开发出来的声明式服务调用组件。关于OpenFeign 组件的 Issue: https://github.com/OpenFeign/feign/issues/373
6.1 快速入门
-
创建openFegin模块 引入依赖

-
注册该服务
server:
port: 1116
spring:
application:
name: openFegin
eureka:
client:
service-url:
defaultZone: http://eurekaA:1111/eureka #这里注册到A后 A会自动同步到B
- 开启 Feign 的支持
@SpringBootApplication
@EnableFeignClients
public class OpenfeginApplication {
public static void main(String[] args) {
SpringApplication.run(OpenfeginApplication.class, args);
}
}
- 定义 HelloService 接口,去使用 OpenFeign
@FeignClient("provider") //注册到eureka中的服务名(想要调用哪个服务)
public interface HelloService {
@GetMapping("/provider") //调用该服务中的接口名
String hello();
}
- 测试调用
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public String hello() {
return helloService.hello();
}
}
6.2 参数传递
和普通参数传递的区别:
- 参数一定要绑定参数名。
- 如果通过 header 来传递参数,一定记得中文要转码。
在provider中添加一个测试接口
@DeleteMapping("/delete3")
public void delete3(@RequestHeader String name) throws UnsupportedEncodingException {
System.out.println("name = " + URLDecoder.decode(name,"UTF-8"));
}
添加调用接口
@FeignClient("provider") //注册到eureka中的服务名(想要调用哪个服务)
public interface HelloService {
@GetMapping("/provider")//调用该服务中的接口名
String hello();
@GetMapping("/provider1")
String provide1(@RequestParam("name") String name);
@PostMapping("/user1")
User test2(@RequestBody User user);
@PutMapping("/user3")
void test3(@RequestParam("user") Map map);
@DeleteMapping("/delete/{id}")
void delete1(@PathVariable("id") Integer id);
@DeleteMapping("/delete3")
void delete3(@RequestHeader("name") String name);
}
测试
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public void hello() throws UnsupportedEncodingException {
helloService.hello();
helloService.provide1("provide1");
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
user.setPassword("123");
helloService.test2(user);
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("id", 2);
map.add("username", "李四");
map.add("password", "456");
helloService.test3(map);
helloService.delete1(3);
helloService.delete3(URLEncoder.encode("张三", "UTF-8"));
}
}
6.3 继承特性
将 provider 和 openfeign 中公共的部分提取出来,一起使用。
创建普通的maven项目模块common_api
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>com.cicro</groupId>
<artifactId>commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
然后定义公共接口,就是provider 和 openfeign 中公共的部分:
public interface IHelloService {
@GetMapping("/provider")//调用该服务中的接口名
String hello();
@GetMapping("/provider1")
String provide1(@RequestParam("name") String name);
@PostMapping("/user1")
User test2(@RequestBody User user);
@DeleteMapping("/delete/{id}")
void delete1(@PathVariable("id") Integer id);
@DeleteMapping("/delete3")
void delete3(@RequestHeader("name") String name) throws UnsupportedEncodingException;
}
定义完成后,接下来,在 provider 和 openfeign 中,分别引用该模块:
<dependency>
<groupId>com.cicro.springcloud_demo</groupId>
<artifactId>common-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
添加成功之后,在 provider 中实现该接口:
@RestController
public class ProviderController implements IHelloService {
@Value("${server.port}")
private int port;
@Override
public String hello() {
return "hello provider:" + port;
}
@Override
public String provide1(String name) {
System.out.println(new Date());
return "hello" + name;
}
/**
* 以key-value方式传参
* @param user
* @return
*/
@PostMapping("/user")
public User test1(User user) {
return user;
}
/**
* 以json方式传参
* @param user
* @return
*/
@PostMapping("/user1")
public User test2(@RequestBody User user) {
System.out.println("user = " + user);
return user;
}
/**
* 以key-value方式传参
* @param user
* @return
*/
/* @PutMapping("/user3")
public void test3(User user) {
System.out.println("user = " + user);
}*/
/**
* 以json方式传参
* @param user
* @return
*/
@PutMapping("/user4")
public void test4(@RequestBody User user) {
System.out.println("user = " + user);
}
/**
* 以地址栏路径传参
* @param id
* @return
*/
@Override
public void delete1(@PathVariable Integer id) {
System.out.println("id >>>>>= " + id);
}
/**
* 以key-value方式传参
* @param id
* @return
*/
@DeleteMapping("/delete")
public void delete2(Integer id) {
System.out.println("id = " + id);
}
@Override
public void delete3(@RequestHeader String name) throws UnsupportedEncodingException {
System.out.println("name = " + URLDecoder.decode(name,"UTF-8"));
}
}
在 openfeign 中,定义接口继承自公共接口:
@FeignClient("provider") //注册到eureka中的服务名(想要调用哪个服务)
public interface HelloService extends IHelloService {
}
然后测试即可 测试代码不变
关于继承特性:
- 使用继承特性,代码简洁明了不易出错。服务端和消费端的代码统一,一改俱改,不易出错。这是优点也是缺点,这样会提高服务端和消费端的耦合度。
- 在公共接口中声明方法的映射请求路径后,在服务提供者中实现该接口后就不用声明了
- 6.2 中所讲的参数传递,在使用了继承之后,依然不变,参数该怎么传还是怎么传。
6.4 日志级别配置
OpenFeign 中,我们可以通过配置日志,来查看整个请求的调用过程。日志级别一共分为四种:
- NONE:不开启日志,默认就是这个
- BASIC:记录请求方法、URL、响应状态码、执行时间
- HEADERS:在 BASIC 的基础上,加载请求/响应头
- FULL:在 HEADERS 基础上,再增加 body 以及请求元数据。
四种级别,可以通过 Bean 来配置:
@SpringBootApplication
@EnableFeignClients
public class OpenfeginApplication {
public static void main(String[] args) {
SpringApplication.run(OpenfeginApplication.class, args);
}
@Bean
Logger.Level level(){
return Logger.Level.FULL;
}
}
开启日志级别
logging:
level:
com:
cicro:
openfegin: debug

6.5 数据压缩
feign:
compression:
request:
#开启请求的数据压缩
enabled: true
#压缩的数据类型
mime-types: text/xml,application/json
#压缩下限,当传输的数据大于2048时,才会进行数据压缩
min-request-size: 2048
response:
#开启相应的数据压缩
enabled: true
6.6 服务降级(集成hystrix)
首先定义服务降级的方法:
@Component
@RequestMapping("/s") //防止请求地址重复
public class HelloServiceFailBack implements HelloService {
@Override
public String hello() {
return "error";
}
@Override
public String provide1(String name) {
return "error2";
}
@Override
public User test2(User user) {
return null;
}
@Override
public void delete1(Integer id) {
}
@Override
public void delete3(String name) throws UnsupportedEncodingException {
}
}
在 HelloService 中配置这个服务降级类:
@FeignClient(value = "provider",fallback = HelloServiceFailBack.class) //注册到eureka中的服务名(想要调用哪个服务)
public interface HelloService extends IHelloService {
}
最后 在配置文件中开启hystrix
feign:
hystrix:
enabled: true
也可以通过自定义 FallbackFactory 来实现请求降级
Component
public class HelloServiceFailBackFactory implements FallbackFactory<HelloService> {
@Override
public HelloService create(Throwable throwable) {
return new HelloService() {
@Override
public String hello() {
return "error";
}
@Override
public String provide1(String name) {
return "error2";
}
@Override
public User test2(User user) {
return null;
}
@Override
public void delete1(Integer id) {
}
@Override
public void delete3(String name) throws UnsupportedEncodingException {
}
};
}
@FeignClient(value = "provider",fallbackFactory = HelloServiceFailBackFactory.class) //注册到eureka中的服务名(想要调用哪个服务)
public interface HelloService extends IHelloService {
}
七. Resilience4j
7.1 Resilience4j简介
Resilience4j 是受Netflix的Hysrix项目启发,专门为Java 8 和函数式编程设计的轻量级容错框架。Resilicenes4j 仅使用了一个第三方开源库Vavr,Vavr不依赖其他库。相比较而言,Netflix Hysrix对Archaius存在编译依赖,Archaius有许多外部依赖,比如Guava和Apache Commons Configuration。Resilience4j 按模块发布,可以有选择的使用其中某些功能而无需引入全部的Resilience4j 组件。
resilience4j是一款受hystrix启发的容错组件,提供了如下几款核心组件:
- resilience4j-circuitbreaker: Circuit breaking
- resilience4j-ratelimiter: Rate limiting
- resilience4j-bulkhead: Bulkheading
- resilience4j-retry: Automatic retrying (sync and async)
- resilience4j-cache: Response caching
7.2 基本使用
7.2.1 熔断器
Resilience4j 提供了很多功能,不同的功能对应不同的依赖,可以按需添加。
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>0.13.2</version>
</dependency>
熔断测试
@Test
public void test() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
//故障率阈值百分比,超过这个阈值,断路器就会打开
.failureRateThreshold(50)
//断路器保持打开的时间,在到达设置的时间之后,断路器会进入到 half open 状态
.waitDurationInOpenState(Duration.ofMillis(1000))
//当断路器处于half open 状态时,环形缓冲区的大小
.ringBufferSizeInClosedState(2)
.build();
CircuitBreakerRegistry r1 = CircuitBreakerRegistry.of(config);
CircuitBreaker cb1 = r1.circuitBreaker("javaboy");
System.out.println(cb1.getState());//获取断路器的一个状态
cb1.onError(0, new RuntimeException());
System.out.println("第一次error后:" + cb1.getState());//获取断路器的一个状态
cb1.onError(0, new RuntimeException());
System.out.println("第二次error后:" + cb1.getState());//获取断路器的一个状态
CheckedFunction0<String> supplier =
CircuitBreaker.decorateCheckedSupplier(cb1, () -> "hello resilience4j");
Try<String> result = Try.of(supplier)
.map(v -> v + " hello world");
System.out.println(result.isSuccess());
System.out.println(result.get());
}
注意,由于 ringBufferSizeInClosedState 的值为 2,表示当有两条数据时才会去统计故障率,所以,下面的手动故障测试,至少调用两次 onError ,断路器才会打开。

7.2.2 限流
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
<version>0.13.2</version>
</dependency>
/**
* 限流 每秒执行2个请求
*/
@Test
public void test2() {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofMillis(1000))
.limitForPeriod(2)
.timeoutDuration(Duration.ofMillis(1000))
.build();
RateLimiter rateLimiter = RateLimiter.of("javaboy", config);
CheckedRunnable checkedRunnable =
RateLimiter.decorateCheckedRunnable(rateLimiter, () -> {
System.out.println(new Date());
});
Try.run(checkedRunnable)
.andThenTry(checkedRunnable)
.andThenTry(checkedRunnable)
.andThenTry(checkedRunnable)
.andThenTry(checkedRunnable)
.andThenTry(checkedRunnable)
.andThenTry(checkedRunnable)
.andThenTry(checkedRunnable)
.andThenTry(checkedRunnable)
.andThenTry(checkedRunnable)
.onFailure(t -> System.out.println(t.getMessage()));
}
7.2.3 重试
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
<version>0.13.2</version>
</dependency>
/**
* 重试
*/
@Test
public void test3() {
RetryConfig config = RetryConfig.custom()
//重试次数
.maxAttempts(2)
//重试间隔
.waitDuration(Duration.ofMillis(500))
//重试异常
.retryExceptions(RuntimeException.class)
.build();
Retry retry = Retry.of("javaboy", config);
Retry.decorateRunnable(retry, new Runnable() {
int count = 0;
//开启了重试功能之后,run 方法执行时,如果抛出异常,会自动触发重试功能
@Override
public void run() {
if (count++ < 3) {
System.out.println(count);
throw new RuntimeException();
}
}
}).run();
}
7.3 集成spring boot
创建resilience工程

手动添加 Resilience4j 依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.3.1</version>
</dependency>
7.3.1 重试
在 application.yml 中配置 retry:
server:
port: 1300
spring:
application:
name: resilience
eureka:
client:
service-url:
defaultZone: http://eurekaA:1111/eureka #这里注册到A后 A会自动同步到B
resilience4j:
retry:
retry-aspect-order: 100 #retry的优先级
backends:
retryA:
maxRetryAttempts: 5 # 重试次数
waitDuration: 500 # 重试等待时间
exponentialBackoffMultiplier: 1.1 # 间隔乘数
retryExceptions: #重试次数达到时抛出的异常
- java.lang.RuntimeException
编写测试代码
@SpringBootApplication
public class Resilience4jApplication {
public static void main(String[] args) {
SpringApplication.run(Resilience4jApplication.class, args);
}
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Service
@Retry(name="retryA") //表示要使用的重试策略
public class Res4jService {
@Autowired
RestTemplate restTemplate;
public void test() {
restTemplate.delete("http://localhost:1113/delete?id={1}", 1);
}
}
@RestController
public class Res4jController {
@Autowired
Res4jService res4jService;
@GetMapping("/test")
public void test(){
res4jService.test();
}
}
启动项目 访问test接口 观察provider中的日志发现 访问一次,provider中打印了5次错误日志
7.3.2 熔断器
配置熔断降级的相关参数
resilience4j:
circuitbreaker:
instances:
cba:
ringBufferSizeInClosedState: 5
ringBufferSizeInHalfOpenState: 3
waitInterval: 5000
recordExceptions:
- org.springframework.web.client.HttpServerErrorException
circuit-breaker-aspect-order: 99
配置完成后,用 @CircuitBreakder 注解标记相关方法:
@Service
@CircuitBreaker(name = "cba",fallbackMethod = "error")
public class Res4jService {
@Autowired
RestTemplate restTemplate;
public void test() {
restTemplate.delete("http://localhost:1113/delete?id={1}", 1);
}
public void error(Throwable t) {
System.out.println("error"+t.getLocalizedMessage());
}
}
7.3.3 限流
RateLimiter 作为限流工具,主要在服务端使用,用来保护服务端的接口。
这里我们的provider作为服务端接口被resilience模块调用 , 因此限流要在provider中做 引入相关依赖
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.3.1</version>
</dependency>
在provider中配置相关参数
#配置每秒中处理1个请求
resilience4j:
ratelimiter:
limiters:
rla:
limit-for-period: 1
limit-refresh-period: 1s
timeout-duration: 1s
为了看出测试效果 在provider限流的接口打印日志
@DeleteMapping("/delete")
@RateLimiter(name="rla") //这里通过 @RateLimiter 注解来标记该接口限流。
public void delete2(Integer id) {
System.out.println("id = " + id + " " + new Date());
}
然后,在客户端模拟多个请求,查看限流效果:
public void test() {
for (int i = 0; i < 5; i++) {
restTemplate.delete("http://localhost:1113/delete?id={1}", i + 1);
}
}
测试发现provider中 每秒中只处理了一个请求

八. 服务监控
微服务由于服务数量众多,所以出故障的概率很大,这种时候不能单纯的依靠人肉运维。
早期的 Spring Cloud 中,服务监控主要使用 Hystrix Dashboard,集群数据库监控使用 Turbine。
在 Greenwich 版本中,官方建议监控工具使用 Micrometer。
Micrometer:
- 提供了度量指标,例如 timers、counters
- 一揽子开箱即用的解决方案,例如缓存、类加载器、垃圾收集等等
新建micrometer监控工程

项目创建成功后,添加如下配置,开启所有端点:
management:
endpoints:
web:
exposure:
include: "*"
启动项目后访问 http://localhost:8080/actuator

我们需要一个可视化工具来展示这些 JSON 数据。
8.1 Prometheus
下载安装:
官网下载地址: https://prometheus.io/download/
wget https://github.com/prometheus/prometheus/releases/download/v2.21.0/prometheus-2.21.0.linux-386.tar.gz
tar -zxvf prometheus-2.21.0.linux-386.tar.gz
解压完成后,配置一下数据路径和要监控的服务地址:
cd prometheus-2.21.0.linux-386
vim prometheus.yml

接下来,将 Prometheus 整合到 Spring Boot 项目中。
首先 添加所需依赖
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
然后在 application.properties 配置中,添加 Prometheus 配置:
management.endpoints.web.exposure.include=*
management.endpoint.prometheus.enabled=true
management.metrics.export.prometheus.enabled=true
management.endpoint.metrics.enabled=true
接下来启动 Prometheus。
./prometheus --config.file=prometheus.yml
启动成功后,浏览器输入 http://192.168.19.128:9090/ 查看 Prometheus 数据信息
九. Zuul
Zuul是Netflix开源的微服务网关,可以和Eureka、Ribbon、Hystrix等组件配合使用,Spring Cloud对Zuul进行了整合与增强,Zuul默认使用的HTTP客户端是Apache HTTPClient,也可以使用RestClient或okhttp3.OkHttpClient。 Zuul的主要功能是路由转发和过滤器。路由功能是微服务的一部分,比如/provider/test转发到到provider服务。zuul默认和Ribbon结合实现了负载均衡的功能
9.1 工作原理
zuul的核心是一系列的filters, 其作用类比Servlet框架的Filter,或者AOP。zuul把请求路由到用户处理逻辑的过程中,这些filter参与一些过滤处理,比如Authentication,Load Shedding等

Zuul使用一系列不同类型的过滤器,使我们能够快速灵活地将功能应用于我们的边缘服务。这些过滤器可帮助我们执行以下功能
- 身份验证和安全性 - 确定每个资源的身份验证要求并拒绝不满足这些要求的请求
- 洞察和监控 - 在边缘跟踪有意义的数据和统计数据,以便为我们提供准确的生产视图
- 动态路由 - 根据需要动态地将请求路由到不同的后端群集
压力测试 - 逐渐增加群集的流量以衡量性能。 - 负载均衡 - 为每种类型的请求分配容量并删除超过限制的请求
- 静态响应处理 - 直接在边缘构建一些响应,而不是将它们转发到内部集群
过滤器的生命周期

9.2 快速入门
搭建zuul模块工程 引入依赖

将zuul注册到eureka中
server.port=1200
spring.application.name=zuul
eureka.client.service-url.defaultZone=http://eurekaA:1111/eureka
开启zuul网关
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
配置完成后,重启 Zuul,接下来,在浏览器中,通过 Zuul 的代理就可以访问到 provider 了。
http://localhost:1200/provider/provider1
在这个访问地址中,provider 就是要访问的服务名称,/provider1则是要访问的服务接口。
Zuul 中的路由规则也可以自己配置。
zuul.routes.provider=/zuul/provider/**
访问http://localhost:1200/zuul/provider/provider1
9.3 请求过滤
对于来自客户端的请求,可以在 Zuul 中进行预处理,例如权限判断等。
定义一个简单的权限过滤器:
@Component
public class AuthFilter extends ZuulFilter {
/**
* 过滤器类型 认证权限相关一般是pre
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤器优先级
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 是否过滤
* @return
*/
@Override
public boolean shouldFilter() {
return true; //来自前端的所有请求均过滤
}
/**
* 核心的过滤逻辑写在这里
* @return 这个方法虽然有返回值,但是这个返回值目前无所谓 忽略
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String username = request.getParameter("username");
String password = request.getParameter("password");
if (!"lisi".equals(username) || !"123".equals(password)){
context.setSendZuulResponse(false);
context.addZuulResponseHeader("content-type","text/html;charset=utf-8");
context.setResponseBody("认证失败");
context.setResponseStatusCode(401);
}
return null;
}
}
9.4 常用其他配置
匹配规则
例如有两个服务,一个叫 consumer,另一个叫 consumer-hello,在做路由规则设置时,假如出现了如下配置:
zuul.routes.consumer=/consumer/**
zuul.routes.consumer-hello=/consumer/hello/**
此时,如果访问一个地址:http://localhost:1200/consumer/hello/123,会出现冲突。实际上,这个地址是希望和 consumer-hello 这个服务匹配的,这个时候,只需要把配置文件改为 yml 格式就可以了
忽略路径
默认情况下,zuul 注册到 eureka 上之后,eureka 上的所有注册服务都会被自动代理。如果不想给某一个服务做代理,可以忽略该服务,配置如下
zuul.ignored-services=provider
上面这个配置表示忽略 provider 服务,此时就不会自动代理 provider 服务了。
也可以忽略某一类地址:
zuul.ignored-patterns=/**/hello/**
这个表示请求路径中如果包含 hello,则不做代理。
前缀
zuul.prefix=/hello
这样,以后所有的请求地址自动多了前缀,/hello
十. Gateway
Spring Cloud Gateway是Spring官方基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代ZUUL,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。



和zuul对比
- Zuul 是 Netflix 公司的开源产品,Spring Cloud Gateway 是 Spring 家族中的产品,可以和Spring 家族中的其他组件更好的融合。
- Zuul1 不支持长连接,例如 websocket。
- Spring Cloud Gateway 支持限流。
- Spring Cloud Gateway 基于 Netty 来开发,实现了异步和非阻塞,占用资源更小,性能强于Zuul。
10.1 基本用法
新建gateway模块工程

项目创建成功后,直接配置一个 RouteLocator 这样一个 Bean,就可以实现请求转发。
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
RouteLocator routeLocator(RouteLocatorBuilder builder){
return builder.routes()
.route("route",r->r.path("/get").uri("http://httpbin.org"))
.build();
}
}
也可以不用配bean 直接在yml文件中配置
spring:
cloud:
gateway:
routes:
- id: route
uri: http://httpbin.org
predicates:
- Path=/get
10.2 服务化(集成eureka)
首先将gateway注册到eureka中 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置yml
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: http://httpbin.org
predicates:
- Path=/get
application:
name: gateway
eureka:
client:
service-url:
defaultZone: http://eurekaA:1111/eureka
logging:
level:
org.springframework.cloud.gateway: debug
将gateway注册到eureka上后,并开启自动代理后,gateway就会自动代理其他注册到erueka上的服务
接下来,就可以通过 Gateway 访问到其他注册在 Eureka 上的服务了,访问方式和 Zuul 一样。
10.3 predicate
通过时间匹配
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: http://www.itboyhub.com/
predicates:
- Before=2021-01-20T06:06:06+08:00[Asia/Shanghai]
表示在请求进行转发的时候,可以通过判断在这个时间之前或者之后进行转发。比如我们现在设置只有在 2021年 1 月 1 日之前才会转发到我的网站,在这之后不进行转发.
除了 After 之外,还有两个关键字:
- Before,表示在某个时间点之前进行请求转发
- Between,表示在两个时间点之间,两个时间点用 , 隔开
通过 Cookie 匹配
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: http://www.itboyhub.com/
predicates:
- Cookie=ithub,javaboy
Cookie Route Predicate 可以接收两个参数,一个是 Cookie name , 一个是正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
通过Header匹配
Header Route Predicate 和 Cookie Route Predicate 一样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: http://www.itboyhub.com/
predicates:
- Header=X-Request-Id, \d+
通过Host匹配
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则。
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: http://www.itboyhub.com/
predicates:
- Host=**.itboyhub.com
通过请求方式匹配
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: http://www.itboyhub.com/
predicates:
- Method=GET
通过请求路径匹配
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: http://httpbin.org
predicates:
- Path=/get
通过请求参数匹配
Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: http://www.itboyhub.com/
predicates:
- Query=name
通过请求 ip 地址进行匹配
如果请求的远程地址是 192.168.1.104,则此路由将匹配。
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: http://www.itboyhub.com/
predicates:
- RemoteAddr=192.168.1.104
组合使用
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: http://www.itboyhub.com/
predicates:
- RemoteAddr=192.168.1.104
- Method=GET
- Host=**.itboyhub.com
10.4 Filter
Spring Cloud Gateway 中的过滤器分为两大类:
- atewayFilter
- GlobalFilter
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启自动代理
routes:
- id: route
uri: lb://provider #所有请求都转发到provider服务上(lb表示带负载均衡的)
filters:
- AddRequestParameter=name,123
predicates:
- Method=GET
这个过滤器就是在请求转发路由的时候,自动额外添加参数。
直接访问http://localhost:8080/provider1即可
十一. Spring Cloud Config (分布式配置中心)
11.1 基本用法
分布式配置中心解决方案:
11.1.2 简介
Spring Cloud Config 是一个分布式系统配置管理的解决方案,它包含了 Client 和 Server 。配置文件放在 Server 端,通过 接口的形式提供给 Client。
Spring Cloud Config 主要功能:
- 集中管理各个环境、各个微服务的配置文件
- 提供服务端和客户端支持
- 配置文件修改后,可以快速生效
- 配置文件通过 Git/SVn 进行管理,天然支持版本回退功能。
- 支持高并发查询、也支持多种开发语言。
11.1.3 准备工作
本地准备好相应的配置文件,提交到 GitHub:https://github.com/liujiahaoGit/demo/tree/master/config
11.1.4 config server 搭建

项目创建成功后,项目启动类上添加注解,开启 config server 功能:
@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class, args);
}
}
配置仓库的基本信息:
spring.application.name=config-server
server.port=8082
# 配置文件仓库地址
spring.cloud.config.server.git.uri=https://github.com/liujiahaoGit/demo.git
# 配置文件仓库地址
spring.cloud.config.server.git.search-paths=config/client1/
# 仓库的用户名密码
spring.cloud.config.server.git.username=xxx
spring.cloud.config.server.git.password=xxx
启动项目后,就可以访问配置文件了。访问地址:http://localhost:8082/client1/prod
访问地址有如下规则:
/{application}/{profile}/[{label}]
/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.yml
/{label}/{application}-{profile}.properties
applicaiton 表示配置文件名
profile 表示配置文件 profile,例如 test、dev、prod
label 表示git 分支,参数可选,默认就是master
接下来,可以修改配置文件,并且重新提交到 GitHub,此时,刷新 ConfigServer 接口,就可以及时看到最新的配置内容。
11.1.5 config client 搭建

项目创建成功后,resources 目录下,添加 bootstrap.properties 配置,内容如下:
server.port=8083
# 下面三行配置,分别对应 config-server 中的 {application}、{profile}以及{label}占位符
spring.application.name=client1
spring.cloud.config.profile=prod
spring.cloud.config.label=master
#config server 地址
spring.cloud.config.uri=http://localhost:8082
创建测试接口
@RestController
public class TestConfig {
@Value("${active}")
String active;
@GetMapping("/test")
public String test() {
return active;
}
}
11.1.6 占位符配置
使用占位符灵活控制查询目录。
修改 config-server 配置文件:
# 配置文件仓库地址
spring.cloud.config.server.git.search-paths=config/{application}/
spring.cloud.config.profile={profile}
spring.cloud.config.label={label}
这里的 {application} 占位符,表示链接上来的 client1 的spring.application.name属性的值。
在 confi-server 中,也可以用{profile} 表示 client 的spring.cloud.config.profile,也可以用{label} 表示 client 的 spring.cloud.config.label
虽然在实际开发中,配置文件一般都是放在 Git 仓库中,但是,config-server 也支持将配置文件放在classpath 下。
在 config-server 中添加如下配置:
spring.profiles.active=native
也可以在 config-server 中,添加如下配置,表示指定配置文件的位置:
spring.cloud.config.server.native.search-locations=file:/E:/properties/
11.2 配置文件加密
11.2.1 常见加密方案
- 不可逆加密
- 可逆加密
不可逆加密,就是理论上无法根据加密后的密文推算出明文。一般用在密加密上,常见的算法如MD5 消息摘要算法、SHA 安全散列算法。
可逆加密,看名字就知道可以根据加密后的密文推断出明文的加密方式,可逆加密一般又分为两种:
- 对称加密
- 非对称加密
对称加密指加密的密钥和解密的密钥是一样的。常见算法des、3des、aes
非对称加密就是加密的密钥和解密的密钥不一样,加密的叫做公钥,可以告诉任何人,解密的叫做私钥,只有自己知道。常见算法 RSA。
11.2.2 对称加密
首先下载不限长度的 JCE https://download.csdn.net/download/zxc_123_789/12895137
将下载的文件解压,解压出来的2个 jar 拷贝到 Java 安装目录中:C:\Program Files\Java\jdk1.8.0_201\lib\security
然后,在 config-server 的 bootstrap.properties 配置文件中,添加如下内容配置密钥:
encrypt.key=miyao #密钥
启动 config-server ,访问如下地址,查看密钥配置是否OK
http://localhost:8082/encrypt/status
然后,访问:http://localhost:8082/encrypt ,注意这是一个 POST 请求,访问该地址,可以对一段明文进行加密。把加密后的明文存储到 Git 仓库中,存储时,要注意加一个 {cipher} 前缀。

启动config_client 测试发现拿到的数据已经是解密后的数据了
11.2.3 非对称加密
非对称加密需要我们首先生成一个密钥对
在命令行执行如下命令,生成 keystore:
keytool -genkeypair -alias config-server -keyalg RSA -keystore H:\桌面\config-server.keystore
然后按照提示输入指定的密钥和密码 后面要在配置文件中配置
命令执行完成后,拷贝生成的 keystore 文件到 config-server 的 resources 目录下。
然后在 config-server 的 bootstrap.properties 目录中,添加如下配置:
encrypt.key-store.location=config-server.keystore
encrypt.key-store.alias=config-server
encrypt.key-store.password=123456
encrypt.key-store.secret=123456
重启 config-server ,测试方法与对称加密一致。
如果在启动config_server后 class类路径下没有加载到keystore 则在 pom.xml 的 build 节点中加入如下配置
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.keystore</include>
</includes>
</resource>
</resources>
11.3 安全管理
防止用户直接通过访问 config-server 看到配置文件内容,我们可以用 spring security 来保护 config-server 接口。
首先在 config-server 中添加 spring security 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加完依赖后,config-server 中的接口就自动被保护起来了。
在 config-server 的配置文件中,添加如下配置,固定用户名密码:
spring.security.user.name=user
spring.security.user.password=123
然后,在 config-client 的 bootstrap.properties 配置文件中,添加如下配置
spring.cloud.config.username=user
spring.cloud.config.password=123
11.4 服务化
之前的配置都是直接在 config-client 中写死 config-server 的地址。
首先 , 将config-server和config-client注册到eureka中 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在 application.properties 配置文件中配置注册信息。
eureka.client.service-url.defaultZone=http://eurekaA:1111/eureka
然后,修改 config-client 的配置文件,配置服务化后的地址
spring:
application:
name: client1
cloud:
config:
discovery:
enabled: true # 开启通过 eureka 获取 config-server 的功能
service-id: config-server # 配置 config-server 服务名称
profile: test
label: master
#security的用户名密码
username: user
password: 123
server:
port: 8083
eureka:
client:
service-url:
defaultZone: http://eurekaA:1111/eureka #这里注册到A后 A会自动同步到B
启动服务测试即可
11.5 动态刷新
当配置文件发生变化之后,config-server 可以及时感知到变化,但是 config-client 不会及时感知到变化,默认情况下,config-client 只有重启才能加载到最新的配置文件,要让client也动态感知到变化
在config-client中添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然后,在bootstrap中添加配置,使 refresh 端点暴露出来:
## 將refresh端点暴露出来
management.endpoints.web.exposure.include=refresh
最后,再给 config-client 使用了配置文件的地方加上@RefreshScope 注解,这样,当配置改变后,只需要调用 refresh 端点,config-client 中的配置就可以自动刷新。
@RestController
@RefreshScope
public class TestConfig {
@Value("${active}")
String active;
@GetMapping("/test")
public String test() {
return active;
}
}
重启 config-client,以后,只要配置文件发生变化,发送 POST 请求,调用 http://localhost:8083/actuator/refresh 接口即可,配置文件就会自动刷新
11.6 请求失败重试
config-client 在调用 config-server 时,一样也可能发生请求失败的问题,这个时候,我们可以配置一个请求重试的功能。
要给 config-client 添加重试功能,只需要添加如下依赖即可:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
修改bootstrap.yml配置文件 开启失败快速响应
spring:
application:
name: client1
cloud:
config:
discovery:
enabled: true # 开启通过 eureka 获取 config-server 的功能
service-id: config-server # 配置 config-server 服务名称
profile: prod
label: master
#security的用户名密码
username: user
password: 123
fail-fast: true #开启失败快速响应
retry:
initial-interval: 1000 #请求重试的时间间隔
max-attempts: 10 #最大重试次数
max-interval: 2000 #最大间隔时间
multiplier: 1.1 #重试时间间隔乘数
十二. Spring Cloud Bus
12.1 批量刷新配置
Spring Cloud Bus 通过轻量级的消息代理连接各个微服务,可以用来广播配置文件的更改,或者管理服务监控。
首先给 config-server 和 config-client 分别加上 Spring Cloud Bus 依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
然后,给两个分别配置,使之都连接到 RabbitMQ 上:
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
同时,由于 configserver 将提供刷新接口,所以给 configserver 加上 actuator 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在 config-server 中,添加开启 bus-refresh 端点
management.endpoints.web.exposure.include=bus-refresh
由于给 config-server 中的所有接口都添加了保护,所以刷新接口将无法直接访问,此时,可以通过修改 Security 配置,对端点的权限做出修改:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 开启了 HttpBasic 登录,这样,在发送刷新请求时,就可以直接通过 HttpBasic 配置认证信息了。
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.csrf().disable();
}
}
最后分别启动 config-server 和 config-client,然后修改配置信息提交到 GitHub,刷新 config-client 接口,查看是否有变化。
发送如下 POST 请求:http://localhost:8082/actuator/bus-refresh

这个 post 是针对 config-server 的,config-server 会把这个刷新的指令传到 rabbitmq ,然后rabbitmq 再把指令传给 各个 client。
12.2 单个刷新
如果更新配置文件之后,不希望每一个微服务都去刷新配置文件,那么可以通过如下配置解决问题。
首先,给每一个 config-client 添加一个 instance-id:
eureka.instance.instance-id=${spring.application.name}:${server.port}
然后,对 config-client 进行打包。打包完成后,通过如下命令启动两个 config-client 实例:
java -jar config-client-0.0.1-SNAPSHOT.jar --server.port=8083
java -jar config-client-0.0.1-SNAPSHOT.jar --server.port=8084
修改配置文件,并且提交到 GitHub 之后,可以通过如下方式只刷新某一个微服务,例如只刷新 8083的服务。
http://localhost:8082/actuator/bus-refresh/client1:8083

client1:8083 表示服务的 instance-id。
十三. Spring Cloud Stream
13.1 简介
Spring Cloud Stream 用来构建消息驱动的微服务。
Spring Cloud Stream 中,提供了一个微服务和消息中间件之间的一个粘合剂,这个粘合剂叫做Binder,Binder 负责与消息中间件进行交互。而我们开发者则通过 inputs 或者 outputs 这样的消息通道与 Binder 进行交互。
13.2 快速入门
搭建stream工程模块

项目创建成功后,添加 RabbitMQ 的基本配置信息:
spring.application.name=stream
server.port=8085
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
创建一个简单的消息接收器:
@EnableBinding(Sink.class) //表示绑定 Sink 消息通道
public class StreamController {
private static final Logger LOGGER= LoggerFactory.getLogger(StreamController.class);
@StreamListener(Sink.INPUT)
public void test(Object message){
LOGGER.info("receive:"+message);
}
}
启动 stream 项目,然后,在 rabbitmq 后台管理页面去发送一条消息。

13.3 自定义通道
首先创建一个名为 MyChannel 的接口:
public interface MyChannel {
String INPUT = "input-ceshi";
String OUTPUT = "output-ceshi";
@Output(OUTPUT)
MessageChannel output();
@Input(INPUT)
SubscribableChannel input();
}
- 注意,两个消息通道的名字是不一样的
- 从 F 版开始,默认使用通道的名称作为实例名称,所以这里的通道名字不可以相同(早期版本可以相同),这样的话,为了能够正常收发消息,需要我们在 application.properties 中做一些额外配置。
自定义一个消息接收器,用来接收自己的消息通道里的消息:
@EnableBinding(MyChannel.class)
public class MsgReceive {
private static final Logger LOGGER= LoggerFactory.getLogger(MsgReceive.class);
@StreamListener(MyChannel.INPUT)
public void test(Object message){
LOGGER.info("receive:"+message);
}
}
测试接口
@Autowired
MyChannel myChannel;
/**
* 发送消息
*/
@GetMapping("/publish")
public void publish(){
myChannel.output().send(MessageBuilder.withPayload("自定义通道测试").build());
}
}
为了让消息输入输出通道对接上(因为现在这两个的通道名称不一样),再增加一点额外配置。
spring.cloud.stream.bindings.input-ceshi.destination=ceshi-topic
spring.cloud.stream.bindings.output-ceshi.destination=ceshi-topic
13.4 消息分组
默认情况下,如果消费者是一个集群,此时,一条消息会被多次消费。通过消息分组,我们可以解决这个问题。
只需要添加如下配置即可:
spring.cloud.stream.bindings.input-ceshi.group=g1
spring.cloud.stream.bindings.output-ceshi.group=g1
13.5 消息分区
通过消息分区可以实现相同特征的消息总是被同一个实例处理。只需要添加如下配置即可:
spring.application.name=stream
server.port=8085
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.cloud.stream.bindings.input-ceshi.destination=ceshi-topic
spring.cloud.stream.bindings.output-ceshi.destination=ceshi-topic
# 分组
spring.cloud.stream.bindings.input-ceshi.group=g1
spring.cloud.stream.bindings.output-ceshi.group=g1
# 开启消息分区(消费者上配置)
spring.cloud.stream.bindings.input-ceshi.consumer.partitioned=true
# 消费者实例个数(消费者上配置)
spring.cloud.stream.instance-count=2
# 当前实例的下标(消费者上配置)
spring.cloud.stream.instance-index=0
# 该消息想让哪一个消费者消费(生产者上配置)
spring.cloud.stream.bindings.output-ceshi.producer.partition-key-expression=0
# 消费端的节点数量(生产者上配置
spring.cloud.stream.bindings.output-ceshi.producer.partition-count=2
接下来,启动两个实例测试,注意,启动时,spring.cloud.stream.instance-index 要动态修改。
java -jar stream-0.0.1-SNAPSHOT.jar --server.port=8087 --spring.cloud.stream.instance-index=1
java -jar stream-0.0.1-SNAPSHOT.jar --server.port=8085 --spring.cloud.stream.instance-index=0
测试发现 每次发送的消息都会发送到8085端口的服务上
十四 Spring Cloud Sleuth
14.1 简介
在这种大规模的分布式系统中,一个完整的系统是由很多种不同的服务来共同支撑的。不同的系统可能分布在上千台服务器上,横跨多个数据中心。一旦系统出问题,此时问题的定位就比较麻烦。
分布式链路追踪:
在微服务环境下,一次客户端请求,可能会引起数十次、上百次服务端服务之间的调用。一旦请求出问题了,我们需要考虑很多东西:
- 如何快速定位问题?
- 如果快速确定此次客户端调用,都涉及到哪些服务?
- 到底是哪一个服务出问题了?
要解决这些问题,就涉及到分布式链路追踪。
分布式链路追踪系统主要用来跟踪服务调用记录的,一般来说,一个分布式链路追踪系统,有三个部分:
- 数据收集
- 数据存储
- 数据展示
Spring Cloud Sleuth 是 Spring Cloud 提供的一套分布式链路追踪系统。
-
trace:一系列Span组成的一个树状结构。请求一个微服务系统的API接口,这个API接口,需要调用多个微服务,调用每个微服务都会产生一个新的Span,所有由这个请求产生的Span组成了这个Trace。
-
span:基本工作单元,发送一个远程调度任务 就会产生一个Span,Span是一个64位唯一标识的ID,Trace是用另一个64位ID唯一标识的,Span还有其他数据信息,比如摘要、时间戳事件、Span的ID、以及进度ID。
-
annotation:用来及时记录一个事件的,一些核心注解用来定义一个请求的开始和结束 。这些注解包括以下:
- cs - Client Sent -客户端发送一个请求,这个注解描述了这个Span的开始
- sr - Server Received -服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳便可得到网络传输的时间。
- ss - Server Sent (服务端发送响应)–该注解表明请求处理的完成(当请求返回客户端),如果ss的时间戳减去sr时间戳,就可以得到服务器请求的时间。
- cr - Client Received (客户端接收响应)-此时Span的结束,如果cr的时间戳减去cs时间戳便可以得到整个请求所消耗的时间。
14.2 快速入门
创建sleuth工程模块 引入依赖

配置服务名
spring.application.name=sleuth
定义两个接口,在 hello2 中调用 hello3,形成调用链:
@RestController
@Slf4j
public class SleuthController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/test")
public String test() {
log.info("sleuth start");
log.info("sleuth end");
return "sleuth";
}
@GetMapping("/consumer")
public String consumer() {
log.info("consumer start");
String s = restTemplate.getForObject("http://localhost:8080/test", String.class);
log.info("consumer end");
return s;
}
}
访问consumer观察打印日志
2020-10-04 14:15:17.904 INFO [sleuth,0d2297ca0f7fa673,0d2297ca0f7fa673,true] 19352 --- [nio-8080-exec-1] com.cicro.sleuth.SleuthController : consumer start
2020-10-04 14:15:17.969 INFO [sleuth,0d2297ca0f7fa673,7d35a4ba594ca0f0,true] 19352 --- [nio-8080-exec-3] com.cicro.sleuth.SleuthController : sleuth start
2020-10-04 14:15:17.970 INFO [sleuth,0d2297ca0f7fa673,7d35a4ba594ca0f0,true] 19352 --- [nio-8080-exec-3] com.cicro.sleuth.SleuthController : sleuth end
2020-10-04 14:15:18.015 INFO [sleuth,0d2297ca0f7fa673,0d2297ca0f7fa673,true] 19352 --- [nio-8080-exec-1] com.cicro.sleuth.SleuthController : consumer end
发现一个 trace 由多个 span 组成,一个trace 相当于就是一个调用链,而一个 span 则是这个链中的每一次调用过程。
Spring Cloud Sleuth 中也可以收集到异步任务中的信息。
开启异步任务:
@SpringBootApplication
@EnableAsync
public class SleuthApplication {
public static void main(String[] args) {
SpringApplication.run(SleuthApplication.class, args);
}
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
}
提供一个异步方法
@Service
@Slf4j
public class SleuthService {
@Async
public String test(){
log.info("test...");
return "test";
}
}
在controller中调用
@Autowired
private SleuthService sleuthService;
@GetMapping("/consumer1")
public String consumer1() {
log.info("consumer1 start");
String s = sleuthService.test();
log.info("consumer1 end");
return s;
}
启动项目进行测试,发现 Sleuth 也打印出日志了,在异步任务中,异步任务是单独的 spanid。

Spring Cloud Sleuth 也可以收集定时任务的信息。
首先开启定时任务支持
@SpringBootApplication
@EnableAsync
@EnableScheduling
public class SleuthApplication {
public static void main(String[] args) {
SpringApplication.run(SleuthApplication.class, args);
}
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
}
测试
@GetMapping("/consumer2")
@Scheduled(cron = "0/5 * * * * ?")
public String consumer2() {
sleuthService.test();
log.info("consumer2...");
return "consumer2";
}
在定时任务中,每一次定时任务都会产生一个新的 Trace,并且在调用过程中,SpanId 都是一致的,这个和普通的调用不一样。
十五 Spring Cloud alibaba
15.1 简介
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
15.2 主要功能
- 服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
项目的版本号格式为 x.x.x 的形式,其中 x 的数值类型为数字,从 0 开始取值,且不限于 0~9 这个范围。项目处于孵化器阶段时,第一位版本号固定使用 0,即版本号为 0.x.x 的格式。
由于 Spring Boot 1 和 Spring Boot 2 在 Actuator 模块的接口和注解有很大的变更,且 spring-cloud-commons 从 1.x.x 版本升级到 2.0.0 版本也有较大的变更,因此我们采取跟 SpringBoot 版本号一致的版本:
- 1.5.x 版本适用于 Spring Boot 1.5.x
- 2.0.x 版本适用于 Spring Boot 2.0.x
- 2.1.x 版本适用于 Spring Boot 2.1.x
- 2.2.x 版本适用于 Spring Boot 2.2.x
15.3 Nacos
Nacos 主要提供了服务发现、服务配置以及服务管理。
基本特性:
- 服务发现
- 动态配置
- 动态 DNS 服务
- 服务及元数据管理
15.3.1 安装
首先下载安装包
https://github.com/alibaba/nacos/releases/download/1.3.2/nacos-server-1.3.2.tar.gz
然后解压,解压后,如果 win,直接在 bin 目录下执行startup.cmd -m standalone
如果Linux,bin 目录下执行startup.sh -m standalone
注意,系统一定要配置好 jdk,测试一下 java 和 javac 两个命令要存在。
Nacos 启动成功后,浏览器输入:http://localhost:8848/nacos 就能看到启动页面。
如果有登录页面,登录的默认用户名/密码都是 nacos。
15.3.2 配置中心
首先在服务端配置,点击配置管理–>配置列表–>+
这里主要配置三个东西,Data ID、Group 以及要配置的内容。
Data Id 的格式是 ${prefix}-${spring.profile.active}.${file-extension}
${prefix} 的值,默认为 spring.application.name 的值
${spring.profile.active} 表示项目当前所处的环境
${file-extension} 表示配置文件的扩展名

配置完成后,新建 Spring Boot 项目,加入 Nacos 依赖:

然后,新建 bootstrap.properties 配置文件,配置 nacos 信息:
spring.application.name=nacos
spring.cloud.nacos.config.server-addr=localhost:8848
spring.cloud.nacos.config.file-extension=yaml
测试类
@RestController
@RefreshScope
public class NacosController {
@Value("${name}")
private String name;
@GetMapping("/get")
public String nacos() {
return name;
}
}
15.3.3 注册中心
服务提供者(生产者)
在nacos中添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
修改nacos的配置 将之注册到注册中心上

然后打包此项目 以不同的端口启动

启动成功后 发现nacos控制台服务列表中已注册成功
创建项目 添加依赖(消费者)

然后,新建 bootstrap.properties 配置文件,配置 nacos 信息:
spring.application.name=nacos-consumer
spring.cloud.nacos.config.server-addr=localhost:8848
spring.cloud.nacos.config.file-extension=yaml
在配置列表中添加配置 将此服务注册到nacos上

提供测试类
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/consumer")
public String consumer(){
return restTemplate.getForObject("http://nacos/get",String.class);
}
}
启动项目

访问测试接口(默认自带负载均衡功能)
15.4 Sentinel
15.4.1 简介
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
15.4.2 安装控制台
下载地址:https://github.com/alibaba/Sentinel/releases/download/v1.8.0/sentinel-dashboard-1.8.0.jar
下载后直接启动即可 访问localhost:8080 用户名喝密码均为sentinel
创建springboot项目并引入依赖

项目创建成功后,配置 sentinel 控制台地址:
spring.application.name=sentinel
server.port=8082
spring.cloud.sentinel.transport.dashboard=localhost:8080
创建测试接口
@RestController
public class HelloController {
@GetMapping("/sentinel")
public String sentinel() {
return "hello sentinel";
}
}
在sentinel控制台上配置限流规则

模拟请求访问
@Test
void contextLoads() {
RestTemplate template = new RestTemplate();
for (int i = 0; i < 15; i++) {
String s = template.getForObject("http://localhost:8082/sentinel", String.class);
System.out.println("s = " + s + " " + new Date());
}
}
访问后观察仪表盘
sentinel集成nacos,在nacos上配置限流规则

在sentinel工程上引入相关依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.0</version>
</dependency>
在bootstrap.properties中国配置连接nacos
spring.cloud.sentinel.datasource.ds.nacos.server-addr=localhost:8848
spring.cloud.sentinel.datasource.ds.nacos.dataId=sentinel-rule
spring.cloud.sentinel.datasource.ds.nacos.groupId=DEFAULT_GROUP
spring.cloud.sentinel.datasource.ds.nacos.data-type=json
spring.cloud.sentinel.datasource.ds.nacos.rule-type=flow
启动项目后观察sentinel控制台会自动创建nacos上刚才配置的规则 以达到和在sentinel控制台配置流控规则相同的效果

微服务与Spring Cloud实战精析
1972

被折叠的 条评论
为什么被折叠?



