Spring Cloud Hystrix 的请求缓存

通过方法重载开启缓存

如果我们使用了自定义Hystrix请求命令的方式来使用Hystrix,那么我们只需要重写getCacheKey方法即可实现请求缓存,如下:

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
public class BookCommand extends HystrixCommand<Book> {

private RestTemplate restTemplate;
private Long id;

@Override
protected Book getFallback() {
Throwable executionException = getExecutionException();
System.out.println(executionException.getMessage());
return new Book("宋诗选注", 88, "钱钟书", "三联书店");
}

@Override
protected Book run() throws Exception {
return restTemplate.getForObject("http://HELLO-SERVICE/getbook5/{1}", Book.class,id);
}

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

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

系统在运行时会根据getCacheKey方法的返回值来判断这个请求是否和之前执行过的请求一样,即被缓存,如果被缓存,则直接使用缓存数据而不去请求服务提供者,那么很明显,getCacheKey方法将在run方法之前执行。我现在在服务提供者中打印一个日志,如下:

1
2
3
4
5
6
7
8
9
10
@RequestMapping(value = "/getbook5/{id}", method = RequestMethod.GET)
public Book book5(@PathVariable("id") Integer id) {
System.out.println(">>>>>>>>/getbook5/{id}");
if (id == 1) {
return new Book("《李自成》", 55, "姚雪垠", "人民文学出版社");
} else if (id == 2) {
return new Book("中国文学简史", 33, "林庚", "清华大学出版社");
}
return new Book("文学改良刍议", 33, "胡适", "无");
}

然后我们服务消费者的Controller中来执行这个请求,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping("/test5")
public Book test5() {
HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("commandKey");
HystrixRequestContext.initializeContext();
BookCommand bc1 = new BookCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1l);
Book e1 = bc1.execute();
BookCommand bc2 = new BookCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1l);
Book e2 = bc2.execute();
BookCommand bc3 = new BookCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1l);
Book e3 = bc3.execute();
System.out.println("e1:" + e1);
System.out.println("e2:" + e2);
System.out.println("e3:" + e3);
return e1;
}

我连着发起三个相同的请求,我们来看看服务提供者的日志打印情况,注意,在服务请求发起之前,需要先初始化HystrixRequestContext。执行效果如下:

图片

上面是服务提供者打印出来的日志,下面是服务消费者打印出来的日志,发起了三个请求,但是服务提供者实际上只执行了一次,其他两次都使用了缓存数据。

有一种特殊的情况:如果我将服务提供者的数据修改了,那么缓存的数据就应该被清除,否则用户在读取的时候就有可能获取到一个错误的数据,缓存数据的清除也很容易,也是根据id来清除,方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping("/test5")
public Book test5() {
HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("commandKey");
HystrixRequestContext.initializeContext();
BookCommand bc1 = new BookCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1l);
Book e1 = bc1.execute();
HystrixRequestCache.getInstance(commandKey, HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf(1l));
BookCommand bc2 = new BookCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1l);
Book e2 = bc2.execute();
BookCommand bc3 = new BookCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("")).andCommandKey(commandKey), restTemplate, 1l);
Book e3 = bc3.execute();
System.out.println("e1:" + e1);
System.out.println("e2:" + e2);
System.out.println("e3:" + e3);
return e1;
}

这里我们执行完第一次请求之后,id为1的数据就已经被缓存下来了,然后我通过HystrixRequestCache中的clear方法将缓存的数据清除掉,这个时候如果我再发起请求,则又会调用服务提供者的方法,我们来看一下执行结果,如下:

图片

此时服务提供者的方法执行了两次,因为我在第一次请求结束后将id为1的缓存清除了。

通过注解开启缓存

当然,我们也可以通过注解来开启缓存,和缓存相关的注解一共有三个,分别是@CacheResult、@CacheKey和@CacheRemove,我们分别来看。

@CacheResult

@CacheResult方法可以用在我们之前的Service方法上,表示给该方法开启缓存,默认情况下方法的所有参数都将作为缓存的key,如下:

1
2
3
4
5
@CacheResult
@HystrixCommand
public Book test6(Integer id,String aa) {
return restTemplate.getForObject("http://HELLO-SERVICE/getbook5/{1}", Book.class, id);
}

此时test6方法会自动开启缓存,默认所有的参数都将作为缓存的key,如果在某次调用中传入的两个参数和之前传入的两个参数都一致的话,则直接使用缓存,否则就发起请求,如下:

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping("/test6")
public Book test6() {
HystrixRequestContext.initializeContext();
//第一次发起请求
Book b1 = bookService.test6(2, "");
//参数和上次一致,使用缓存数据
Book b2 = bookService.test6(2, "");
//参数不一致,发起新请求
Book b3 = bookService.test6(2, "aa");
return b1;
}

当然这里我们也可以在@CacheResult中添加cacheKeyMethod属性来指定返回缓存key的方法,注意返回的key要是String类型的,如下:

1
2
3
4
5
6
7
8
@CacheResult(cacheKeyMethod = "getCacheKey2")
@HystrixCommand
public Book test6(Integer id) {
return restTemplate.getForObject("http://HELLO-SERVICE/getbook5/{1}", Book.class, id);
}
public String getCacheKey2(Integer id) {
return String.valueOf(id);
}

此时默认的规则失效。

@CacheKey

当然除了使用默认数据之外,我们也可以使用@CacheKey来指定缓存的key,如下:

1
2
3
4
5
@CacheResult
@HystrixCommand
public Book test6(@CacheKey Integer id,String aa) {
return restTemplate.getForObject("http://HELLO-SERVICE/getbook5/{1}", Book.class, id);
}

这里我们使用@CacheKey注解指明了缓存的key为id,和aa这个参数无关,此时只要id相同就认为是同一个请求,而aa参数的值则不会作为判断缓存的依据(这里只是举例子,实际开发中我们的调用条件可能都要作为key,否则可能会获取到错误的数据)。如果我们即使用了@CacheResult中cacheKeyMethod属性来指定key,又使用了@CacheKey注解来指定key,则后者失效。

@CacheRemove

这个当然是用来让缓存失效的注解,用法也很简单,如下:

1
2
3
4
5
@CacheRemove(commandKey = "test6")
@HystrixCommand
public Book test7(@CacheKey Integer id) {
return null;
}

注意这里必须指定commandKey,commandKey的值就为缓存的位置,配置了commandKey属性的值,Hystrix才能找到请求命令缓存的位置。举个简单的例子,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("/test6")
public Book test6() {
HystrixRequestContext.initializeContext();
//第一次发起请求
Book b1 = bookService.test6(2);
//清除缓存
bookService.test7(2);
//缓存被清除,重新发起请求
Book b2 = bookService.test6(2);
//参数一致,使用缓存数据
Book b3 = bookService.test6(2);
return b1;
}