1.1 请求缓存
当系统用户不断增长时,每个微服务需要承受的并发压力也越来越大,在分布式环境中,通常压力来自对依赖服务的调用,因为依赖服务的资源需要通过通信来实现,这样的依赖方式比起进程内的调用方式会引起一部分的性能损失。
在高并发的场景下,Hystrix
提供了请求缓存的功能,我们可以方便的开启和使用请求缓存来优化系统,达到减轻高并发时的请求线程消耗、降低请求响应时间的效果。
Hystrix
的缓存,这个功能是有点鸡肋的,因为这个缓存是基于request
的,为什么这么说呢?因为每次请求来之前都必须HystrixRequestContext.initializeContext();
进行初始化,每请求一次controller
就会走一次filter
,上下文又会初始化一次,前面缓存的就失效了,又得重新来。
所以你要是想测试缓存,你得在一次controller
请求中多次调用那个加了缓存的service
或HystrixCommand
命令。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;
}
}
两个服务提供者provider1
,provider2
新增加一个方法来模拟数据库数据,代码如下:
@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
模块,运行结果如下:
可以看到上图的两个线程是隔离的。
当请求非常多的时候,你合并请求就变得非常重要了,如果你不合并,一个请求都1
到2
秒,这明显不能忍的,会造成效率缓慢,如果你合并后,这时就可以并行处理,降低延迟,但是如果请求不多的时候,只有单个请求,这时候合并也会出现
效率缓慢的,因为如果请求一次依赖服务的平均响应时间是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
,下图所示:
每次访问都有记录,如下: