Spring Cloud-9-Hystrix请求缓存

在高并发的情况下,处理好缓存,就能有效的降低服务器的压力,Spring Cloud的Hystrix也提供了请求缓存的能力,接下来,我们来看一下如何开启.

环境准备

还是基于之前的服务注册中心,提供者和消费者, 先把服务注册中心开启, 服务提供者和消费者需要修改.

通过方法重载开启缓存

当我们在使用自定义Hystrix请求命令时,我们只需要在实现HystrixCommand或者HystrixObservableCommand时,通过重载getCacheKey()方法来开启请求缓存.

修改消费者

修改一下之前的StringCommand类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class StringCommand extends HystrixCommand<String> {

private RestTemplate restTemplate;

private Long id;

@Override
protected String getFallback() {
Throwable executionException = getExecutionException();

log.info("Exception:{}", executionException.getMessage());

return new String("StringCommand.getFallback");
}

public StringCommand(Setter setter, RestTemplate restTemplate, Long id) {
super(setter);
this.restTemplate = restTemplate;
this.id = id;
}

public StringCommand(Setter setter, RestTemplate restTemplate) {
super(setter);
this.restTemplate = restTemplate;
}

@Override
protected String run() throws Exception {
return restTemplate.getForObject("http://EUREKA-CLIENT/dc/" + id, String.class);
}

@Override
protected String getCacheKey() {
return String.valueOf(id);
}
}

修改的部分就是,加了一个构造函数,添加了id, 然后在向提供者发请求时,携带了id参数,然后重写了getCacheKey()方法.

系统在运行时会根据getCacheKey()方法的返回值来判断这个请求和之前执行过的请求是否一样,如果一样的话,就会直接使用缓存数据,而不去请求服务提供者,所以很明显,getCacheKey()方法是在run()方法之前执行的.

然后在controller中添加一个方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@GetMapping("/testCache")
public String testCache(){
HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("commandKey");
HystrixRequestContext.initializeContext();

StringCommand stringCommand1 = new StringCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1L);
String str1 = stringCommand1.execute();

StringCommand stringCommand2 = new StringCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1L);
String str2 = stringCommand2.execute();

StringCommand stringCommand3 = new StringCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1L);
String str3 = stringCommand3.execute();

log.info("str1:{}", str1);
log.info("str2:{}", str2);
log.info("str3:{}", str3);

return null;
}

这里就是发送了三个请求,id都是1

修改服务提供者

修改eureka-client工程,中的/dc接口, 修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/dc/{id}")
public String dc(@PathVariable Long id) throws InterruptedException {
String services = "Services: " + discoveryClient.getServices();
log.info(services);

log.info("-------------/dc/{}", id);

if (id.equals(1L)){
return "id是1的返回值";
} else if (id.equals(2L)) {
return "id是2的返回值";
}

return services;
}

测试

修改好之后,启动服务提供者和消费者.然后访问http://localhost:9000/testCache

然后来看一下消费者端的控制台输出:

1
2
3
2019-02-25 15:33:25.284  INFO 30380 --- [nio-9000-exec-2] c.e.consumer.controller.DcController     : str1:id是1的返回值
2019-02-25 15:33:25.284 INFO 30380 --- [nio-9000-exec-2] c.e.consumer.controller.DcController : str2:id是1的返回值
2019-02-25 15:33:25.284 INFO 30380 --- [nio-9000-exec-2] c.e.consumer.controller.DcController : str3:id是1的返回值

是打印了三条内容

再来看一下服务提供者的控制台输出:

1
2019-02-25 15:33:25.248  INFO 21400 --- [nio-8081-exec-5] c.eureka.client.controller.DcController  : -------------/dc/1

只输出了一次,说明client端只收到了一次请求. 其他两次请求都是走的缓存.

清除缓存

当我们把服务提供者的信息修改掉之后,那么缓存中的数据也应该要被清除,否则用户在读取数据的时候可能取到的是缓存中的旧数据,那么如何清除缓存,来看一下:

修改controller的/testCache接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@GetMapping("/testCache")
public String testCache(){
HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("commandKey");
HystrixRequestContext.initializeContext();

StringCommand stringCommand1 = new StringCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1L);
String str1 = stringCommand1.execute();

// 清除缓存
HystrixRequestCache.getInstance(commandKey, HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf(1L));

StringCommand stringCommand2 = new StringCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1L);
String str2 = stringCommand2.execute();

StringCommand stringCommand3 = new StringCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1L);
String str3 = stringCommand3.execute();

log.info("str1:{}", str1);
log.info("str2:{}", str2);
log.info("str3:{}", str3);

return null;
}

其实就是第一次请求完成后加了一行HystrixRequestCache.getInstance(commandKey, HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf(1L)); 来清除缓存.

再次访问http://localhost:9000/testCache,然后看下控制台输出.

消费者端的输出还是:

1
2
3
2019-02-25 16:04:42.370  INFO 21660 --- [nio-9000-exec-1] c.e.consumer.controller.DcController     : str1:id是1的返回值
2019-02-25 16:04:42.370 INFO 21660 --- [nio-9000-exec-1] c.e.consumer.controller.DcController : str2:id是1的返回值
2019-02-25 16:04:42.370 INFO 21660 --- [nio-9000-exec-1] c.e.consumer.controller.DcController : str3:id是1的返回值

而提供者端的输出是:

1
2
2019-02-25 16:04:42.324  INFO 21400 --- [nio-8081-exec-9] c.eureka.client.controller.DcController  : -------------/dc/1
2019-02-25 16:04:42.358 INFO 21400 --- [nio-8081-exec-2] c.eureka.client.controller.DcController : -------------/dc/1

可以看到,提供者是接收到了两次请求的, 因为我们在第一次请求结束后清理了缓存.

通过注解开启缓存

通过注解也可以开启缓存,相关注解有以下三个:

  • @CacheResult
  • @CacheKey
  • @CacheRemove

    分别来看一下这几个注解

@CacheResult

@CacheResult可以用在方法上面,作用就是给该方法开启缓存,默认情况下所有的参数都会作为缓存的key

修改HelloService的方法,如下:

1
2
3
4
5
6
7
8
9
10
11
@CacheResult
@HystrixCommand(fallbackMethod = "helloFallback")
public String helloService(Long id, String name){
return restTemplate.getForObject("http://EUREKA-CLIENT/dc/{1}", String.class, id);
}


@HystrixCommand
public String helloFallback(Long id, String name){
return "error";
}

我们在helloService这个方法上添加了@CacheResult注解,此时这个方法就会开启缓存,所有的参数都会作为缓存的key,如果再次调用传入的参数和之前的参数都一致的话,就会直接使用缓存,否则才会发起请求:

controller中,添加以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/testCache2")
public String testCache2(){
HystrixRequestContext.initializeContext();

// 第一次请求
helloService.helloService(1L, "zhangsan");

// 第二次请求,参数和第一次一致
helloService.helloService(1L, "zhangsan");

// 第三次请求,修改参数
helloService.helloService(2L, "lisi");

return null;
}

这里的第一次请求会给提供者发送请求,第二次的话,因为参数和第一次请求是一致的,所以这里会走缓存, 第三次请求因为参数变了,所以会给提供者发送请求获取数据

自定义缓存的key

我们也可以自定义缓存的key,不用默认的规则,只需要在@CacheResult中添加cacheKeyMethod属性来指定返回缓存key的方法,注意,key必须是String类型的,看个例子:

1
2
3
4
5
6
7
8
9
@CacheResult(cacheKeyMethod = "getCacheKey")
@HystrixCommand(fallbackMethod = "helloFallback")
public String helloService(Long id, String name){
return restTemplate.getForObject("http://EUREKA-CLIENT/dc/{1}", String.class, id);
}

public String getCacheKey(Long id, String name){
return String.valueOf(id);
}

此时,就会用id作为key,默认的规则就失效了.

@CacheKey

我们也可以通过使用@CacheKey注解来指定缓存的key,如下:

1
2
3
4
5
@CacheResult
@HystrixCommand(fallbackMethod = "helloFallback")
public String helloService(@CacheKey Long id, String name){
return restTemplate.getForObject("http://EUREKA-CLIENT/dc/{1}", String.class, id);
}

这里我们给id这个参数加了@CacheKey注解,意思就是会使用id这个参数来作为Key,跟name这个参数就没有任何关系,此时,只要id相同,就可以认为是同一个请求.

如果同时指定了@CacheResult中的cacheKeyMethod来指定key,又使用了@CacheKey注解,这个时候@CacheKey是没有效果的

@CacheRemove

这个就是让缓存失效的注解,具体用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@CacheResult(cacheKeyMethod = "getCacheKey")
@HystrixCommand(fallbackMethod = "helloFallback")
public String helloService(Long id, String name){
return restTemplate.getForObject("http://EUREKA-CLIENT/dc/{1}", String.class, id);
}

public String getCacheKey(Long id, String name){
return String.valueOf(id);
}

@CacheRemove(commandKey = "helloService")
@HystrixCommand
public String cacheRemove(@CacheKey Long id, String name){
return null;
}

这里必须指定commandKey,commandKey的值就是缓存的位置,配置了commandKey属性的值,Hystrix才能找到请求命令缓存的位置.
在controller中测试如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@GetMapping("/testCache2")
public String testCache2(){
HystrixRequestContext.initializeContext();

// 第一次请求
helloService.helloService(1L, "zhangsan");

// 清除缓存
helloService.cacheRemove(1L,"");

// 第二次请求,缓存被清除,重新发起
helloService.helloService(1L, "zhangsan");

// 第三次请求,参数一致,使用缓存
helloService.helloService(1L, "lisi");

return null;
}