【第六章】Hystrix 请求缓存、请求合并、线程隔离

1.1 请求缓存

当系统用户不断增长时,每个微服务需要承受的并发压力也越来越大,在分布式环境中,通常压力来自对依赖服务的调用,因为依赖服务的资源需要通过通信来实现,这样的依赖方式比起进程内的调用方式会引起一部分的性能损失。

在高并发的场景下,Hystrix 提供了请求缓存的功能,我们可以方便的开启和使用请求缓存来优化系统,达到减轻高并发时的请求线程消耗、降低请求响应时间的效果。

Hystrix的缓存,这个功能是有点鸡肋的,因为这个缓存是基于request的,为什么这么说呢?因为每次请求来之前都必须HystrixRequestContext.initializeContext();进行初始化,每请求一次controller就会走一次filter,上下文又会初始化一次,前面缓存的就失效了,又得重新来。

所以你要是想测试缓存,你得在一次controller请求中多次调用那个加了缓存的serviceHystrixCommand命令。Hystrix的书上写的是:在同一用户请求的上下文中,相同依赖服务的返回数据始终保持一致。在当次请求内对同一个依赖进行重复调用,只会真实调用一次。在当次请求内数据可以保证一致性

因此,希望大家在这里不要理解错了。

1.1.1 请求缓存图解析

请求缓存图,如下所示:

在这里插入图片描述
假设两个线程发起相同的HTTP请求,Hystrix会把请求参数初始化到ThreadLocal中,两个Command异步执行,每个Command会把请求参数从ThreadLocal中拷贝到Command所在自身的线程中,Command在执行的时候会通过CacheKey优先从缓存中尝试获取是否已有缓存结果。

如果命中,直接从HystrixRequestCache返回,如果没有命中,那么需要进行一次真实调用,然后把结果回写到缓存中,在请求范围内共享响应结果。

RequestCache主要有三个优点:

  • 在当次请求内对同一个依赖进行重复调用,只会真实调用一次。
  • 在当次请求内数据可以保证一致性。
  • 可以减少不必要的线程开销

例子还是接着上篇的HelloServiceCommand来进行演示,我们只需要实现HystrixCommand的一个缓存方法名为getCacheKey()即可,代码如下所示:

public class HelloServiceCommand extends HystrixCommand<String> {
    
    

    private RestTemplate restTemplate;

    protected HelloServiceCommand(String commandGroupKey,RestTemplate restTemplate) {
    
    
    //根据commandGroupKey进行线程隔离的
        super(HystrixCommandGroupKey.Factory.asKey(commandGroupKey));
        this.restTemplate = restTemplate;
    }

    @Override
    protected String run() throws Exception {
    
    
        System.out.println(Thread.currentThread().getName());
        return restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
    }


    @Override
    protected String getFallback() {
    
    
        return "error";
    }


    //Hystrix的缓存
    @Override
    protected String getCacheKey() {
    
    
        //一般动态的取缓存Key,比如userId,这里为了做实验写死了,写为hello
        return "hello";
    }
}

Controller代码如下所示:

@RestController
public class ConsumerController {
    
    

    @Autowired
    private  RestTemplate restTemplate;

    @RequestMapping("/consumer")
    public String helloConsumer() throws ExecutionException, InterruptedException {
    
    

        //Hystrix的缓存实现,这功能有点鸡肋。
        HystrixRequestContext.initializeContext();
        HelloServiceCommand command = new HelloServiceCommand("hello",restTemplate);
        String execute = command.execute();//清理缓存
//       HystrixRequestCache.getInstance("hello").clear();
        return null;
    }

}

在原来的两个provider模块都增加增加一条输出语句,如下:

provider1模块:

@RestController
public class HelloController {
    
    

    @RequestMapping("/hello")
    public String hello(){
    
    
        System.out.println("访问来1了......");
        return "hello1";
    }

}

provider2模块:

@RestController
public class HelloController {
    
    

    @RequestMapping("/hello")
    public String hello(){
    
    
        System.out.println("访问来2了......");
        return "hello1";
    }

}

浏览器输入localhost:8082/consumer

运行结果如下:

在这里插入图片描述
可以看到你刷新一次请求,上下文又会初始化一次,前面缓存的就失效了,又得重新来,这时候根本就没有缓存了。因此,你无论刷新多少次请求都是出现“访问来了”,缓存都是失效的。如果是从缓存来的话,根本就不会输出“访问来了”。

但是,你如你在一起请求多次调用同一个业务,这时就是从缓存里面取的数据。不理解可以看一下Hystrix的缓存解释:在同一用户请求的上下文中,相同依赖服务的返回数据始终保持一致。在当次请求内对同一个依赖进行重复调用,只会真实调用一次。在当次请求内数据可以保证一致性

Controller代码修改如下:

@RestController
public class ConsumerController {
    
    

    @Autowired
    private  RestTemplate restTemplate;

    @RequestMapping("/consumer")
    public String helloConsumer() throws ExecutionException, InterruptedException {
    
    

        //Hystrix的缓存实现,这功能有点鸡肋。
        HystrixRequestContext.initializeContext();
        HelloServiceCommand command = new HelloServiceCommand("hello",restTemplate);
        String execute = command.execute();
      HelloServiceCommand command1 = new HelloServiceCommand("hello",restTemplate);
        String execute1 = command1.execute();
  //清理缓存 
  // HystrixRequestCache.getInstance("hello").clear();

   return null; 
}

接着运行,运行结果如下:
在这里插入图片描述

可以看到只有一个”访问来了“,并没有出现两个”访问来了“

之所以没出现第二个,是因为是从缓存中取了。

删除缓存

例如删除key名为hello的缓存:

HystrixRequestCache.getInstance("hello").clear();

你要写操作的时候,你把一条数据给给删除了,这时候你就必须把缓存清空了。

1.2 请求合并、线程隔离

Hystrix中进行请求合并也是要付出一定代价的,请求合并会导致依赖服务的请求延迟增高,延迟的最大值是合并时间窗口的大小,默认为10ms,当然我们也可以通过hystrix.collapser.default.timerDelayInMilliseconds属性进行修改。

如果请求一次依赖服务的平均响应时间是20ms,那么最坏情况下(合并窗口开始是请求加入等待队列)这次请求响应时间就会变成30ms。在Hystrix中对请求进行合并是否值得主要取决于Command本身,高并发度的接口通过请求合并可以极大提高系统吞吐量。

从而基本可以忽略合并时间窗口的开销,反之,并发量较低,对延迟敏感的接口不建议使用请求合并。

通过使用HystrixCollapser可以实现合并多个请求批量执行。下面的图标显示了使用请求合并和不是请求合并,他们的线程迟和连接情况:

在这里插入图片描述
使用请求合并可以减少线程数和并发连接数,并且不需要使用这额外的工作。请求合并有两种作用域,全局作用域会合并全局内的同一个HystrixCommand请求,请求作用域只会合并同一个请求内的同一个HystrixCommand请求。但是请求合并会增加请求的延时。

在这里插入图片描述

可以看出Hystrix会把多个Command放入Request队列中,一旦满足合并时间窗口周期大小,Hystrix会进行一次批量提交,进行一次依赖服务的调用,通过充写HystrixCollapser父类的mapResponseToRequests方法,将批量返回的请求分发到具体的每次请求中。

首先我们先自定义一个BatchCommand类来继承Hystrix给我们提供的HystrixCollapser类,代码示例如下所示:

public class HjcBatchCommand extends HystrixCollapser<List<String>,String,Long> {
    
    

    private Long id;

    private RestTemplate restTemplate;
  //在200毫秒内进行请求合并,不在的话,放到下一个200毫秒
    public HjcBatchCommand(RestTemplate restTemplate,Long id) {
    
    
        super(Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("hjcbatch"))
                .andCollapserPropertiesDefaults(HystrixCollapserProperties.Setter()
                        .withTimerDelayInMilliseconds(200)));
        this.id = id;
        this.restTemplate = restTemplate;
    }

    //获取每一个请求的请求参数
    @Override
    public Long getRequestArgument() {
    
    
        return id;
    }

    //创建命令请求合并
    @Override
    protected HystrixCommand<List<String>> createCommand(Collection<CollapsedRequest<String, Long>> collection) {
    
    
        List<Long> ids = new ArrayList<>(collection.size());
        ids.addAll(collection.stream().map(CollapsedRequest::getArgument).collect(Collectors.toList()));
        HjcCommand command = new HjcCommand("hjc",restTemplate,ids);
        return command;
    }

    //合并请求拿到了结果,将请求结果按请求顺序分发给各个请求
    @Override
    protected void mapResponseToRequests(List<String> results, Collection<CollapsedRequest<String, Long>> collection) {
    
    
        System.out.println("分配批量请求结果。。。。");

        int count = 0;
        for (CollapsedRequest<String,Long> collapsedRequest : collection){
    
    
            String result = results.get(count++);
            collapsedRequest.setResponse(result);
        }
    }
}

接着用自定义个HjcCommand来继承Hystrix提供的HystrixCommand来进行服务请求

public class HjcCommand extends HystrixCommand<List<String>> {
    
    

    private RestTemplate restTemplate;
    private List<Long> ids;


    public HjcCommand(String commandGroupKey, RestTemplate restTemplate,List<Long> ids) {
    
    
      //根据commandGroupKey进行线程隔离
        super(HystrixCommandGroupKey.Factory.asKey(commandGroupKey));
        this.restTemplate = restTemplate;
        this.ids = ids;
    }

    @Override
    protected List<String> run() throws Exception {
    
    
        System.out.println("发送请求。。。参数为:"+ids.toString()+Thread.currentThread().getName());
        String[] result = restTemplate.getForEntity("http://HELLO-SERVICE/hjcs?ids={1}",String[].class, StringUtils.join(ids,",")).getBody();
        return Arrays.asList(result);
    }
}

但是注意一点:你请求合并必须要异步,因为你如果用同步,是一个请求完成后,另外的请求才能继续执行,所以必须要异步才能请求合并

所以Controller层代码如下:

@RestController
public class ConsumerController {
    
    
    @Autowired
    private  RestTemplate restTemplate;

    @RequestMapping("/consumer")
    public String helloConsumer() throws ExecutionException, InterruptedException {
    
    

        //请求合并
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        HjcBatchCommand command = new HjcBatchCommand(restTemplate,1L);
        HjcBatchCommand command1 = new HjcBatchCommand(restTemplate,2L);
        HjcBatchCommand command2 = new HjcBatchCommand(restTemplate,3L);

        //这里你必须要异步,因为同步是一个请求完成后,另外的请求才能继续执行,所以必须要异步才能请求合并
        Future<String> future = command.queue();
        Future<String> future1 = command1.queue();

        String r = future.get();
        String r1 = future1.get();

        Thread.sleep(2000);
        //可以看到前面两条命令会合并,最后一条会单独,因为睡了2000毫秒,而你请求设置要求在200毫秒内才合并的。
        Future<String> future2 = command2.queue();
        String r2 = future2.get();

        System.out.println(r);
        System.out.println(r1);
        System.out.println(r2);

        context.close();

        return null;

    }

}

两个服务提供者provider1provider2新增加一个方法来模拟数据库数据,代码如下:

@RestController
public class HelloController {
    
    

    @RequestMapping("/hello")
    public String hello(){
    
    
        System.out.println("访问来2了......");
        return "hello2";
    }

    @RequestMapping("/hjcs")
    public List<String> laowangs(String ids){
    
    
        List<String> list = new ArrayList<>();
        list.add("laowang1");
        list.add("laowang2");
        list.add("laowang3");
        return list;
    }

}

启动Ribbon模块,运行结果如下:
在这里插入图片描述
可以看到上图的两个线程是隔离的。

当请求非常多的时候,你合并请求就变得非常重要了,如果你不合并,一个请求都12秒,这明显不能忍的,会造成效率缓慢,如果你合并后,这时就可以并行处理,降低延迟,但是如果请求不多的时候,只有单个请求,这时候合并也会出现

效率缓慢的,因为如果请求一次依赖服务的平均响应时间是200ms,那么最坏情况下(合并窗口开始是请求加入等待队列)这次请求响应时间就会变成300ms。所以说要看场合而定的。

下面用注解的代码来实现请求合并。代码如下:

@Service
public class HjcService {
    
    

    @Autowired
    private RestTemplate restTemplate;

    @HystrixCollapser(batchMethod = "getLaoWang",collapserProperties = {
    
    @HystrixProperty(name = "timerDelayInMilliseconds",value = "200")})
    public Future<String> batchGetHjc(long id){
    
    
        return null;
    }

    @HystrixCommand
    public List<String> getLaoWang(List<Long> ids){
    
    
        System.out.println("发送请求。。。参数为:"+ids.toString()+Thread.currentThread().getName());
        String[] result = restTemplate.getForEntity("http://HELLO-SERVICE/hjcs?ids={1}",String[].class, StringUtils.join(ids,",")).getBody();
        return Arrays.asList(result);
    }

}

如果我们还要进行服务的监控的话,那么我们需要在Ribbon模块,和两个服务提供者模块提供如下依赖:

Ribbon模块依赖如下:

    <!--仪表盘-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
            <version>1.4.0.RELEASE</version>
        </dependency>

        <!--监控-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-actuator</artifactId>
        </dependency>

两个provider模块依赖如下:

    <!--监控-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-actuator</artifactId>
        </dependency>

接着在Ribbon启动类打上@EnableHystrixDashboard注解,然后启动,localhost:8082/hystrix,下图所示:
在这里插入图片描述
每次访问都有记录,如下:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_42039228/article/details/123732212