HttpClient API 文档:6. HTTP 缓存

6.1. 一般概念

HttpClient Cache 提供了用 HttpClient(等效浏览器缓存的 Java 实现)来兼容 HTTP / 1.1 的缓存层。该实现遵循责任链模式。HttpClient 缓存的实现类可以替代默认无缓存的 HttpClient;完全可以通过缓存实现的请求将不会触发实际的原始请求。在使用条件 GETs 和 If-Modified-Since 和/或 If-None-Match 请求头的情况下,尽可能使用源自动验证过时的缓存条目。

通常,HTTP / 1.1 缓存被设计为在语义上透明;也就是说,缓存不应该更改客户端和服务器之间的请求-响应交换的含义。因此,将缓存的 HttpClient 放到现有的兼容客户端-服务器关系中应该是安全的。尽管从 HTTP 协议的角度来看,缓存模块是客户端的一部分,但是该实现旨在与透明缓存代理上的要求兼容。

最后,HttpClient 缓存实现类包括对 RFC 5861 指定的 Cache-Control 扩展的支持(过时错误和过时重新验证)。

当缓存 HttpClient 执行一个请求时,它会执行以下流程:

  1. 检查要求是否符合基本的 HTTP 1.1 协议并尝试正确的请求。
  2. 刷新当前请求导致无效的一些缓存项。
  3. 确定当前请求可以从缓存中提供。如果不能, 则直接将这个请求发送给原始服务器并换回响应,如果合适的话保存缓存.
  4. 如果是 cache-servable 请求时,将尝试从缓存中读取。当缓存中没有的情况下,调用原始的服务器,如果可以的话将响应缓存。
  5. 如果缓存中的响应能够被当做一个响应提供服务,构造一个包含 ByteArrayEntity 的 BasicHttpResponse 并返回。否则,尝试重新验证对原始服务器的缓存项。
  6. 当一个缓存的响应不能重新生效的情况下,调用原始服务器,如果可以的话缓存响应。

当缓存 HttpClient 收到响应时,它会执行以下流程:

  1. 根据协议内容,检查响应。
  2. 确定响应是否可以被缓存。
  3. 如果可以被缓存,读取配置中允许的最多内容,并将其保存在缓存中。
  4. 如果对缓存来说响应过大,则重新构建被消费的部分响应,并跳过缓存流程直接返回。

需要注意的是,HttpClient Cache 本身并不是一个 HttpClient 的不同实现,而是通过插件的方式作为一个额外管道执行请求。

6.2. RFC-2616 规范

我们相信 HttpClient 缓存是无条件地符合 RFC-2616。就是说,无论规范在什么地方指示对于 HTTP 缓存都是“必须”,“不得”,“应该”或“不应”,缓存层都将尝试以满足那些要求的方式运行。这意味着当您放入缓存模块时,它不会产生不正确的行为。

6.3. 使用样例

下面是一个如何建立一个基本的缓存 HttpClient 例子。根据配置,他会存储一个最大数量为 1000 的缓存实例,每个最多能够保存 8192 bytes。这里的数字仅仅是个例子,并不是规定,也不能被当做建议。

CacheConfig cacheConfig = CacheConfig.custom()
        .setMaxCacheEntries(1000)
        .setMaxObjectSize(8192)
        .build();
RequestConfig requestConfig = RequestConfig.custom()
        .setConnectTimeout(30000)
        .setSocketTimeout(30000)
        .build();
CloseableHttpClient cachingClient = CachingHttpClients.custom()
        .setCacheConfig(cacheConfig)
        .setDefaultRequestConfig(requestConfig)
        .build();

HttpCacheContext context = HttpCacheContext.create();
HttpGet httpget = new HttpGet("http://www.mydomain.com/content/");
CloseableHttpResponse response = cachingClient.execute(httpget, context);
try {
    CacheResponseStatus responseStatus = context.getCacheResponseStatus();
    switch (responseStatus) {
        case CACHE_HIT:
            System.out.println("A response was generated from the cache with " +
                    "no requests sent upstream");
            break;
        case CACHE_MODULE_RESPONSE:
            System.out.println("The response was generated directly by the " +
                    "caching module");
            break;
        case CACHE_MISS:
            System.out.println("The response came from an upstream server");
            break;
        case VALIDATED:
            System.out.println("The response was generated from the cache " +
                    "after validating the entry with the origin server");
            break;
    }
} finally {
    response.close();
}

6.4. 配置

HttpClient Cache 继承了所有非缓存实现的配置项和参数(包括像超时时间、连接池大小这样的配置选项)。对于特殊的缓存配置,可以通过定义 CacheConfig 实例来自定义行为,它包含以下的配置:

  • Cache size(缓存大小)。后端存储支持这些限制,可以像最大的可缓存响应体大小一样指定缓存项的数量。

  • Public/private caching(公共/私有缓存)。 在默认情况下,缓存模块本身是一个共享缓存,例如,不会缓存带有授权请求头的响应和标记为 Cache-Control: private 的响应。然而,如果缓存仅仅被应用于一个“用户”(类似于浏览器缓存的行为)逻辑,那么它将关闭共享缓存设置。

  • Heuristic caching(启发式缓存)。按照 RFC2616,,缓存可以缓存特定的缓存条目,即使没有显示原始设置的缓存控制头。这种行为在默认情况下是关闭的,但是如果你处理的是一个没有完全设置原始请求头的请求,你可能想要打开它。你需要一个可以启发式的缓存,然后指定一个默认的新生命周期,并且/或者资源被最后修改后的一小段时间。参考 HTTP/1.1 RFC 的 13.2.2 和 13.2.4 部分,可以获得启发式缓存的更多细节。

  • Background validation(后台验证)。 缓存模块支持 RFC5861 的 stale-while-revalidate 指令,允许后台特定的缓存项重新生效。可以需要调整设置后台工作线程的最小和最大工作数量,以及他们回收前闲置的最大时间。没有足够的 worker 来跟上需求时,您还可以控制队列的大小用于重新生效线程。

6.5. 后端存储

默认的 HttpClient Cache 的实现会存储缓存项,并且在应用的 JVM 内存中缓存响应体。虽然这些能够提供高性能,但是有内存大小的限制,或者因为缓存项在应用重启后会失效,所以可能并不适用于您的应用。当前版本允许将缓存项保存到硬盘上,或者使用其他外部进程 EhCache 和 Memcached 存储缓存项。

如果这些选项都满足不了你的应用,可以通过实现 HttpCachedStorage 提供你自己的后台存储,并且在构建的时候提供给缓存 HttpClient。在这种情况下,缓存项会通过你自己的框架存储,但是会重用所有 HTTP/1.1 协议的逻辑和缓存处理。一般来说,应该能够创建一个支持原子操作的 key/value 存储(类似于 java 的 Map 接口)的 HttpCachedStorage 实现。

最后,通过一些额外的努力完全有可能建立一个多层缓存层次结构;比如,包装一个内存中的缓存 HttpClient 在磁盘上的存储缓存条目,在 memcached 中远程存储,后一种模式类似于虚拟内存,L1 / L2 处理器缓存等。

推荐阅读更多精彩内容