应用可以通过分布式集群等方式动态扩容,但数据库不行,一个系统的吞吐量最终取决于数据库,而缓存可以在应用和数据库之间起到一层保护,为了竟可能多的提高缓存命中率,降低数据库压力,做到及时性和性能的平衡,根据不同缓存特点和应用场景采用多级缓存,具体如下:

  • 客户端缓存:本地存储,浏览器使用LocalStorage,APP可以使用SQLite,客户端和服务端采用HTTP头部信息如 Last-ModifiedIf-Modified-SinceEtagIf-None-Match进行沟通。
  • 服务端一级缓存:采用目前性能最好的缓存框架Caffeine(参考文章《你应该知道的缓存进化史》)
  • 服务端二级缓存:采用目前排名第一的分布式键值对缓存数据库Redis(参考月度排行网站DB-Engines.com的数据)

实践要点

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况: 1. 协商缓存生效,返回304和Not Modified 2. 协商缓存失效,返回200和请求结果

Last-Modified和If-Modified-Since

  1. 浏览器首次请求,服务端返回请求时在response header中设置 Last-Modified 资源上次更新时间,浏览器缓存下这个时间;
  2. 浏览器再次请求,request header中带上if-modified-since:[保存的last-modified的值],服务端根据浏览器发送的修改时间和服务端的修改时间进行比对,一致的话代表资源没有改变,服务端返回正文为空的304响应,让浏览器中缓存中读取资源。

ETag和If-None-Match

  1. 浏览器首次请求,服务端返回请求时在response header中设置 ETag ;
  2. 浏览器再次发送请求,在request header中带上 If-None-Match :[保存的etag的值],服务端将发送的etag的值和服务端重新生成的etag的值进行比对,如果一致代表资源没有改变,服务端返回正文为空的304响应,告诉浏览器从缓存中读取资源。

ETag和Last-Modified方案对比

由于Last-Modified依赖的是保存的绝对时间,而ETag常用的方法包括对资源内容使用抗碰撞散列函数,或者是资源的一个版本号,比Last-Modified更精确,但是ETag每次访问服务端生成都需要进行读写操作,而Last-Modified只需要读取操作。二者对比:

  1. 精确度: ETag > Last-Modified
  2. 性能: ETag < Last-Modified

综上: 服务端校验优先级:ETag > Last-Modified

浏览器行为

  1. 打开网页(地址栏输入网址后回车): 查找磁盘缓存,如有,则状态为:200,类型为:from disk cache,不发送网络请求。
  2. 普通刷新(F5): 查找内存缓存,如有,则状态为:200,类型为:form memory cache, 不发送网络请求。
  3. 强制刷新(Ctrl+F5):强制请求网络(发送的请求头部均带有 Cache-control:no-cachePragma:no-cache ),服务端返回200和最新内容。

为了防止服务端缓存更新了,但是浏览器不请求的情况发生,一般当服务端强求变化以后对应的请求入参应该也要变化,也即系统的前一个(或者前前一个)请求必须是不缓存,依次传递,确保浏览器不会使用from disk cache和form memory cache。

缓存穿透

缓存穿透是指查询的数据在数据库是没有的,那么在缓存中自然也没有,导致这部分请求必然直达数据库。 措施: 1. 区分缓存不存在和数据不存在,比如约定取缓存值为null代表数据不存在,缓存key不存在代表缓存不存在。 2. 校验缓存key的范围,对于明显不合规的直接阻止其进入缓存系统。

缓存击穿

缓存击穿是指由于某个热点缓存失效,瞬间就有大量的请求没命中,然后直达数据库。

措施: - 对于热点数据采用定时自动刷新策略。

缓存雪崩

缓存雪崩是指缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,导致大量请求直达数据库。

措施: - 监控缓存健康状况 - 采用多级缓存 - 缓存的过期时间尽量随机,定时刷新时间也尽量错开

缓存污染

由于错误的使用缓存数据进行修改,并未更新到数据库,从而导致各实例的缓存数据混乱,并且和数据库不一致的情况

措施: - 规范和代码审查

缓存序列化

当缓存的是对象时,由于序列号和反序列化的机制不同,导致缓存反序列化报错。

措施: - 采用兼容性较好的反序列化框架,如json - 对缓存对象修改需要慎重,新增字段放在最后,删除字典建议兼容和过渡,比如用@Deprecated注解进行标注弃用,待所有缓存更新后再手动删除 - 必要的时候采用双写策略,即同时上线新老两个版本的缓存,更新时两个缓存同时更新,读采用老缓存,待新缓存全部覆盖老缓存时,将读切换至新缓存,同时停止双写。