详解分布式系统的缓存设计

作者:微信小助手

发布时间:2022-06-09T15:28:39

作者:vivo互联网服务器团队-Zhang Peng


一、缓存简介


1.1 什么是缓存


缓存就是数据交换的缓冲区。缓存的本质是一个内存 Hash。缓存是一种利用空间换时间的设计,其目标就是更快、更近:极大的提高。


  • 将数据写入/读取速度更快的存储(设备);

  • 将数据缓存到离应用最近的位置;

  • 将数据缓存到离用户最近的位置。


缓存是用于存储数据的硬件或软件的组成部分,以使得后续更快访问相应的数据。缓存中的数据可能是提前计算好的结果、数据的副本等。典型的应用场景:有 cpu cache, 磁盘 cache 等。本文中提及到缓存主要是指互联网应用中所使用的缓存组件。


缓存命中率是缓存的重要度量指标,命中率越高越好。

缓存命中率 = 从缓存中读取次数 / 总读取次数


1.2 何时需要缓存


引入缓存,会增加系统的复杂度。所以,引入缓存前,需要先权衡是否值得,考量点如下:


  • CPU 开销 - 如果应用某个计算需要消耗大量 CPU,可以考虑缓存其计算结果。典型场景:复杂的、频繁调用的正则计算;分布式计算中间状态等。

  • IO 开销 - 如果数据库连接池比较繁忙,可以考虑缓存其查询结果。


在数据层引入缓存,有以下几个好处:


  • 提升数据读取速度。

  • 提升系统扩展能力,通过扩展缓存,提升系统承载能力。

  • 降低存储成本,Cache+DB 的方式可以承担原有需要多台 DB 才能承担的请求量,节省机器成本。


1.3 缓存的基本原理


根据业务场景,通常缓存有以下几种使用方式:


  • 懒汉式(读时触发):先查询 DB 里的数据, 然后把相关的数据写入 Cache。

  • 饥饿式(写时触发):写入 DB 后, 然后把相关的数据也写入 Cache。

  • 定期刷新:适合周期性的跑数据的任务,或者列表型的数据,而且不要求绝对实时性。


1.4 缓存淘汰策略


缓存淘汰的类型:


1)基于空间:设置缓存空间大小。

2)基于容量:设置缓存存储记录数。

3)基于时间

  • TTL(Time To Live,即存活期)缓存数据从创建到过期的时间。

  • TTI(Time To Idle,即空闲期)缓存数据多久没被访问的时间。


缓存淘汰算法:


1)FIFO:先进先出。在这种淘汰算法中,先进入缓存的会先被淘汰。这种可谓是最简单的了,但是会导致我们命中率很低。试想一下我们如果有个访问频率很高的数据是所有数据第一个访问的,而那些不是很高的是后面再访问的,那这样就会把我们的首个数据但是他的访问频率很高给挤出。


2)LRU:最近最少使用算法。在这种算法中避免了上面的问题,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。但是这个依然有个问题,如果有个数据在 1 个小时的前 59 分钟访问了 1 万次(可见这是个热点数据),再后一分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。


3)LFU:最近最少频率使用。在这种算法中又对上面进行了优化,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了 LRU 不能处理时间段的问题。


这三种缓存淘汰算法,实现复杂度一个比一个高,同样的命中率也是一个比一个好。而我们一般来说选择的方案居中即可,即实现成本不是太高,而命中率也还行的 LRU。


二、缓存的分类


缓存从部署角度,可以分为客户端缓存和服务端缓存。


客户端缓存


  • HTTP 缓存

  • 浏览器缓存

  • APP 缓存(1、Android  2、IOS)


服务端缓存


  • CDN 缓存:存放 HTML、CSS、JS 等静态资源。

  • 反向代理缓存:动静分离,只缓存用户请求的静态资源。

  • 数据库缓存:数据库(如 MySQL)自身一般也有缓存,但因为命中率和更新频率问题,不推荐使用。

  • 进程内缓存:缓存应用字典等常用数据。

  • 分布式缓存:缓存数据库中的热点数据。


其中,CDN 缓存、反向代理缓存、数据库缓存一般由专职人员维护(运维、DBA)。后端开发一般聚焦于进程内缓存、分布式缓存。


2.1 HTTP 缓存


2.2 CDN 缓存


CDN 将数据缓存到离用户物理距离最近的服务器,使得用户可以就近获取请求内容。CDN 一般缓存静态资源文件(页面,脚本,图片,视频,文件等)。


国内网络异常复杂,跨运营商的网络访问会很慢。为了解决跨运营商或各地用户访问问题,可以在重要的城市,部署 CDN 应用。使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。



图片引用自:Why use a CDN


2.1.1 CDN 原理


CDN 的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。


1)未部署 CDN 应用前的网络路径:


  • 请求:本机网络(局域网)=> 运营商网络 => 应用服务器机房

  • 响应:应用服务器机房 => 运营商网络 => 本机网络(局域网)


在不考虑复杂网络的情况下,从请求到响应需要经过 3 个节点,6 个步骤完成一次用户访问操作。


2)部署 CDN 应用后网络路径:


  • 请求:本机网络(局域网) => 运营商网络

  • 响应:运营商网络 => 本机网络(局域网)


在不考虑复杂网络的情况下,从请求到响应需要经过 2 个节点,2 个步骤完成一次用户访问操作。与不部署 CDN 服务相比,减少了 1 个节点,4 个步骤的访问。极大的提高了系统的响应速度。


2.1.2 CDN 特点


优点


  • 本地 Cache 加速:提升访问速度,尤其含有大量图片和静态页面站点;

  • 实现跨运营商的网络加速:消除了不同运营商之间互联的瓶颈造成的影响,实现了跨运营商的网络加速,保证不同网络中的用户都能得到良好的访问质量;

  • 远程加速:远程访问用户根据 DNS 负载均衡技术智能自动选择 Cache 服务器,选择最快的 Cache 服务器,加快远程访问的速度;

  • 带宽优化:自动生成服务器的远程 Mirror(镜像)cache 服务器,远程用户访问时从 cache 服务器上读取数据,减少远程访问的带宽、分担网络流量、减轻原站点 WEB 服务器负载等功能。

  • 集群抗攻击:广泛分布的 CDN 节点加上节点之间的智能冗余机制,可以有效地预防黑客入侵以及降低各种 D.D.o.S 攻击对网站的影响,同时保证较好的服务质量。


缺点


  • 不适宜缓存动态资源

解决方案:主要缓存静态资源,动态资源建立多级缓存或准实时同步;


  • 存在数据的一致性问题

1.解决方案(主要是在性能和数据一致性二者间寻找一个平衡)。

2.设置缓存失效时间(1 个小时,过期后同步数据)。

3.针对资源设置版本号。

​​


2.2 反向代理缓存


反向代理(Reverse Proxy)方式是指以代理服务器来接受 internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。



2.2.1 反向代理缓存原理


反向代理位于应用服务器同一网络,处理所有对 WEB 服务器的请求。反向代理缓存的原理:


  • 如果用户请求的页面在代理服务器上有缓存的话,代理服务器直接将缓存内容发送给用户。

  • 如果没有缓存则先向 WEB 服务器发出请求,取回数据,本地缓存后再发送给用户。


这种方式通过降低向 WEB 服务器的请求数,从而降低了 WEB 服务器的负载。


反向代理缓存一般针对的是静态资源,而将动态资源请求转发到应用服务器处理。常用的缓存应用服务器有 Varnish,Ngnix,Squid。


2.2.2 反向代理缓存比较


常用的代理缓存有 Varnish,Squid,Ngnix,简单比较如下:


  • Varnish 和 Squid 是专业的 cache 服务,Ngnix 需要第三方模块支持;

  • Varnish 采用内存型缓存,避免了频繁在内存、磁盘中交换文件,性能比 Squid 高;

  • Varnish 由于是内存 cache,所以对小文件如 css、js、小图片的支持很棒,后端的持久化缓存可以采用的是 Squid 或 ATS;

  • Squid 功能全而大,适合于各种静态的文件缓存,一般会在前端挂一个 HAProxy 或 Ngnix 做负载均衡跑多个实例;

  • Nginx 采用第三方模块 ncache 做的缓冲,性能基本达到 Varnish,一般作为反向代理使用,可以实现简单的缓存。


三、进程内缓存


进程内缓存是指应用内部的缓存,标准的分布式系统,一般有多级缓存构成。本地缓存是离应用最近的缓存,一般可以将数据缓存到硬盘或内存。


  • 硬盘缓存:将数据缓存到硬盘中,读取时从硬盘读取。原理是直接读取本机文件,减少了网络传输消耗,比通过网络读取数据库速度更快。可以应用在对速度要求不是很高,但需要大量缓存存储的场景。

  • 内存缓存:直接将数据存储到本机内存中,通过程序直接维护缓存对象,是访问速度最快的方式。


常见的本地缓存实现方案:HashMap、Guava Cache、Caffeine、Ehcache。


3.1 ConcurrentHashMap


最简单的进程内缓存可以通过 JDK 自带的 HashMap 或 ConcurrentHashMap 实现。


  • 适用场景:不需要淘汰的缓存数据。

  • 缺点:无法进行缓存淘汰,内存会无限制的增长。


3.2 LRUHashMap


可以通过继承 LinkedHashMap 来实现一个简单的 LRUHashMap。重写 removeEldestEntry 方法,即可完成一个简单的最近最少使用算法。


缺点:

  • 锁竞争严重,性能比较低。

  • 不支持过期时间。

  • 不支持自动刷新。


3.3  Guava Cache


解决了LRUHashMap 中的几个缺点。Guava Cache 采用了类似 ConcurrentHashMap 的思想,分段加锁,减少锁竞争。


Guava Cache 对于过期的 Entry 并没有马上过期(也就是并没有后台线程一直在扫),而是通过进行读写操作的时候进行过期处理,这样做的好处是避免后台线程扫描的时候进行全局加锁。直接通过查询,判断其是否满足刷新条件,进行刷新。


3.4  Caffeine


Caffeine 实现了 W-TinyLFU(LFU + LRU 算法的变种),其命中率和读写吞吐量大大优于 Guava Cache。其实现原理较复杂,可以参考你应该知道的缓存进化史。


3.5 Ehcache


EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。


优点

  • 快速、简单;

  • 支持多种缓存策略:LRU、LFU、FIFO 淘汰算法;

  • 缓存数据有两级:内存和磁盘,因此无需担心容量问题;

  • 缓存数据会在虚拟机重启的过程中写入磁盘;

  • 可以通过 RMI、可插入 API 等方式进行分布式缓存;

  • 具有缓存和缓存管理器的侦听接口;

  • 支持多缓存管理器实例,以及一个实例的多个缓存区域;

  • 提供 Hibernate 的缓存实现。


缺点

  • 使用磁盘 Cache 的时候非常占用磁盘空间;

  • 不保证数据的安全;

  • 虽然支持分布式缓存,但效率不高(通过组播方式,在不同节点之间同步数据)。


3.6 进程内缓存对比


常用进程内缓存技术对比:



  • ConcurrentHashMap:比较适合缓存比较固定不变的元素,且缓存的数量较小的。虽然从上面表格中比起来有点逊色,但是其由于是 JDK 自带的类,在各种框架中依然有大量的使用,比如我们可以用来缓存我们反射的 Method,Field 等等;也可以缓存一些链接,防止其重复建立。在 Caffeine 中也是使用的 ConcurrentHashMap 来存储元素。


  • LRUMap:如果不想引入第三方包,又想使用淘汰算法淘汰数据,可以使用这个。


  • Ehcache:由于其 jar 包很大,较重量级。对于需要持久化和集群的一些功能的,可以选择 Ehcache。需要注意的是,虽然 Ehcache 也支持分布式缓存,但是由于其节点间通信方式为 rmi,表现不如 Redis,所以一般不建议用它来作为分布式缓存。


  • Guava Cache:Guava 这个 jar 包在很多 Java 应用程序中都有大量的引入,所以很多时候其实是直接用就好了,并且其本身是轻量级的而且功能较为丰富,在不了解 Caffeine 的情况下可以选择 Guava Cache。


  • Caffeine:其在命中率,读写性能上都比 Guava Cache 好很多,并且其 API 和 Guava cache 基本一致,甚至会多一点。在真实环境中使用 Caffeine,取得过不错的效果。


总结一下:如果不需要淘汰算法则选择 ConcurrentHashMap,如果需要淘汰算法和一些丰富的 API,推荐选择。


四、分布式缓存


分布式缓存解决了进程内缓存最大的问题:如果应用是分布式系统,节点之间无法共享彼此的进程内缓存。分布式缓存的应用场景:


  • 缓存经过复杂计算得到的数据。

  • 缓存系统中频繁访问的热点数据,减轻数据库压力。


不同分布式缓存的实现原理往往有比较大的差异。本文主要针对 Memcached 和 Redis 进行说明。


4.1 Memcached