如何使用Hystrix实现服务请求限流?

标签: Java

保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/131/ )。

概述

限流就是为某个方法增加流量阈值,当超过该阈值时,外部请求转而去调用备用方法,进而降低了原方法的压力,防止原方法因为过高的流量崩溃或给系统带来较大的时延。

环境配置

通过Hystrix就可以方便地实现这一操作。接下来,我们给出简单的示例。

首先,准备SpringBoot项目,然后在其中引入适用于SpringBoot的hystrix包:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.2.7.RELEASE</version>
</dependency>

注意,如果这时候启动提示如下的错误,则基本是SpringBoot版本与hystrix版本不匹配导致的。因为hystrix是基于SpringCloud的,而SpringCloud和SpringBoot存在版本对应关系。如果对应不一致,则可能导致出现下面的错误。

java.lang.NoSuchMethodError: 'void org.springframework.boot.builder.SpringApplicationBuilder.<init>(java.lang.Object[])'

这里我们给出一个可用的对应版本,即SpringBoot的2.3.5.RELEASE和spring-cloud-starter-netflix-hystrix的2.2.7.RELEASE。它们这一对是肯定没有问题的。

然后,我们需要在SpringBoot的主类上增加@EnableHystrix注解,以启用Hystrix。如下所示。

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

环境配置就是这么简单,接下来我们就可以写业务代码了。

功能实现

例如我们编写如下的方法,也就是原方法。在原方法中,我们故意让它处理时长较长,以模拟复杂的操作。

@HystrixCommand(
        commandKey = "LimiterService_queryUserNameById",
        fallbackMethod = "fallBackForSayHi",
        threadPoolProperties = {
                // 核心线程设置为5
                @HystrixProperty(name = HystrixPropertiesManager.CORE_SIZE, value = "5"),
                // 最大排队队列长度为10
                @HystrixProperty(name = HystrixPropertiesManager.MAX_QUEUE_SIZE, value = "10"),
                // 队列中最多有2个任务排队,超过该数目的任务则拒绝
                @HystrixProperty(name = HystrixPropertiesManager.QUEUE_SIZE_REJECTION_THRESHOLD, value = "2"),
        },
        commandProperties = {
                // 关掉熔断器,避免熔断器影响
                @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ENABLED, value = "false"),
                // 超时判定时间设置为2000ms,即下方的1000ms延时不会引发超时
                @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "2000"),
        }
)
String queryUserNameById(int i) throws InterruptedException {
    System.out.println("正在查询用户名,用户编号为" + i + "。该查询所在的线程名:" + Thread.currentThread().getName());
    // 故意增加处理时延,以模拟堵塞
    Thread.sleep(1000);
    return "易哥" + i;
}

我们在原方法上增加@HystrixCommand注解。其中比较关键的是fallbackMethod参数,我们指向了该原方法对应的备用方法,即当原方法发生错误时要调用的方法。备用方法如下所示。

String fallBackForSayHi(int i) {
    System.out.println("平台用户" + i);
    return "平台用户" + i;
}

然后,原方法上还有threadPoolProperties注解和commandProperties注解,关于这些注解的详细介绍大家可以参考《高性能架构之道(第二版)》这本书,其中讲解的比较细致。本文的示例代码也来自这本书。

然后,我们使用下面的方法并发调用目标方法100次,每两次调用之间间隔100ms。

/**
 * 演示Hystrix的限流功能
 * 该方法会使用多线程方式调用目标方法100次。每次间隔时间100ms
 *
 * @return 100次调用的目标方法返回值的拼接结果
 * @throws Exception 运行中发生的异常
 */
@RequestMapping("/limiter")
public String limiter() throws Exception {
    StringBuilder stringBuilder = new StringBuilder();

    // 创建100个调用limitService.sayHi方法的任务
    List<Callable<String>> callableList = new ArrayList<>();
    for (int i = 1; i <= 100; i++) {
        int finalI = i;
        callableList.add(() -> limiterService.queryUserNameById(finalI));
    }

    // 创建100个FutureTask以接收全部任务的返回值
    List<FutureTask<String>> futureTaskList = new ArrayList<>();
    for (Callable<String> callable : callableList) {
        futureTaskList.add(new FutureTask<>(callable));
    }

    // 以100ms为间隔,依次触发任务
    for (FutureTask<String> futureTask : futureTaskList) {
        Thread thread = new Thread(futureTask);
        thread.start();
        Thread.sleep(100);
    }

    // 接收任务的返回值
    for (FutureTask<String> futureTask : futureTaskList) {
        stringBuilder.append(futureTask.get()).append("\r\n");
    }

    return stringBuilder.toString();
}

则可以在控制台上看到如下的输出片段:

正在查询用户名,用户编号为2。该查询所在的线程名:hystrix-LimiterService-1
正在查询用户名,用户编号为1。该查询所在的线程名:hystrix-LimiterService-2
正在查询用户名,用户编号为3。该查询所在的线程名:hystrix-LimiterService-3
正在查询用户名,用户编号为4。该查询所在的线程名:hystrix-LimiterService-4
正在查询用户名,用户编号为5。该查询所在的线程名:hystrix-LimiterService-5
平台用户8
平台用户9
平台用户10
平台用户11
正在查询用户名,用户编号为7。该查询所在的线程名:hystrix-LimiterService-1
正在查询用户名,用户编号为6。该查询所在的线程名:hystrix-LimiterService-2
正在查询用户名,用户编号为12。该查询所在的线程名:hystrix-LimiterService-3
正在查询用户名,用户编号为13。该查询所在的线程名:hystrix-LimiterService-4
正在查询用户名,用户编号为14。该查询所在的线程名:hystrix-LimiterService-5

通过上述输出可以判断,用户编号1到5的调用直接进入线程执行,用户编号6、7的调用则进入了队列排队。而后续用户编号8到11的调用则直接进入了备用方法。与我们的参数设置一致。

然后,可以收集到如下的返回结果。

易哥1
易哥2
易哥3
易哥4
易哥5
易哥6
易哥7
平台用户8
平台用户9
平台用户10
平台用户11
易哥12
易哥13
易哥14

关于限流,大家常疑惑的点在于限流策略的选择。我们示例中使用的就是线程限流:目标方法的执行并发取决于线程池的配置。也可以采用信号量限流,那样目标方法的执行并发取决于信号量。关于两者的对比大家可以参考《高性能架构之道(第二版)》这本书。整篇文章的示例代码也来自这本书。

参数详解

以上实现的是最简单的隔离示例,也仅仅使用了最简单的参数,而其他参数都是用的默认值。但是这个作为示例是可以的,真正使用时,大家还是要对Hystrix的各个参数有详细的了解,至少也要知道大体的含义和默认值。否则,后续使用中,可能会遇到不少百思不得其解的地方。

总之,Hystrix要比这个强大的多。大家还是要好好学一下。

下面是我对@HystrixCommand参数的介绍,主要是摘抄参考了《高性能架构之道(第二版)》这本书。上面的示例也是参考的书里的。下面的参数我这里列的不是很全,如果要细致学习的话,十分建议大家去看下这本书。

HystrixCommand的主要参数如下:

其中,执行器参数如下;

线程隔离策略支持超时后的快速失败,即当原方法延迟过大时会直接失败或调用备用方法返回,而不需要等原方法全部执行完毕。当原方法中存在网络调用等容易引发超时的操作时,最好使用THREAD隔离策略。不过,相比于SEMAPHORE策略,其开销略大,因为SEMAPHORE不需要创建额外的线程。

OK,以上就是Hystrix实现请求隔离的示例。

可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐