前面几篇文章分析了 Spring Cloud 中的 Ribbon 和 Feign 实现负载均衡机制。但是有个问题需要注意下:

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又在调用其他的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,那么对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,这就是所谓的“雪崩效应”。

1. Hystrix 登场

出现这种“雪崩效应”肯定是可怕的,在分布式系统中,我们无法保证某个服务一定不出问题,Hystrix 可以解决。

Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多服务无法避免会调用失败,比如超时、异常等等,Hystrix能够保证在一个服务出现问题的情况下,不会导致整体服务的失败,避免级联故障,以提高分布式系统的弹性。

所以叫“断路器”。“断路器”是一种开关装置,就好比我们家里的熔断保险丝,当出现突发情况,会自动跳闸,避免整个电路烧坏。那么当某个服务发生故障时,通过 Hystrix,会向调用方返回一个符合预期的、可处理的默认响应(也称备选响应,即fallBack),而不是长时间的等待或者直接返回一个异常信息。这样就能保证服务调用方可以顺利的处理逻辑,而不是那种漫长的等待或者其他故障。

这就叫“服务熔断”,就跟熔断保险丝一个道理。

2. 服务熔断和服务降级

服务熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务不可用或者响应时间太长,就会进行服务的降级,快速熔断该节点微服务的调用,返回默认的响应信息。当检测到该节点微服务调用响应正常后即可恢复。

上面提到服务的降级,什么意思呢?我打个比方:比如你去银行办理业务,本来有四个窗口都可以办理,现在3号窗口和4号窗口的办理人员有事要离开,那么自然地,用户就会跑去1号窗口或者2号窗口办理,所以1号和2号窗口就会承担更多的压力。

3号窗口和4号窗口的人有事走了,不能让人还在这排队等着吧,否则就出现了上文说的雪崩了,所以会挂一个牌子:暂停服务。这个牌子好比上文提到的熔断,然后返回一个默认的信息,让用户知道。等3号和4号窗口的人回来了,就会把这个牌子拿走,这两个窗口又可以继续回复服务了。

服务降级是在客户端完成的,不是服务端,与服务端是没有关系的。就像银行某个窗口挂了“暂停服务”,那客户会自然去别的窗口。

3. Hystrix 的使用

上面介绍了 Hystrix 的基本原理,接下来我们来落地到代码实现。新建一个项目工程:microservice-order-provider01-hystrix。然后将前面的miroservice-order-provider01的代码拷贝过来,做如下修改:

3.1 依赖导入

<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>

3.2 启动类注解

在启动类中,需要添加 @EnableCircuitBreaker 注解

@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.itcodai.springcloud.dao")
@EnableCircuitBreaker
public class OrderProvider01Hystrix {

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

3.3 对接口的改动

我们一起来看下 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搞大点,查不到即可。

4. 测试一下

启动 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 已经做了熔断处理,请求没有任何问题。

5. 思考

上面介绍了 hystrix 的服务熔断和降级处理,但是有没有发现一个问题,这个 @HystrixCommand 注解是加在 Controller 层的接口方法上的,这会导致两个问题:

第一:如果接口方法很多,那么我是不是要在每个方法上都得加上该注解,而且,针对每个方法,我都要指定一个处理函数,这样会导致 Controller 变得越来越臃肿。

第二:这也不符合设计规范,理论上来说,Controller 层就是 Controller 层,我只管写接口即可。就像上一节介绍的 Feign,也是面向接口的,做均衡处理,我自己定义一个接口专门用来做均衡处理,在 Controller 层将该接口注入即可。那么 hystrix 是否也可以有类似的处理呢?

答案是肯定的,这跟面向切面编程一个道理,Cotroller 你只管处理接口逻辑,当出了问题,OK,交给我 hystrix ,我 hystrix 不在你 Controller 这捣蛋,我去其他地方呆着,你有问题了,我再来处理。这才是正确的、合理的设计方式。

5.1 定义 Hystrix 处理类

所以我们完全不用像上文那样新建一个带 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 客户端做了默认处理。

5.2 给 Feign 指定 hystrix

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 即可。