Skip to content

哦一、什么是灾难性的雪崩效应

什么是灾难性雪崩效应?

俗话说:当雪崩发生时,没有一片雪花是无辜的!!!

那么微服务架构种的灾难性雪崩效应是如何发生的?

平稳状态

在微服务架构的项目中,尤其是中大型项目,肯定会出现一个服务调用其他的服务,其他服务又调用别的服务,服务和服务之间形成了一种链式的调用关系。

当少量请求时,对于整个服务链条是没有过多的影响的。

极端状态出现

虽然每个服务的请求都是少量的,但是最终都访问服务T。所以对于服务T来说请求量就是比较大的。所在的服务器CPU压力比较高。

雪崩的开始

当其中某一个服务突然遇到大量请求时。整个链条上所有服务负载骤增。

灾难到来

导致服务U和服务T的负载过高。运行性能下降。会导致其他调用服务U和服务T的链条出现问题。从而所有的项目可能都出现的问题。

这种情况就称为灾难性的雪崩效应。

灾难性雪崩效应发生的原因

造成灾难性雪崩效应的原因,可以简单归结为下述两种:

  1. 服务提供者(Application Service)不可用。如:硬件故障、程序BUG、缓存击穿、并发请求量过大等。
  2. 重试加大流量。如:用户重试、代码重试逻辑等。

雪崩效应最终的结果就是:服务链条中的某一个服务不可用,导致一系列的服务不可用,最终造成服务逻辑崩溃。这种问题造成的后果,往往是无法预料的。

二、如何防止灾难性雪崩效应的发生

降级

超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. 此方案保证的是:即使某服务出现了问题,整个项目还可以继续运行。

服务发生异常:

你看到的(内部结果,受不了了啊!!):

别人看到的(降级结果,人机比真人厉害,求求你别上线!!):

熔断

当失败率(如因网络故障/超时造成的失败率高)达到阀值自动触发降级,熔断器触发的快速失败会进行快速恢复。

通俗理解:熔断就是具有特定条件的降级,当出现熔断时在设定的时间内容就不在请求Application Service了。所以在代码上熔断和降级都是一个注解。

此方案保证的是:即使某服务出现了问题,整个项目还可以继续运行,且一段时间内,出现问题的服务不会被访问。(熔断:就是统计在一定的时间内,我们调用对方的服务失败率、失败次数,达到一定程度后,说明对方不是高可用,我们就进入熔断开启效果。开启熔断后,在一定的时间内就不会调用对方了,直接走fallback方法!但是,熔断是可以恢复的,熔断过了一段时间后,尝试调用一下对方,如果这次调用能够成功,关闭熔断,如果不成功,继续熔断一段时间!)

服务发生异常(严重问题):

你看到的(心里不爽)(内部结果):

别人看到的(大快人心)(熔断后的结果):

请求缓存

提供了请求缓存。服务A调用服务B,如果在A中添加请求缓存,第一次请求后走缓存了,就不在访问服务B了,即使出现大量请求时,也不会对B产生高负载。

请求缓存可以使用Spring Cache实现。

此方案保证的是:减少对Application Service的调用。

问题发生(信号不好,减少与远程服务器的连接):

你看到的(自以为的快乐,单机也很开心):

别人看到的(自欺欺人的乐趣,别人以为你在排位):

请求合并

提供请求合并。当服务A调用服务B时,设定在5毫秒内所有请求合并到一起,对于服务B的负载就会大大减少,解决了对于服务B负载激增的问题。

此方案保证的是:减少对Application Service的调用。

我以为的(平稳流量):

问题来了(实际流量):

解决办法(还是大壶过瘾):

隔离

隔离分为线程池隔离和信号量隔离。通过判断线程池或信号量是否已满,超出容量的请求直接降级,从而达到限流的作用。

未隔离时,资源公用,所以混乱:

隔离后,资源独享,秩序井然:

三、Hystrix简介

在Spring Cloud中解决灾难性雪崩效应就是通过Spring Cloud Netflix Hystrix实现的。

Hystrix [hɪst'rɪks],中文含义是豪猪,因其背上长满棘刺,从而拥有了自我保护的能力。本文所说的Hystrix(中文:断路器)是Netflix开源的一款容错框架,同样具有自我保护能力。

通俗解释:Hystrix就是保证在高并发下即使出现问题也可以保证程序继续运行的一系列方案。作用包含两点:容错和限流。

在Spring cloud中处理服务雪崩效应,都是在服务调用方(Application Client)实现,需要依赖hystrix组件。

四、搭建基础环境

此工程作为微服务体系中,可能发生问题的节点。

创建父工程

创建工程

创建工程parent

POM依赖

xml
<parent>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-parent</artifactId>
 <version>2.3.12.RELEASE</version>
</parent>
<dependencyManagement>
 <dependencies>
 <!-- Spring Cloud框架版本集中管理 -->
 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-dependencies</artifactId>
 <version>Hoxton.SR12</version>
 <type>pom</type>
 <scope>import</scope>
 </dependency>
 </dependencies>
</dependencyManagement>

创建POJO工程

创建工程

创建工程pojo

POM依赖

xml
<dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
</dependency>

编写实体类型

java
`@Data`
`@NoArgsConstructor`
`@AllArgsConstructor`
public class User implements Serializable {
 private Integer id;
 private String name;
 private int age;
}

创建Application Service工程

创建工程

创建工程 app_service

POM依赖

xml
<dependencies>
 <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>
 <dependency>
 <groupId>com.lhp</groupId>
 <artifactId>pojo</artifactId>
 <version>1.0-SNAPSHOT</version>
 </dependency>
</dependencies>

编写配置文件

yaml
server:
 port: 8080
spring:
 application:
 name: app-service

编写控制器

java
`@RestController`
public class ServiceController {

 /**
 * 模拟根据主键批量查询用户。
 */
 `@RequestMapping`("/batch")
 public List<User> getUsersByIds(`@RequestBody` List<Integer> ids){
 List<User> result = new ArrayList<>();
 for(Integer id : ids){
 result.add(new User(id, "姓名-" + id, 20));
 }
 return result;
 }

 `@GetMapping`("/test")
 public String test(){
 return "服务器AppService返回结果";
 }
}

编写启动类型

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

五、降级

降级是指,当请求超时、资源不足等情况发生时进行服务降级处理,不调用真实服务逻辑,而是使用快速失败(fallback)方式直接返回一个托底数据,保证服务链条的完整,避免服务雪崩。具体解决方案如下。

创建工程

创建工程app_client

POM依赖

xml
<dependencies>
 <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>
 <!-- 容灾处理依赖。 -->
 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
 </dependency>
 <dependency>
 <groupId>com.lhp</groupId>
 <artifactId>pojo</artifactId>
 <version>1.0-SNAPSHOT</version>
 </dependency>
</dependencies>

编写配置文件

yaml
server:
 port: 8000
spring:
 application:
 name: hystrix-ribbon-resttemplate

编写配置类

java
package com.lhp.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.`RestTemplate`;

/**
 * `@author` lhp
 * `@create` 2025-08-18 14:40
 */
`@Configuration`
public class MyConfig {

 `@Bean`
 `@LoadBalanced` // 整合Ribbon负载均衡
 public `RestTemplate` restTemplate(){
 // 创建http请求工厂对象
 HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
 // 设置建立tcp连接的超时时间
 httpRequestFactory.setConnectTimeout(2000);
 // 设置等待对方响应的超时时间
 httpRequestFactory.setReadTimeout(2000);
 return new `RestTemplate`(httpRequestFactory);
 }
}

编写服务接口与实现

编写服务接口

java
public interface Hystrix`RestTemplate`Service {
 String test();
}

编写服务实现

java
`@Service`
public class Hystrix`RestTemplate`ServiceImpl implements Hystrix`RestTemplate`Service {
 `@Autowired`
 private `RestTemplate` restTemplate;
 private final String baseUrl = "http://app-service";

 /**
 * 增加注解 HystrixCommand
 * 注解属性 fallbackMethod - 降级方法的名称
 * `@return`
 */
 `@Override`
 `@HystrixCommand`(fallbackMethod = "downgrade")
 public String test() {
 String url = baseUrl + "/test";
 System.out.println("准备访问远程服务 : /test");
 String result = restTemplate.getForObject(url, String.class);
 System.out.println("远程服务返回:" + result);
 return result;
 }

 /**
 * 降级方法。除方法名称外,其他和具体的服务方法签名一致
 * 降级方法的返回结果,就是托底数据
 */
 public String downgrade(){
 System.out.println("降级方法运行。");
 return "服务器忙,请稍后重试";
 }
}

编写控制器

java
`@RestController`
public class Hystrix`RestTemplate`Controller {
 `@Autowired`
 private Hystrix`RestTemplate`Service service;
	
 `@RequestMapping`("/test")
 public String getNoParams(){
 System.out.println("控制器执行 - test()");
 return service.test();
 }
}

编写启动类型

java
/**
 * EnableHystrix - 开启Hystrix功能。netflix hystrix包提供
 * EnableCircuitBreaker - 开启熔断器|断路由|断路器。spring circuitbreaker提供
 *
 * 这两个注解都是让Spring可以识别Hystrix的注解。
 */
`@SpringBootApplication`
`@EnableHystrix`
`@EnableCircuitBreaker`
public class Hystrix`RestTemplate`App {
 public static void main(String[] args) {
 SpringApplication.run(Hystrix`RestTemplate`App.class, args);
 }
}

测试

访问 http://localhost:8000/test 得到正常返回结果:

关闭app_service工程后,再次访问,得到降级结果:

六、熔断

熔断介绍

当一定时间内,异常请求比例(请求超时、网络故障、服务异常等)达到阀值时,启动熔断器,熔断器一旦启动,则会停止调用具体服务逻辑,通过fallback快速返回托底数据,保证服务链的完整。

熔断有自动恢复机制,如:当熔断器启动后,每隔5秒,尝试将新的请求发送给Application Service,如果服务可正常执行并返回结果,则关闭熔断器,服务恢复。如果仍旧调用失败,则继续返回托底数据,熔断器持续开启状态。

降级是出错了返回托底数据,而熔断是出错的比例达到一定程度后会开启熔断,开启了熔断将会在一定时间不再访问application service。

注:上面的关闭状态、启动状态指的是熔断器处于关闭状态、熔断器处于启动状态。

修改服务接口和实现

服务接口增加方法

java
String circuitBreaker();

服务实现增加方法

java
/**
 * 测试熔断,强化降级。
 * 注解属性 commandProperties - 具体的容灾配置参数。
 * 类型是HystrixProperty[], HystrixProperty类型是名值对。
 * 名 - 是具体的配置参数名,字符串类型,可以从HystrixPropertiesManager中查看,也可以
 * 使用其中的静态常量。
 * 值 - 参数值,字符串类型。
 */
`@Override`
`@HystrixCommand`(fallbackMethod = "circuitBreakerDowngrade", commandProperties = {
 `@HystrixProperty`(name = HystrixPropertiesManager
 .EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,
 value = "5000"), // 统计周期,默认10秒
 `@HystrixProperty`(name = HystrixPropertiesManager
 .CIRCUIT_BREAKER_ENABLED,
 value = "true"), // 是否开启熔断,默认true
 `@HystrixProperty`(name = HystrixPropertiesManager
 .CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,
 value = "2"), // 统计周期内,错误几次,开启熔断, 默认20
 `@HystrixProperty`(name = HystrixPropertiesManager
 .CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE,
 value = "50"), // 统计周期内,错误百分比达到多少,开启熔断, 默认50
 `@HystrixProperty`(name = HystrixPropertiesManager
 .CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,
 value = "30000"), // 开启熔断后,多少毫秒不访问远程服务,默认5000毫秒
 `@HystrixProperty`(name = HystrixPropertiesManager
 .CIRCUIT_BREAKER_FORCE_OPEN,
 value = "false"), // 是否强制开启熔断器, 默认false
 `@HystrixProperty`(name = HystrixPropertiesManager
 .CIRCUIT_BREAKER_FORCE_CLOSED,
 value = "false") // 是否强制关闭熔断器, 默认false
})
public String circuitBreaker() {
 String url = baseUrl + "/test";
 System.out.println("准备访问远程服务,地址是:" + url);
 String result = restTemplate.getForObject(url, String.class);
 System.out.println("远程返回结果是:" + result);
 return result;
}

/**
 * 熔断降级方法
 */
public String circuitBreakerDowngrade(){
 System.out.println("熔断降级触发");
 return "网站建设中";
}

注解属性含义解释

  1. CIRCUIT_BREAKER_ENABLED"circuitBreaker.enabled";是否开启熔断策略。默认值为true。
  2. CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD"circuitBreaker.requestVolumeThreshold";单位时间内(默认10s内),请求超时数超出则触发熔断策略。默认值为20次请求数。通俗说明:单位时间内容要判断多少次请求。
  3. EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDSexecution.isolation.thread.timeoutInMilliseconds设置单位时间,判断circuitBreaker.requestVolumeThreshold的时间单位,默认10秒。单位毫秒。
  4. CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS"circuitBreaker.sleepWindowInMilliseconds";当熔断策略开启后,延迟多久尝试再次请求远程服务。默认为5秒。单位毫秒。这5秒直接执行fallback方法,不在请求远程application service。
  5. CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE"circuitBreaker.errorThresholdPercentage";单位时间内,出现错误的请求百分比达到限制,则触发熔断策略。默认为50%。
  6. CIRCUIT_BREAKER_FORCE_OPEN"circuitBreaker.forceOpen";是否强制开启熔断策略。即所有请求都返回fallback托底数据。默认为false。
  7. CIRCUIT_BREAKER_FORCE_CLOSED"circuitBreaker.forceClosed";是否强制关闭熔断策略。即所有请求一定调用远程服务。默认为false。

控制器增加方法

java
`@RequestMapping`("/circuitBreaker")
public String circuitBreaker(){
	System.out.println("控制器执行 - circuitBreaker()");
	return service.circuitBreaker();
}

测试

七、请求缓存

介绍

Hystrix为了降低访问服务的频率,支持将一个请求与返回结果做缓存处理。如果再次请求的URL没有变化,那么Hystrix不会请求服务,而是直接从缓存中将结果返回。这样可以大大降低访问服务的压力。 Hystrix自带缓存。有两个缺点:

  1. 是一个本地缓存(会缓存到本地服务器的内存中)。在集群情况下缓存是不能同步的。
  2. Hystrix内部并没有直接使用第三方的缓存作为存储,比如Redis、mongoDB等,但我们可以使用一些技术将数据存储到第三方工具中

可以利用spring cache技术,实现请求缓存。 在降级处理的代码基础上完成下面变化。

POM依赖

xml
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

编写配置文件

在application.yml配置文件中增加下述配置:(如果是redis集群使用spring.redis.cluster.nodes进行配置)

yaml
spring:
 redis:
 host: 192.168.192.128

修改启动类

在启动类上面多添加一个注解:

java
`@SpringBootApplication`
`@EnableEurekaClient`
`@EnableHystrix`
`@EnableCircuitBreaker`
`@EnableCaching` // 开启缓存功能
public class AppClientApp {

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

编辑服务实现

修改服务实现类中的服务降级方法逻辑,具体如下:

java
/**
 * 测试请求缓存。
 * 使用Spring Cache技术实现,访问Redis,做缓存处理。
 * Spring Cache是spring-context.jar提供的技术。可以访问多种缓存服务器。
 * 包括redis。
 * 想使用Spring Cache技术,访问缓存服务,需要提供以下依赖:
 * 1. spring-context.jar,直接或间接。
 * 2. 要访问的缓存服务器客户端依赖。如:访问Redis需要Spring Data Redis依赖
 * 使用Spring Cache技术后,查询逻辑的流程是:
 * 1. 访问缓存,查看是否有缓存的结果。如果有直接返回,不执行当前方法。
 * 2. 如果缓存中没有结果,则执行方法。
 * 3. 方法返回结果,会被Spring Cache技术自动保存到缓存服务器中。
 * 4. 方法结束,返回给调用者。
 * 注解: Cacheable
 * 属性:
 * cacheNames - 缓存中key的前缀
 * key - 缓存中key的后缀。可以使用表达式赋值。字面值用单引号标记。方法参数变量
 * 使用#参数名标记,可以使用字符串拼接符号 +
 * 完整的缓存key是 前缀 + :: + 后缀
 */
`@Override`
`@HystrixCommand`(fallbackMethod = "downgrade")
`@Cacheable`(key = "'client'",cacheNames = "com:lhp")
public String test() {
 String url = baseUrl + "/test";
 System.out.println("准备访问远程服务 : /test");
 String result = restTemplate.getForObject(url, String.class);
 System.out.println("远程服务返回:" + result);
 return result;
}

/**
 * 降级方法。除方法名称外,其他和具体的服务方法签名一致
 * 降级方法的返回结果,就是托底数据
 */
public String downgrade(){
 System.out.println("降级方法运行。");
 return "服务器忙,请稍后重试";
}

测试

通过访问 http://localhost:8000/test 测试请求缓存逻辑,第一次访问时,有远程服务调用,后续请求则不会访问远程服务。并在Redis中,通过命令keys com:lhp*查看缓存中的数据。

说明:如果方法中是有参数的话,可以按照下面写法去写:

java
`@Override`
`@HystrixCommand`(fallbackMethod = "downgrade")
// 假如传递过来的id=3,表示在redis中首先查找键是:com:lhp::3 的值,如果找到后,就不会执行该方法了,找不到才会执行,执行完后将结果存储到redis中,键是 com:lhp::3 值是结果
`@Cacheable`(cacheNames = "com:lhp", key = "#id")
public String test(Integer id) {
 String url = baseUrl + "/test";
 String s = restTemplate.getForObject(url, String.class);
 System.out.println("远程方法返回:" + s);
 return s;
}

八、请求合并

当没有请求合并处理时:Application Service 负载是Application Client发送请求的总数量。

增加请求合并处理后:一段时间范围内的所有请求合并为一个请求。大大的降低了Application Service 负载。

什么情况下使用请求合并?

在微服务架构中,我们将一个项目拆分成很多个独立的项目,这些独立的项目通过远程调用来互相配合工作,但是,在高并发情况下,通信次数的增加会导致总的通信时间增加,同时,线程池的资源也是有限的,高并发环境会导致有大量的线程处于等待状态,进而导致响应延迟,为了解决这些问题,我们需要来了解Hystrix的请求合并。

使用请求合并后的缺点

设置请求合并之后,本来一个请求可能5ms就搞定了,但是现在必须再等10ms看看还有没有其他的请求一起的,这样一个请求的耗时就从5ms增加到15ms了,不过,如果我们要发起的命令本身就是一个高延迟的命令,那么这个时候就可以使用请求合并了,因为这个时候时间窗的时间消耗就显得微不足道了,另外高并发也是请求合并的一个非常重要的场景。

代码实现

实现请求合并时,不仅仅需要修改Application Client代码,还需要Application Service的代码配合,因为Application Service必须支持把所有参数捆绑到一起的方式,同时还支持把多个值一起返回。此处理逻辑,对应Application Service工程中的/batch控制处理访问。

服务接口增加方法

java
Future<User> getUserById(Integer id);

服务实现增加方法

java
/**
 * 是一个批处理逻辑。是做请求合并处理的方法。
 * 注意,Hystrix中,请求合并处理,具体方法,不是单处理方法。
 * 当前方法,不会执行。由Hystrix通过代理封装后执行。
 * 增加一个额外的批处理方法逻辑。
 *
 * 注解 HystrixCollapser - 代表当前的方法,是一个要合并的方法。
 * 属性:
 * batchMethod - 批处理方法名称。
 * scope - 有效范围。
 * 可选值:
 * com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL
 * 全局有效。可以合并多个客户端的请求。
 * com.netflix.hystrix.HystrixCollapser.Scope.REQUEST 默认值
 * 请求内有效,只能合并一个请求中的多次远程调用。必须配合指定的框架才能生效。
 * 在当前方法中,会抛出异常。
 * collapserProperties - 合并约束,类似HystrixCommand注解中的commandProperties
 * 类型是HystrixProperty[]
 * `@param` id
 * `@return`
 */
`@Override`
`@HystrixCollapser`(batchMethod = "getUsersByIds",
 scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,
 collapserProperties = {
 `@HystrixProperty`(name =
 HystrixPropertiesManager.MAX_REQUESTS_IN_BATCH,
 value = "2"), // 最多合并多少个请求
 `@HystrixProperty`(name =
 HystrixPropertiesManager.TIMER_DELAY_IN_MILLISECONDS,
 value = "500") // 最多等待多少毫秒
 }
)
public Future<User> getUserById(Integer id) {
 System.out.println("根据主键查询用户方法,Service实现类型中的Override实现");
 return null;
}

/**
 * 批处理方法。编写访问远程批处理服务的逻辑。
 * 使用注解HystrixCommand修饰。
 *
 * 问题:
 * 使用`RestTemplate`访问远程服务的时候,如果服务返回结果是集合。
 * 集合的泛型是自定义类型。因为SpringMVC的`@ResponseBody是把Java对象转换成JSON返回`,
 * `RestTemplate`不知道返回的JSON对应的具体Java类型是什么,
 * 所以使用最通用的类型,Map转换。
 * 远程服务返回的是List<User>,SpringMVC注解ResponseBody处理后,返回的是字符串
 * [{"id":1,"name":"姓名1","age":20}, {...}]
 * `RestTemplate`转换上述JSON格式字符串,[]使用List集合处理。
 * {"id":1,"name":"姓名1","age":20}是什么类型?使用通用类型Map处理。
 * JSON对象是属性名id、name、age是map的key。属性值1、姓名1、20是map的value。
 *
 * 手工使用Jackson实现转换处理。
 *
 * 定义要求:
 * 1. 访问修饰符是public
 * 2. 返回值的类型是远程服务的返回类型。
 * 3. 方法命名,随意。不重复即可。
 * 4. 参数表,是List集合类型,泛型是要合并的方法参数类型。名称和要合并的方法参数名一致
 * 5. 抛出异常,不能抛出范围超过要合并的方法的异常类型。
 * `@param` id
 * `@return`
 */
`@HystrixCommand`
public List<User> getUsersByIds(List<Integer> id){
 ObjectMapper mapper = new ObjectMapper();

 String url = baseUrl + "/batch";
 System.out.println("准备访问远程服务,地址是:" + url + " , 参数是:" + id);
 List<LinkedHashMap> result =
 restTemplate.postForObject(url, id, List.class);
 List<User> users = new ArrayList<>(result.size());
 for(LinkedHashMap userMap : result){
 try {
 // 把Map转换成JSON格式字符串
 String userJson = mapper.writeValueAsString(userMap);
 // 把JSON格式字符串转换成User类型对象
 User user = mapper.readValue(userJson, User.class);
 // 把处理后的User类型对象,保存到返回结果集合中
 users.add(user);
 }catch (Exception e){
 e.printStackTrace();
 }
 }
 System.out.println("查询结果数量是:" + result.size());
 System.out.println("查询的用户集合是:" + result);
 return users;
}

注解属性含义

@HystrixCollapser 进行请求合并

batchMethod:处理请求合并的方法

scope:合并请求的请求作用域。可选值有global和request。

global:代表所有的请求线程都可以等待可合并。 常用

request:代表一个请求线程中的多次远程服务调用可合

timerDelayInMilliseconds:等待时长,默认10毫秒。

maxRequestInBatch:最大请求合并数量。

控制器增加方法

java
/**
 * 根据主键查询用户。
 * 具体实现逻辑,调用远程服务eureka-client-app-service批处理查询实现。
 * 合并当前的请求。把多次请求参数主键,合并成一个集合参数List<Integer>。
 * 一次性访问远程服务,返回的批处理查询结果,拆分后,返回给客户端。
 * `@param` id
 * `@return`
 */
`@RequestMapping`("/getUserById")
public User getUserById(Integer id){
	Future<User> future = service.getUserById(id);
	System.out.println("控制器执行 - getUserById()");
	try {
		return future.get();
	} catch (InterruptedException e) {
		e.printStackTrace();
	} catch (ExecutionException e) {
		e.printStackTrace();
	}
	// 发生了异常。返回null。
	return null;
}

九、线程池隔离

什么是限流

在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。

通过Hystrix的线程池隔离和信号量隔离控制了线程数量也就实现了限流效果。

为什么使用线程池隔离?

比如我们现在有3个业务调用分别是查询订单、查询商品、查询用户,且这三个业务请求都是依赖第三方服务-订单服务、商品服务、用户服务。三个服务均是通过RPC调用。当查询订单服务,假如线程阻塞了,这个时候后续有大量的查询订单请求过来,那么容器中的线程数量则会持续增加直致资源耗尽,导致整个服务对外不可用,这就是灾难性雪崩。如下图:

订单Application Service不可用

Application Client不可用

没有线程池隔离的时候可能因为某个接口的高并发导致其他接口也不可用。

增加线程池隔离后

当使用线程池隔离。不同接口有着自己独立的线程池,即使某个线程池都被占用,也不影响其他线程。

Hystrix中的线程池隔离

Hystrix采用Bulkhead Partition舱壁隔离技术。

舱壁隔离指的的是讲船体内部分为多个隔舱,一旦其中某几个隔舱发生破损进水,水流不会在其他舱壁中流动,从而保证船舱依然具有足够的浮力和稳定性,降低沉船危险。

线程池隔离的优点

  1. 任何一个服务都会被隔离在自己的线程池内,即使自己的线程池资源填满也不会影响其他服务。
  2. 当依赖的服务重新恢复时,可通过清理线程池,瞬间恢复服务的调用。但是如果是tomcat线程池被填满,再恢复就会很麻烦。
  3. 每个都是独立线程池。一定程度上解决了高并发问题。
  4. 由于线程池中线程个数是有限制,所以也解决了限流问题。

线程池隔离的缺点

  1. 增加了CPU开销。因为不仅仅有Tomcat的线程池,还需要有Hystrix线程池。
  2. 每个操作都是独立的线程,就有排队、调度和上下文切换等问题。

代码演示

服务接口增加方法

java
String thread();

服务实现增加方法

java
/**
 * 测试线程池隔离。 也可以提供降级、熔断等处理逻辑。
 * 访问远程服务/getNoParams测试。
 * 当前方法,使用独立的线程池。和其他方法隔离。
 * 提供注解HystrixCommand
 * groupKey - 分组唯一值。默认使用当前类型的类名。代表一个独立接口的唯一命名。
 * 一般都使用类型的名称定义。是使用 字母 ,数字 ,_ 组成的字符串。
 * commandKey - 命令唯一值。默认使用当前方法名称。代表一个独立接口中的命令唯一命名。
 * 一般使用方法名称定义。是使用 字母 ,数字 ,_ 组成的字符串
 * threadPoolKey - 隔离的线程池命名中缀。默认使用groupKey的值。
 * 定义后,线程池命名是 "hystrix-" + threadPoolKey。
 * 池中的线程命名是 "hystrix-" + threadPoolKey + 数字编号(从1开始,自然数递增)
 * threadPoolProperties - 定义隔离线程池的配置信息。如:线程池容量,线程存活时间等。
 * 类型是HystrixProperty[]
 * `@return`
 */
`@Override`
`@HystrixCommand`(groupKey = "MyFirstThread",
		commandKey = "thread",
		threadPoolKey = "pool-name",
		threadPoolProperties = {
		`@HystrixProperty`(name = HystrixPropertiesManager.CORE_SIZE,
				value = "3"), // 线程池容量
		`@HystrixProperty`(name = HystrixPropertiesManager.KEEP_ALIVE_TIME_MINUTES,
				value = "5"), // 线程空闲时,最大存活时间是多少分钟
		`@HystrixProperty`(name = HystrixPropertiesManager.MAX_QUEUE_SIZE,
				value = "5"), // 线程池占满时,最多由多少个请求阻塞等待
		`@HystrixProperty`(name = HystrixPropertiesManager
				.QUEUE_SIZE_REJECTION_THRESHOLD,
				value = "5") // 当阻塞队列MAX_QUEUE_SIZE占满时,可以由多少个
							 // 请求同时阻塞等待后续处理。
		}
)
public String thread() {
	String url = baseUrl + "/test";
	System.out.println("当前方法使用的线程名称是:" +
			Thread.currentThread().getName());
	String result =
			restTemplate.getForObject(url, String.class);
	System.out.println("远程返回:" + result);
	return result;
}

注解属性说明

控制器增加方法

java
/**
 * 线程池隔离测试。
 * `@return`
 */
`@RequestMapping`("/thread")
public String thread(){
	System.out.println(Thread.currentThread().getName() +
			"控制器执行 - thread()");
	return service.thread();
}

其实:这些应对高并发可用的操作是需要组合使用的,比如这里的线程池隔离可以结合降级来使用,当线程池线程用完后,排队的队列也满了后,按照目前Hystrix的配置,其他的请求我们直接给拒绝了,那其实拒绝的这些请求可以走降级方法。

十、OpenFeign的容灾处理

概述

当使用OpenFeign调用远程服务超时会出现500错误。可使用Hystrix来实现容灾处理。

在OpenFeign启动器依赖中,默认包含Hystrix核心类库,但不包含Hystrix启动器中的全部资源,所以可实现Hystrix容灾处理方案,但不能实现Hystrix其他扩展处理(如@EnableHystrix注解就不包含在默认资源中)。

POM依赖

Application Client工程中增加下述依赖:

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

编写配置文件

在application.yml配置中增加下述配置:默认情况下OpenFeign的hystrix是不开启的,需要手动开启。

yaml
feign:
 hystrix:
 enabled: true

编写服务接口

编写FeignHystricService服务接口。使用OpenFeign声明式调用。fallback属性表示降级后处理类型。也可使用内部类实现。

java
`@FeignClient`(name = "app-service", fallback = FeignHystricServiceFallback.class)
public interface FeignHystricService {
 `@PostMapping`("/test")
 String test();
}

编写服务实现

java
`@Component`
public class FeignHystricServiceFallback implements FeignHystricService {
 `@Override`
 public String test() {
 System.out.println("执行方法,出现服务降级,返回托底数据");
 String result = "因为Provider连接不上了,返回托底数据";
 return result;
 }
}

修改启动类型

java
`@SpringBootApplication`
`@EnableHystrix`
`@EnableFeignClients`
`@EnableCircuitBreaker`
public class Hystrix`RestTemplate`App {
 public static void main(String[] args) {
 SpringApplication.run(Hystrix`RestTemplate`App.class, args);
 }
}

Openfeign中的熔断配置

在Openfeign技术中,如果需要开启熔断策略,则只需要在application.yml配置文件中增加下述配置即可。

yaml
hystrix: # hystrix 容灾配置
 command: # hystrix 命令配置,就是具体的容灾方案
 default: # 默认环境,相当于全局范围,后续的配置,参考HystrixPropertiesManager中的常量配置
 circuitBreaker: # 熔断配置, 常用。 其他配置不常用。
 enabled: true
 requestVolumeThreshold: 2
 sleepWindowInMilliseconds: 2000
 errorThresholdPercentage: 50

上面的这些配置在yml中可能不识别,但其实是可以使用的!