前面几篇文章分析了 Spring Cloud 中的 Ribbon 和 Feign 实现负载均衡机制。但是有个问题需要注意下:
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又在调用其他的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,那么对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,这就是所谓的“雪崩效应”。
出现这种“雪崩效应”肯定是可怕的,在分布式系统中,我们无法保证某个服务一定不出问题,Hystrix 可以解决。
Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多服务无法避免会调用失败,比如超时、异常等等,Hystrix能够保证在一个服务出现问题的情况下,不会导致整体服务的失败,避免级联故障,以提高分布式系统的弹性。
所以叫“断路器”。“断路器”是一种开关装置,就好比我们家里的熔断保险丝,当出现突发情况,会自动跳闸,避免整个电路烧坏。那么当某个服务发生故障时,通过 Hystrix,会向调用方返回一个符合预期的、可处理的默认响应(也称备选响应,即fallBack),而不是长时间的等待或者直接返回一个异常信息。这样就能保证服务调用方可以顺利的处理逻辑,而不是那种漫长的等待或者其他故障。
这就叫“服务熔断”,就跟熔断保险丝一个道理。
服务熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务不可用或者响应时间太长,就会进行服务的降级,快速熔断该节点微服务的调用,返回默认的响应信息。当检测到该节点微服务调用响应正常后即可恢复。
上面提到服务的降级,什么意思呢?我打个比方:比如你去银行办理业务,本来有四个窗口都可以办理,现在3号窗口和4号窗口的办理人员有事要离开,那么自然地,用户就会跑去1号窗口或者2号窗口办理,所以1号和2号窗口就会承担更多的压力。
3号窗口和4号窗口的人有事走了,不能让人还在这排队等着吧,否则就出现了上文说的雪崩了,所以会挂一个牌子:暂停服务。这个牌子好比上文提到的熔断,然后返回一个默认的信息,让用户知道。等3号和4号窗口的人回来了,就会把这个牌子拿走,这两个窗口又可以继续回复服务了。
服务降级是在客户端完成的,不是服务端,与服务端是没有关系的。就像银行某个窗口挂了“暂停服务”,那客户会自然去别的窗口。
上面介绍了 Hystrix 的基本原理,接下来我们来落地到代码实现。新建一个项目工程:microservice-order-provider01-hystrix。然后将前面的miroservice-order-provider01的代码拷贝过来,做如下修改:
<dependencies>
<!-- hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--eureka-client客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
在启动类中,需要添加 @EnableCircuitBreaker
注解
@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.itcodai.springcloud.dao")
@EnableCircuitBreaker
public class OrderProvider01Hystrix {
public static void main(String[] args) {
SpringApplication.run(OrderProvider01Hystrix.class, args);
}
}
我们一起来看下 Controller 层的接口:
/**
* 订单服务
* @author shengwu ni
*/
@RestController
@RequestMapping("/provider/order")
public class OrderProviderController {
@Resource
private OrderService orderService;
private static final Logger LOGGER = LoggerFactory.getLogger(OrderProviderController.class);
/**
* HystrixCommond注解中的fallbackMethod指示的是:当该方法出异常时,调用processGetOrderHystrix方法
* @param id id
* @return 订单信息
*/
@GetMapping("/get/{id}")
@HystrixCommand(fallbackMethod = "processGetOrderHystrix")
public TOrder getOrder(@PathVariable Long id) {
TOrder order = orderService.findById(id);
if (order == null) {
throw new RuntimeException("数据库没有对应的信息");
}
return order;
}
/**
* 上面getOrder()方法出异常后的熔断处理方法
* @param id id
* @return 订单信息
*/
public TOrder processGetOrderHystrix(@PathVariable Long id) {
return new TOrder().setId(id)
.setName("未找到该ID的结果")
.setPrice(0d)
.setDbSource("No this datasource");
}
}
我来分析一下代码:OrderService 是上一节的 Feign 接口,我们使用 Feign 来接口式调用。我们看到,在 getOrder(id)
方法上添加了 @HystrixCommand(fallbackMethod = "processGetOrderHystrix")
注解,我简单解释一下:
@HystrixCommand
表示该接口开启 hystrix 熔断机制,如果出现问题,就去调用 fallbackMethod 属性指定的 processGetOrderHystrix
方法,那么往下看,就能看到 processGetOrderHystrix
方法,我们返回了和上面接口一样的数据结构,只不过都是我们自己搞的默认值而已。
getOrder(id)
这个接口中,当查不到订单信息,我故意手动抛出个异常方便我测试,我测试的时候把id搞大点,查不到即可。
启动 eureka 集群,启动这个带有熔断机制的订单提供服务:microservice-order-provider01-hystrix,再启动上一节的 Feign 客户端,在浏览器中输入:
http://localhost:9001/consumer/order/get/100,故意将id设为100,就会看到给我返回如下信息:
{"id":100,"name":"未找到该ID的结果","price":0.0,"dbSource":"No this datasource"}
这就说明 hystrix 已经做了熔断处理,请求没有任何问题。
上面介绍了 hystrix 的服务熔断和降级处理,但是有没有发现一个问题,这个 @HystrixCommand
注解是加在 Controller 层的接口方法上的,这会导致两个问题:
第一:如果接口方法很多,那么我是不是要在每个方法上都得加上该注解,而且,针对每个方法,我都要指定一个处理函数,这样会导致 Controller 变得越来越臃肿。
第二:这也不符合设计规范,理论上来说,Controller 层就是 Controller 层,我只管写接口即可。就像上一节介绍的 Feign,也是面向接口的,做均衡处理,我自己定义一个接口专门用来做均衡处理,在 Controller 层将该接口注入即可。那么 hystrix 是否也可以有类似的处理呢?
答案是肯定的,这跟面向切面编程一个道理,Cotroller 你只管处理接口逻辑,当出了问题,OK,交给我 hystrix ,我 hystrix 不在你 Controller 这捣蛋,我去其他地方呆着,你有问题了,我再来处理。这才是正确的、合理的设计方式。
所以我们完全不用像上文那样新建一个带 hystrix 的订单提供服务:microservice-order-provider01-hystrix。我们新建一个 hystrix 处理类:OrderClientServiceFallbackFactory,要实现FallbackFactory<OrderClientService>
接口,其中 OrderClientService
就是前面定义的 Feign 接口。
也就是说,把 hystrix 和 feign 绑起来,因为都是客户端的东东。我通过 feign 去调用服务的时候,如果出问题了,就来执行我自定义的 hystrix 处理类中的方法,返回默认数据。代码如下:
/**
* 统一处理熔断
* OrderClientService是Feign接口,所有访问都会走feign接口
* @author shengwu ni
*/
@Component
public class OrderClientServiceFallbackFactory implements FallbackFactory<OrderClientService> {
@Override
public OrderClientService create(Throwable throwable) {
return new OrderClientService() {
/**
* 当订单服务的getOrder()方法出异常后的熔断处理方法
* @param id id
* @return 返回信息
*/
@Override
public TOrder getOrder(Long id) {
return new TOrder().setId(id)
.setName("未找到该ID的结果")
.setPrice(0d)
.setDbSource("No this datasource");
}
@Override
public List<TOrder> getAll() {
return null;
}
};
}
}
我来分析一下代码,实现了 FallbackFactory<OrderClientService>
接口后,需要重写 create 方法,还是返回 OrderClientService
接口对象,只不过对这个 feign 客户端做了默认处理。
OK,现在 hystrix 是绑定了 Feign 接口了,但是 Feign 接口中的某个方法如果出问题了,它怎么知道找谁去做熔断呢?所以在 Feign 接口也需要绑定一下我们定义的 hystrix 处理类:
/**
* feign客户端
* @author shengwu ni
*/
//@FeignClient(value = "MICROSERVICE-ORDER")
@FeignClient(value = "MICROSERVICE-ORDER", fallbackFactory = OrderClientServiceFallbackFactory.class)
public interface OrderClientService {
@GetMapping("/provider/order/get/{id}")
TOrder getOrder(@PathVariable(value = "id") Long id);
@GetMapping("/provider/order/get/list")
List<TOrder> getAll();
}
我把之前的注释掉了,新添加了个 fallbackFactory 属性,指定了自定义的 hystrix 处理类。这样的话,Controller 中的所有方法都可以在 hystrix 里有个默认实现了。
同时,别忘了在 application.yml 中开启熔断:
# 开启熔断
feign:
hystrix:
enabled: true
OK,重新测一下,启动 eureka 集群,启动之前写好的未加 hystrix 的订单提供服务,三个当中随便起一个即可。再启动带有 hystrix 的 Feign 客户端,再测试一下上面的 url 即可。