高并发系统之缓存设计

一,概述

1.什么是缓存

缓存,是一种存储数据的组件,它的作用是让对数据的请求更快地返回。

缓存不仅可以放在内存中来存储,也可以放在 SSD中。

实际上,凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存。

缓存分类

常见的缓存主要就是静态缓存、分布式缓存和热点本地缓存这三种。

a.静态缓存

在 Nginx 上部署静态资源的缓存可以减少对于后台应用服务器的压力。例如,有大量文章内容访问的网站,可以把文章渲染成页面缓存起来,而不是访问数据库再渲染。

b.分布式缓存

如何针对动态请求做缓存呢?这就需要分布式缓存了。

Memcached、Redis 就是分布式缓存的典型例子。它们性能强劲,通过一些分布式的方案组成集群可以突破单机的限制。

c.热点本地缓存

当遇到极端的热点数据查询的时候。热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存节点或者数据库的压力。

极端热点数据的查询通常会命中某一个缓存节点或者某一个数据库分区,短时间内会形成极高的热点查询。

所以需要在代码中使用一些本地缓存方案,如 HashMap,Guava Cache 或者是 Ehcache 等,它们和应用程序部署在同一个进程中,优势是不需要跨网络调度,速度极快,所以可以来阻挡短时间内的热点查询。

由于本地缓存是部署在应用服务器中,应用服务器通常会部署多台,当数据更新时,不能确定哪台服务器本地中了缓存,更新或者删除所有服务器的缓存不是一个好的选择,所以通常会等待缓存过期。因此,这种缓存的有效期很短,通常为分钟或者秒级别,以避免返回前端脏数据。

2.缓存的不足

缓存的主要作用是提升访问速度,从而能够抗住更高的并发。但事物都是具有两面性的,缓存也不例外。

首先,缓存比较适合于读多写少的业务场景,并且数据最好带有一定的热点属性。

其次,缓存会给整体系统带来复杂度,并且会有数据不一致的风险。当更新数据库成功,更新缓存失败的场景下,缓存中就会存在脏数据。对于这种场景,我们可以考虑使用较短的过期时间或者手动清理的方式来解决。

再次,缓存通常使用内存作为存储介质,但是内存并不是无限的。因此,在使用缓存的时候要做数据存储量级的评估,对于可预见的需要消耗极大存储成本的数据,要慎用缓存方案。

最后,缓存会给运维也带来一定的成本,运维需要对缓存组件有一定的了解,在排查问题的时候也多了一个组件需要考虑在内。


二,缓存的读写策略

1.Cache Aside

Cache Aside旁路策略, 是我们在使用分布式缓存时最常用的策略。

在更新数据时不更新缓存,而是删除缓存中的数据,在读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。

其中读策略的步骤是:

从缓存中读取数据;
如果缓存命中,则直接返回数据;
如果缓存不命中,则从数据库中查询数据;
查询到数据后,将数据写入到缓存中,并且返回给用户。


写策略的步骤是:

更新数据库中的记录;
删除缓存记录。


2.Read/Write Through

这两个个策略的核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。

Write Through

Write Through的策略是这样的:先查询要写入的数据在缓存中是否已经存在,如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,如果缓存中数据不存在,这种情况叫做“Write Miss(写失效)”。

选择两种“Write Miss”的处理方式:一个是“Write Allocate(按写分配)”,做法是写入缓存相应位置,再由缓存组件同步更新到数据库中;另一个是“No-write allocate(不按写分配)”,做法是不写入缓存中,而是直接更新到数据库中。

在 Write Through 策略中,一般选择“No-write allocate”方式,原因是无论采用哪种“Write Miss”方式,而“No-write allocate”方式相比“Write Allocate”减少了一次缓存的写入,能够提升写入的性能。


Read Through

Read Through 策略的步骤是这样的:先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据。


3.Write Back 策略

这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中。

Write Back 策略是计算机体系结构中的策略,写入策略中的只写缓存,异步写入后端存储,这样的的策略有很多的应用场景。


三,缓存的高可用方案

对于缓存命中率这个指标(缓存命中率 = 命中缓存的请求数 / 总请求数),核心缓存的命中率需要维持在 99% 甚至是 99.9%,哪怕下降 1%,系统都会遭受毁灭性的打击。

假设系统的 QPS 是 10000/s,每次调用会访问 10 次缓存或者数据库中的数据,那么当缓存命中率仅仅减少 1%,数据库每秒就会增加 10000 * 10 * 1% = 1000 次请求。而一般来说我们单个 MySQL 节点的读请求量峰值就在 1500/s 左右,增加的这 1000 次请求很可能会给数据库造成极大的冲击。

命中率仅仅下降 1% 造成的影响就如此可怕,更不要说缓存节点故障了。

为了解决这些问题,缓存的高可用方案有:客户端方案、中间代理层方案和服务端方案三大类:

1.客户端方案

就是在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性。

客户端方案需要关注缓存的写和读两个方面:

写入数据时,需要把被写入缓存的数据分散到多个节点中,即进行数据分片;对于读,一般会采用主从机制和多副本机制来保证缓存命中率。

对于写数据的分片算法常见的就是 Hash 分片算法和一致性 Hash 分片算法两种。

Hash 分片算法就是对这个缓存的 Key 做Hash 算法生成 Hash 值来对节点数取模,得出的结果就是要存入缓存节点的序号。这种方式有个问题是节点数变化时,缓存命中率会下降,所以一般建议采用一致性 Hash 分片算法(略)。

读数据时,可以利用多组的缓存来做容错,提升缓存系统的可用性。关于读数据,这里可以使用主从和多副本两种策略,两种策略是为了解决不同的问题而提出的。

2.中间代理层方案

是在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用。

3.服务端方案

服务端方案就是 Redis 2.4 版本后提出的 Redis Sentinel 方案。


四,缓存穿透

1.什么是缓存穿透

缓存穿透其实是指从缓存中没有查到数据,而不得不从后端系统(比如数据库)中查询的情况。

什么样的缓存穿透对系统有害呢?答案是大量的穿透请求超过了后端系统的承受范围,造成了后端系统的崩溃。

一个经典场景:读取一个用户表中未注册的用户。

缓存的读写策略常采用 Cache Aside 策略。按照这个策略,先读缓存,再穿透读数据库。由于用户并不存在,所以缓存和数据库中都没有查询到数据,因此也就不会向缓存中回种数据(也就是向缓存中设置值的意思)。

这样当再次请求这个用户数据的时候还是会再次穿透到数据库。在这种场景下,缓存并不能有效地阻挡请求穿透到数据库上。

2.解决方案

有两种:

回种空值以及使用布隆过滤器。

回种空值比较简单,但需要占用大量存储空间,使用的时候应该评估一下缓存容量是否能够支撑。

而布隆过滤器维护了一个数组,可以用来迅速判断一个元素是否存在,那么当我们需要查询某一个用户的信息时,我们首先查询这个 ID 在布隆过滤器中是否存在,如果不存在就直接返回空值,而不需要继续查询数据库和缓存,这样就可以极大地减少异常查询带来的缓存穿透。

但布隆过滤器有两个缺陷:

1. 它在判断元素是否在集合中时是有一定错误几率的,比如它会把不是集合中的元素判断为处在集合中(Hash 算法的问题)

布隆过滤器虽然存在误判的情况,但是还是会减少缓存穿透的情况发生,只是需要尽量减少误判的几率,这样判断正确的几率更高,对缓存的穿透也更少。一个解决方案是:

使用多个 Hash 算法为元素计算出多个 Hash 值,只有所有 Hash 值对应的数组中的值都为 1 时,才会认为这个元素在集合中。

2. 不支持删除元素。

最后,对于极热点缓存数据穿透造成的“狗桩效应”,可以通过设置分布式锁或者后台线程定时加载的方式来解决。


五,采用CDN解决远程访问静态资源的问题

1. 对于移动 APP 来说,这些静态资源主要是图片、视频和流媒体信息。

2. 对于 Web 网站来说,则包括了 JavaScript 文件,CSS 文件,静态 HTML

静态资源通常很大,访问的关键点是就近访问,即北京用户访问北京的数据,杭州用户访问杭州的数据,这样才可以达到性能的最优。

所以考虑在业务服务器的上层,增加一层特殊的缓存,用来承担绝大部分对于静态资源的访问,这一层特殊缓存的节点需要遍布在全国各地,这样可以让用户选择最近的节点访问。缓存的命中率也需要一定的保证。

CDN(Content Delivery Network/Content Distribution Network,内容分发网络)。

简单来说,CDN 就是将静态的资源分发到,位于多个地理位置机房中的服务器上,因此它能很好地解决数据就近访问的问题,也就加快了静态资源的访问速度。

如何将用户的请求映射到 CDN 节点上,需要用到DNS 技术。DNS 解析结果需要做本地缓存,降低 DNS 解析过程的响应时间;GSLB 可以给用户返回一个离着他更近的节点,加快静态资源的访问速度。

你可能感兴趣的:(高并发系统之缓存设计)