最近考虑在 App 中加入缓存机制,主要是出于两点考虑:

  1. 优化体验。App 启动的时候可以使用缓存数据去填充页面,一进来就出现空白的加载页面。
  2. 为用户在断网情况下继续提供数据浏览。

综合考虑后,感觉使用 Http 提供的缓存机制比较合适。主要是:

  1. 灵活。客户端和服务器都可以对缓存进行控制。
  2. 集成简单。由于 App 使用了 OkHttp,本身就支持 Http 的缓存机制,而且由于是在 Http 层对缓存进行支持,客户端不用添加其他的判断逻辑对是否缓存进行判断。
  3. 跨平台。由于是在 Http 上进行支持的,所以如果需要, iOS 端也可以做相应的支持。

缺点的话,由于 Http 的缓存是基于文件系统的,也就是 key-value 模式,缓存的粒度比较大,不能实现一些元数据的公用。不过对目前的 App 来说够用了。下面都是对 Http 的缓存机制相关调研。

缓存流程

CacheFlow

Cache-Control 相关头域

Cache-Control 头域分两种,一种用于 Request 的,一种是用于 Response 的,由于 Request 和 Response 都可以有 Cache-Control 头域,所以 Client 和 Server 都可以对缓存机制进行对应的控制。比如当 C 端要强制更新的时候,可以发送带 Cache control: no-store 的 Request。

Request Cache-Control

  • "no-cache"
  • "no-store"
  • "max-age" "=" delta-seconds
  • "max-stale" "=" delta-seconds
  • "min-fresh" "=" delta-seconds
  • "no-transform"
  • "only-if-cached"

Response Cache-Control

  • "public"
  • "private"
  • "no-cache"
  • "no-store"
  • "no-transform"
  • "must-revalidate"
  • "proxy-revalidate"
  • "max-age" "=" delta-seconds
  • "s-maxage" "=" delta-seconds

不同头域的意义

  • Cache-Control:public :该 response 为公有缓存
  • Cache-Control:private :该 response 为私有缓存
  • Cache-Control:no-cache :可以缓存 response,但是必须要经过新鲜度再验证响应后才可以返回缓存。
  • Cache-Control:no-store :对应的 resquest/response 不允许存储,这里的不允许存储即:不允许进行任何持久化存储,如果有存在非持久化存储,也要尽快消除。
  • Cache-Control:s-maxage : 意思和 max-age 类似,但是只用于公有缓存,在共有缓存中使用的时候会覆盖 max-age 的值
  • Cache-Control:max-age = s :指定相对过期日期,单位为秒
  • Expires :指定绝对过期日期
  • Cache-Control:max-stale = s :过期后的 s 秒内缓存可以继续使用
  • Cache-Control:min-fresh = s :至少在 s 秒内缓存要保持新鲜
  • Cache-Control:must-revalidate : 强制缓存重新进行新鲜度验证,因为 http 头中存在一些头域会改变缓存原本的失效时间(比如 request 中的 max-stale),而通过这个头域可以强制缓存进行更新,如果在进行新鲜度验证的时候不能连接到服务器,则返回 504 Gateway Timeout 错误。
  • Cache-Control:no-transform : 部分网络代理为了提高性能会对图片等文档进行转换处理(比如压缩),no-transform 可以强制要求网络代理不要对资源进行转换。
  • Cache-Control:only-if-cached : 该头域表示不进行与网络相关的交互,只返回已经缓存且满足要求的数据,否则的话返回 504 错误。
  • Cache-Control:proxy-revalidate : 和 must-revalidate 类似,只是特定用于公共缓存的。

PS:

  1. max-age 的优先级比 Expires 高。 See:参考
  2. max-age,max-stale 和 min-fresh 的关系:缓存使用期 age 表示缓存自 Server 将其发出(或最后一次被服务器再验证)之后过去的时间,则如果 age + min-fresh < max-age,缓存未过期;如果 age + min-fresh >= max-age && age + min-fresh < max-age + max-stale,则虽然缓存过期了,但是缓存继续可以使用,只是在头部添加 110 警告码;如果 age + min-fresh >= max-age + max-stale,则缓存过期且不可使用。缓存使用期 age 的计算可以参考《Http 权威指南》的 7.11.2 节,具体关于缓存使用期和新鲜生存期的问题可以参考 7.11 节。(参考1,参考2
  3. no-cache 和 max-age = 0 的区别:参考
  4. no-cache 和 no-store 区别:no-cache 实际会进行缓存,只是在缓存响应之前会先进行一次新鲜度再验证(浏览器的 f5 刷新);而 no-store 不会进行缓存,每次都是向获取服务器的最新数据(浏览器的强制刷新)。

新鲜度再验证的实现

有两种实现方式最近修改日期(If-Modified-Since)和实体标签(ETag)验证。 相关首部:

  • If-Modified-Since:
  • If-Unmodified-Sinece:
  • If-None-Match:
  • If-Range:<data|tags>
  • If-Match:

最近修改日期(If-Modified-Since)验证:

一般通过 If-Modified-Since 配和 Last-Modified 来实现验证,Last-Modified >= If-Modified-Since 则新鲜度过期。也可配合 If-Unmodified-Sinece。

实体标签(ETag)验证:

通过特定标签对比进行验证,比如 ETag 可以用来存版本号,这样就可以通过版本号对比查看新鲜度,可以配合 If-None-Match 来使用。

主要使用前两个 If-Modified-Since 和 If-None-Match 头域,详细参考《Http 权威指南》的 7.8.5,7.8.6 节

试探性过期

当 response 中没有 Cache-Control:max-age 和 Expires 的时候,缓存可以计算出一个试探性最大使用期。常用算法 LM—Factor 算法。

参考