大家好, 我是「老黑」。
缓存,已经是一个老生常谈的技术了,在高并发读的情况下对于读服务来说可谓是抗流量的银弹。
高并发三大利器:缓存、限流、降级。
今天我们就来谈谈缓存。「对于缓存,我的理解是让数据更接近于用户,目的是让用户的访问速度更快。」 所以距离越接近用户的缓存,越快越有效!缓存的工作原理是先从缓存中获取数据,如果有数据则直接返回给用户,如果没有数据则从慢速设备上读取实际数据并且将数据放入缓存。
按照层级关系,我们来划分一下缓存,同时也是我们今天的「大纲」:
浏览器缓存
浏览器是我们网上冲浪的重要工具,为了能够让我们顺畅的冲浪,它也会帮助我们缓存一些东西,主要存放一些实时性不太敏感的数据,比如商品详情页框架、商家评分、评价、广告词等。对于实时性要求高的数据则不能使用浏览器缓存。浏览器缓存是有过期时间的,我们可以通过对响应头Expires、Cache-control进行控制。
客户端缓存
客户端缓存很容易理解,意思就是存放在客户端的缓存。它的使用场景不多,在我们大促的时候,为了防止瞬间流量把服务端击垮,一般会在大促来临之前把app需要访问的一些素材(如js/css/image等)提前下发到客户端进行缓存,在大促来临之际app就不需要去拉取这些素材了。另外的话还有一些兜底数据或者样式文件也会存放于客户端缓存中,在服务端异常或者网络异常的时候保证app不崩。
CDN缓存
CDN(Content Delivery Network),即内容分发网络。它是建立并覆盖在承载网之上,由分布在不同区域的边缘节点服务器群组成的分布式网络。我们通常会将一些静态页面数据、活动页面、图片等数据存放于CDN缓存中。
CDN缓存有两种机制:推送机制(当内容变更后主动将数据推送到CDN节点)和拉取机制(先访问CDN节点,无数据的时候会从源服务器获取数据返回并存储CDN节点)。
举个例子,如果你要去买汽车,你应该是到4s店去买汽车,如果4s店有你可以直接提走,如果4s店没有,那么4s店铺需要去进一批货,然后回到店铺,然后再给你。在这个case中,4s店其实就承当了一个CDN缓存节点的角色。
反向代理缓存
反向代理,我们一般情况都是指反向代理服务器Nginx。
Nginx缓存主要分为Nginx Http缓存与Nginx代理层缓存。
Nginx Http缓存提供expires、etag、if-modified-since指令来实现反向代理缓存。Nginx代理层缓存主要以Http模块与proxy_cacahe模块进行配置即可。
本地缓存
本地缓存,一般是指将客户机本地的物理内存划分出一部分空间用来缓冲客户机回写到服务器的数据。从全局的角度,我们可以有「磁盘缓存」、「CPU缓存」、「应用缓存」。
「磁盘缓存」分为读缓存和写缓存。
读缓存是指,操作系统为已读取的文件数据,在内存较空闲的情况下留在内存空间中(这个内存空间被称之为“内存池”),当下次软件或用户再次读取同一文件时就不必重新从磁盘上读取,从而提高速度。
写缓存实际上就是将要写入磁盘的数据先保存于系统为写缓存分配的内存空间中,当保存到内存池中的数据达到一个程度时,便将数据保存到硬盘中。
「CPU缓存」可以分为一级缓存(L1 Cache)、二级三级缓存(L2/L3)。当CPU要读取一个数据时,首先从L1中查找,没有的话再从L2/L3中查找,如果还没有那就从内存中查找,内存如果还没有那就从磁盘查找。查找顺序为:CPU->L1->L2/L3->内存->磁盘。
「应用缓存」分为本地应用缓存与其他应用缓存。
本地应用缓存指的是本服务所使用的缓存,用Java服务来举例,又分为 堆内缓存 与 堆外缓存 。
堆内缓存,一般指的是Java堆的缓存对象,堆内缓存的好处是不需要序列化/反序列化,也是最快的缓存,缺点也很明显,缓存数据多的时候,GC(垃圾回收)的频率会增大,时间会加长。堆内缓存一般使用软引用/弱引用来引用对象,使用这两种引用的好处是当堆内存不足时,可以强制回收这部分内存,释放堆空间。堆内缓存最大的问题是重启时内存中的缓存数据会丢失,如果堆内缓存使用的多,再加上刚好流量风暴,有可能击垮应用。堆内缓存的实现一般有:Guava Cache、Ehcache等。
堆外缓存,这个听说的同学比较少,它处于Java堆之外的内存,不受GC控制,也不受限堆大小,只受限于机器内存,所以,使用它一定小心谨慎,如果处理不当它可能存在内存泄漏的风险!堆外内存需要序列化/反序列化,所以它会比堆内缓存慢一些。
其他应用缓存,指的是除了本服务之外的缓存,比如local redis cache。local redis cache指的是在本服务器上部署一组Redis,应用直接读本机获取缓存数据,多机之间利用主从机制同步数据。这种方式的优点是没有网络消耗,性能是最优的。
分布式缓存
如果数据量不大的情况下,使用local redis cache的架构是最优的。
使用local redis cache最大的问题是:
- 单机器容量问题
- 多实例数据一致性问题
- 多实例缓存命中率降低导致回源DB
如果遇到这样的问题,那么应该将数据分片,尽可能的均匀分布到多台服务器,这便是分布式缓存。
分布式缓存常见的分片策略有:
- 节点取余
- 一致性哈希
- 虚拟槽分区
我们最常见的Redis-Cluster集群则是使用虚拟槽分区的方式来对数据分片的。
我们点到即止,对于Redis缓存相关,后面会有很多文章来专门讨论,敬请期待吧!
其他:缓存命中率
缓存命中率是我们非常重要的一个指标,我们如果使用缓存,一定需要通过监控这个指标来看缓存的工作状态。
它的计算方式为:
命中率缓存命中次数读取总次数缓存命中率越高越好,如何提高缓存命中率呢?我们应该对于不同场景数据有不同的缓存策略,比如:
- 大促来临之际应该提前将热点数据缓存,这种方式我们称之为缓存预热或缓存热加载;
- 在case1的基础上,将热点缓存数据与普通缓存数据做数据隔离,这一点前期需要人为干预,后期需要实时热点发现;
- 将数据分类,不同类别的数据配置合适的失效时间;
- 调整缓存粒度,通常情况下缓存粒度越小缓存命中率越高;
- 增大存储容量,当容量不够的时候会触发过期策略导致部分缓存数据失效,从而影响缓存命中率;
缓存问题:缓存击穿
[一句话概述]缓存击穿是指数据库和缓存都没有的数据,每次都要经过缓存去访问数据库,大量的请求有可能导致DB宕机。(强调都没有数据+并发访问)
这里我继续点到即止,后续奉上,敬请期待。
缓存问题:缓存穿透
[一句话概述]缓存击穿是指数据库有,缓存没有的数据,大量请求访问这个缓存不存在的数据,最后请求打到DB可能导致DB宕机。(强调单个Key过期+并发访问)
这里我继续点到即止,后续奉上,敬请期待。
缓存问题:缓存雪崩
[一句话概述]缓存击穿是指数据库有,缓存没有的数据,大量请求访问这些缓存不存在的数据,最后请求打到DB可能导致DB宕机。(强调批量Key过期+并发访问)
这里我继续点到即止,后续奉上,敬请期待。
缓存问题:缓存一致性
[一句话概述]缓存一致性指的是缓存与DB之间的数据一致性,我们需要通过各种手段来防止缓存与DB不一致,我们要保证缓存与DB的数据一致或者数据最终一致。
这里我继续点到即止,后续奉上,敬请期待。
缓存的其他问题
缓存的好处我们非常受益,用户的每一次请求都伴随着无数缓存的诞生,但是缓存同时也给我们带来了不小的挑战,比如在上面提到的一些疑难课题:缓存穿透、缓存击穿、缓存雪崩和缓存一致性。
除此之外,我们还会涉及到其他的一些缓存难题,如:缓存倾斜、缓存阻塞、缓存慢查询、缓存主从一致性问题、缓存高可用、缓存故障发现与故障恢复、集群扩容收缩、大Key热Key......
我们今天只做一个缓存的开篇,具体的细节,留给我们后续的章节中吧。