# 094. 基于 request cache 请求缓存技术优化批量商品数据查询接口

我们上一讲讲解的那个图片,顺着那个图片的流程,来一个一个的讲解 hystrix 的核心技术

  1. 创建 command,2 种 command 类型
  2. 执行 command,4 种执行方式
  3. 查找是否开启了 request cache,是否有请求缓存,如果有缓存,直接取用缓存,返回结果

# 官方教学

官网文档 Request Cache (opens new window)

您可以通过在 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);
    }
}
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

由于这取决于 请求上下文,我们必须初始化 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();
    }
}
1
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();
    }
}
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

日志也只会有两条日志,两个上下文中各一条

关于缓存的清除

CommandUsingRequestCache command2a = new CommandUsingRequestCache(2);

assertTrue(command2a.execute());
// 第一次执行结果,所以不应该来自缓存
assertFalse(command2a.isResponseFromCache());
// commandKey 在声明 command 的时候我们可以自定义,所以很容易做成静态方法构建这个 key
HystrixRequestCache.getInstance(command2a.getCommandKey(),
        HystrixConcurrencyStrategyDefault.getInstance())
        .clear(command2a.getCacheKey());
1
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);
    }
    // 其他代码不就粘贴了
}
1
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;
}
1
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());
    }
}
1
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
1
2
3
4

对于课程中的背景举例,个人感觉不太妥:

在一次请求上下文中,我们会去执行 N 多代码,调用 N 多依赖服务,有的依赖服务可能还会调用好几次,这个时候就可以使用请求缓存来提高性能;

但是目前个人实际开发中,对于在一个请求中药调用多次的,也会手动缓存起来,应该很少用到这种请求缓存把?