# 094. 基于 request cache 请求缓存技术优化批量商品数据查询接口
我们上一讲讲解的那个图片,顺着那个图片的流程,来一个一个的讲解 hystrix 的核心技术
- 创建 command,2 种 command 类型
- 执行 command,4 种执行方式
- 查找是否开启了 request cache,是否有请求缓存,如果有缓存,直接取用缓存,返回结果
# 官方教学
您可以通过在 HystrixCommand 或 HystrixObservableCommand 对象上实现 getCacheKey()
方法来启用请求缓存,如下所示:
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
/**
*
* @author : zhuqiang
* @date : 2019/6/3 21:51
*/
public class CommandUsingRequestCache extends HystrixCommand<Boolean> {
private final int value;
protected CommandUsingRequestCache(int value) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.value = value;
}
@Override
protected Boolean run() {
// 当值为 0 或者是 2 的整倍数的时候,返回 true
System.out.println("run 方法被执行");
return value == 0 || value % 2 == 0;
}
@Override
protected String getCacheKey() {
return String.valueOf(value);
}
}
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
由于这取决于 请求上下文,我们必须初始化 HystrixRequestContext。在简单的单元测试中,您可以按如下方式执行此操作:
@Test
public void testWithoutCacheHits() {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
assertTrue(new CommandUsingRequestCache(2).execute());
assertTrue(new CommandUsingRequestCache(2).execute());
assertFalse(new CommandUsingRequestCache(1).execute());
assertTrue(new CommandUsingRequestCache(2).execute());
assertTrue(new CommandUsingRequestCache(0).execute());
assertTrue(new CommandUsingRequestCache(58672).execute());
} finally {
context.shutdown();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
测试结果只有 4 条 run 方法被执行
被打印,因为其中有 4 条是不相同的数字;
通常,此上下文将通过包装用户请求或其他生命周期挂钩的 ServletFilter 进行初始化和关闭。
以下示例显示 command 如何在请求上下文中从缓存中检索其值(以及如何查询对象以了解其值是否来自缓存):
@Test
public void testWithCacheHits() {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
CommandUsingRequestCache command2a = new CommandUsingRequestCache(2);
CommandUsingRequestCache command2b = new CommandUsingRequestCache(2);
assertTrue(command2a.execute());
// 第一次执行结果,所以不应该来自缓存
assertFalse(command2a.isResponseFromCache());
assertTrue(command2b.execute());
// 这是第二次执行结果,应该来自缓存
assertTrue(command2b.isResponseFromCache());
} finally {
// 关闭上下文
context.shutdown();
}
// 开始一个新的请求上下文
context = HystrixRequestContext.initializeContext();
try {
CommandUsingRequestCache command3b = new CommandUsingRequestCache(2);
assertTrue(command3b.execute());
// 当前的 command 是一个新的请求上下文
// 所以也不应该来自缓存
assertFalse(command3b.isResponseFromCache());
} finally {
context.shutdown();
}
}
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
日志也只会有两条日志,两个上下文中各一条
关于缓存的清除
CommandUsingRequestCache command2a = new CommandUsingRequestCache(2);
assertTrue(command2a.execute());
// 第一次执行结果,所以不应该来自缓存
assertFalse(command2a.isResponseFromCache());
// commandKey 在声明 command 的时候我们可以自定义,所以很容易做成静态方法构建这个 key
HystrixRequestCache.getInstance(command2a.getCommandKey(),
HystrixConcurrencyStrategyDefault.getInstance())
.clear(command2a.getCacheKey());
2
3
4
5
6
7
8
9
看代码相对来说比较麻烦,但是理解了一次请求上下文后,就应该明白,清除场景是很少见的;
看上下文和代码之间没有显式的进行关联,但是通过这句代码 HystrixRequestContext contextForCurrentThread = HystrixRequestContext.getContextForCurrentThread();
能联想到是使用了 ThreadLocal<HystrixRequestContext>
来保存上下文数据的。
这样一来在 ServletFilter 中初始化 HystrixRequestContext,就会让缓存生效,就清楚原理了
# 在业务背景下使用 hystrix 的 RequestCache
我们在批量获取商品接口中,使用单个获取的商品信息的 command 来测试缓存是否生效
public class GetProductCommand extends HystrixCommand<ProductInfo> {
private Long productId;
// 重写 getCacheKey 方法
@Override
protected String getCacheKey() {
return String.valueOf(productId);
}
// 其他代码不就粘贴了
}
2
3
4
5
6
7
8
9
10
在 filter 中初始化上下文
// 把 filter 按照 spring mvc 的方式注册
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean bean = new FilterRegistrationBean<>();
// 在 jdk8 中 Filter 接口 除了 javax.servlet.Filter.doFilter 方法外,其他两个方法都是默认方法了
// 所以这里使用了拉姆达表达式
bean.setFilter((request, response, chain) -> {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
chain.doFilter(request, response);
} finally {
context.shutdown();
}
});
bean.addUrlPatterns("/*");
return bean;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
调用处修改下,以便测试缓存是否生效
/**
* @param productIds 英文逗号分隔
*/
@RequestMapping("/getProducts")
public void getProduct(String productIds) {
List<Long> pids = Arrays.stream(productIds.split(",")).map(Long::parseLong).collect(Collectors.toList());
for (Long pid : pids) {
GetProductCommand getProductCommand = new GetProductCommand(pid);
getProductCommand.execute();
System.out.println("pid " + pid + ";是否来自缓存:" + getProductCommand.isResponseFromCache());
}
}
2
3
4
5
6
7
8
9
10
11
12
测试,访问 http://localhost:7001/getProducts?productIds=1,2,1,3
输出日志
pid 1;是否来自缓存:false
pid 2;是否来自缓存:false
pid 1;是否来自缓存:true
pid 3;是否来自缓存:false
2
3
4
对于课程中的背景举例,个人感觉不太妥:
在一次请求上下文中,我们会去执行 N 多代码,调用 N 多依赖服务,有的依赖服务可能还会调用好几次,这个时候就可以使用请求缓存来提高性能;
但是目前个人实际开发中,对于在一个请求中药调用多次的,也会手动缓存起来,应该很少用到这种请求缓存把?