我们上节实现了Feign远程调用,现在我们的系统已经是很完整了,所缺少的是系统的可用性:比如我们的用户服务,访问电影购票服务,电影购票系统突然宕机了,用户系统并不知道对方宕机了,还在持续的调用等待,当大量的用户都在访问时,用户系统很可能会CPU飙升,导致系统不可用,最终由于电影系统宕机的问题,也导致了用户系统跟着一块宕机了。这个时候就体现出来系统容错性的重要。
对于容错一般情况下有3种解决方案:
方案一:
超时机制:配置一下超时时间,例如1秒——每次请求在1秒内必须返回,否则到点就把线程阻断,释放资源!
方案二:
舱壁模式:一般来说,现代的轮船都会分很多舱室,舱室之间用钢板焊死,彼此隔离。这样即使有某个/某些船舱进水,也不会影响其他舱室,浮力够,船不会沉。这样每一个服务都是独立的线程池。
方案三:
断路器:现实世界的断路器大家肯定都很了解,每个人家里都会有断路器。断路器实时监控电路的情况,如果发现电路电流异常,就会跳闸,从而防止电路被烧毁。主要就是:实时监测应用,如果发现在一定时间内失败次数/失败率达到一定阈值,就“跳闸”,断路器打开——此时,请求直接返回,而不去调用原本调用的逻辑。
跳闸一段时间后(例如15秒),断路器会进入半开状态,这是一个瞬间态,此时允许一次请求调用该调的逻辑,如果成功,则断路器关闭,应用正常调用;如果调用依然不成功,断路器继续回到打开状态,过段时间再进入半开状态尝试——通过”跳闸“,应用可以保护自己,而且避免浪费资源;而通过半开的设计,可实现应用的“自我修复“。
springcloud 使用Hystrix来进行服务的容错
Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。Hystrix主要通过以下几点实现延迟和容错。
包裹请求:使用HystrixCommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用到了设计模式中的“命令模式”。
跳闸机制:当某服务的错误率超过一定阈值时,Hystrix可以自动或者手动跳闸,停止请求该服务一段时间。
资源隔离:Hystrix为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。
监控:Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时、以及被拒绝的请求等。
回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑可由开发人员自行提供,例如返回一个缺省值。
自我修复:断路器打开一段时间后,会自动进入“半开”状态。
Eureka注册中心和服务提供者 代码不变
hystrix 代码如下:
pom引入
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>test-consumer-user-feign-hystrix</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test-consumer-user-feign-hystrix</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 引入Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--增加eurekaclient-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--增加feign 注解-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--增加 hystrix 注解-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
<!-- 引入spring cloud的依赖,不能少,主要用来管理Spring Cloud生态各组件的版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动类
package com.example.testconsumeruserfeignhystrix;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
// hystrix 在启动类上添加 @EnableCircuitBreaker 注解
@EnableCircuitBreaker
public class TestConsumerUserFeignHystrixApplication {
public static void main(String[] args) {
SpringApplication.run(TestConsumerUserFeignHystrixApplication.class, args);
}
}
实体类
package com.example.testconsumeruserfeignhystrix.pojo;
import lombok.*;
/**
* 图书馆信息
*
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
// 为类提供一个全参的构造方法
@AllArgsConstructor
public class LibraryInfo {
private Long id;
/**
* 图书馆名称
*/
private String name;
/**
* 图书馆地址
*/
private String address;
/**
* 总座位数
*/
private Integer number;
/**
* 可预约数量
*/
private Integer count;
}
feign
package com.example.testconsumeruserfeignhystrix.api;
import com.example.testconsumeruserfeignhystrix.pojo.LibraryInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "test-provider-library")
public interface LibraryFeignClient {
/**
* 注意 @PathVariable("id") 括号里面的ID不能省略
* @param id
* @return
*/
@GetMapping("/library/{id}")
LibraryInfo getFindById(@PathVariable("id") Long id);
}
controller
package com.example.testconsumeruserfeignhystrix.controller;
import com.example.testconsumeruserfeignhystrix.api.LibraryFeignClient;
import com.example.testconsumeruserfeignhystrix.pojo.LibraryInfo;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户控制类
*/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
/**
* 注入feign
*/
@Autowired
private LibraryFeignClient libraryFeignClient;
@GetMapping("/library/{id}")
// 指定降级方法
@HystrixCommand(fallbackMethod ="findByIdFallback")
public LibraryInfo findById(@PathVariable Long id){
return libraryFeignClient.getFindById(id);
}
/**
* 降级方法
* @param id
* @return
*/
public LibraryInfo findByIdFallback(Long id,Throwable throwable){
log.info("进入降级方法,原因是:{}",throwable);
return new LibraryInfo(id,"默认图书馆","北京",1200,130);
}
}
配置文件
server:
port: 8005
spring:
## 指定注册到eureka server上的服务名称
application:
name: test-consumer-user-feign-hystrix
eureka:
client:
service-url:
# 指定eureka server通信地址,注意/eureka/小尾巴不能少
defaultZone: http://localhost:8761/eureka/
instance:
# 是否注册IP到eureka server,如不指定或设为false,那就会注册主机名到eureka server
prefer-ip-address: true
依次启动 注册中心,和服务类 ,消费类
访问:http://localhost:8005/user/library/1
服务访问没有问题
然后把,服务类关掉,再访问
说明已经进入降级方法了