通鉴
个人介绍:童健2018年加入Qunar.com技术团队。目前在火车票事业部/技术部。个人对分布式微服务架构和高并发系统感兴趣,对编写CleanCode有着执着的追求。
一、前言在应用中使用缓存技术可以大大减少计算量,有效提高响应速度,让有限的资源为更多的用户服务。然而,似乎没有一种缓存方案能够满足所有的业务场景。我们需要根据自己的特殊场景和背景选择最合适的缓存方案,尽可能以最小的成本和最快的效率达到最好的目标。本文将从多个方面对缓存进行分析,从而考虑缓存方案的选择。
二、文章要点理解缓存的基础概念了解 CPU 缓存分布式缓存原理了解影响缓存效率的因素高并发中缓存问题的解决方案三、缓存的理解3.1 狭义的理解缓存指的是CPU缓存。CPU想读取一个数据时,先从CPU缓存中查找,立即读取并发送给CPU处理;如果没有找到,会以相对较慢的速度从内存中读取,并发送给CPU处理。同时,该数据所在的数据块将被转移到缓存中,以便将来从缓存中读取整个数据,而不调用内存。
3.2 广义的理解任何位于两种速度差异很大的硬件/软件之间,用于协调两者之间数据传输速度差异的结构都可以称为缓存。
3.3 缓存的优点如下所示,网络应用程序架构通常有以下几层:
该架构的不同级别之间可以有缓存。例如:
给数据库加上缓存可以减少文件系统 I/O;给应用程序加上缓存能够减少对数据库的查询;给 Web 服务器加上缓存能够减少应用服务器请求;给客户端浏览器加上缓存能够减少对网站的访问。计算机在 CPU 和主存之间添加了高速缓存以加快读取速率;操作系统磁盘中一般也添加了缓存,可以减少磁盘机械操作。总而言之,缓存在以下三个方面得到了改进:
性能——将相应数据存储起来以避免数据的重复创建、处理和传输,可有效提高性能。比如将不改变的数据缓存起来,例如国家列表等,这样能明显提高 Web 程序的反应速度;稳定性——同一个应用中,对同一数据、逻辑功能和用户界面的多次请求时经常发生的。当用户基数很大时,如果每次请求都进行处理,消耗的资源是很大的浪费,也同时造成系统的不稳定。例如,Web 应用中,对一些静态页面的呈现内容进行缓存能有效的节省资源,提高稳定性。而缓存数据也能降低对数据库的访问次数,降低数据库的负担和提高数据库的服务能力;可用性——有时,提供数据信息的服务可能会意外停止,如果使用了缓存技术,可以在一定时间内仍正常提供对最终用户的支持,提高了系统的可用性。四、CPU 缓存简介中央处理器缓存内存是位于中央处理器和内存之间的临时内存。它的容量比内存小得多,但它的交换速度却比内存快得多。缓存的出现主要是为了解决CPU运算速度和内存读写速度的矛盾,因为CPU运算速度比内存读写速度快很多,会让CPU花很长时间等待数据到达或者把数据写入内存。缓存中的数据是内存的一小部分,但这一小部分会在短时间内被CPU访问。当CPU调用大量数据时,可以避开内存,直接从缓存中调用,从而加快读取速率。可见,给CPU增加缓存是一种高效的解决方案,使整个内存(cache+memory)成为一个兼具缓存率和内存的大容量存储系统。SRAM内存基本上是用来缓存的,电脑内部内存的组织如下图所示:
内存越高,容量越小,成本越高,速度越快。由于CPU和主存之间巨大的速度差异,系统设计者被迫在CPU寄存器和主存之间插入一个叫做L1缓存的小SRAM缓存,大约2-4个时钟周期(计算机中最小的时间单位)就可以访问。后来发现L1缓存和主存差距还是很大的,L1缓存和主存之间插入了L2缓存,大概10个时钟周期就可以访问。后来增加了L3等,这样,在这种模式下,在不断的进化中形成了现在的存储系统。
五、分布式缓存原理5.1 本地缓存本地缓存可能是最常用的缓存方法之一,比如Ehcache、Guava Cache等。它是应用程序中的缓存组件。它最大的优点是应用和缓存在同一个进程中,请求缓存非常快,没有过多的网络开销等。当单个应用程序不需要集群支持或在集群条件下节点不需要相互通知时,使用本地缓存更合适。同时,它的缺点是缓存与应用程序耦合,所以多个应用程序不能直接共享缓存,每个应用程序或集群的每个节点都需要维护自己独立的缓存,这是对内存的浪费。
5.2 分布式缓存特性分布式缓存可以高性能读取数据,动态扩展缓存节点,自动发现和切换故障节点,自动平衡数据分区,为用户提供图形化的管理界面,非常方便部署和维护。优秀的分布式缓存系统包括阿里自主开发的Memcached、Redis、Tair
那么,分布式缓存是如何工作的呢?
5.3 分布式缓存实现原理数据读取
分布式缓存由一台服务器管理和控制,数据由多个客户端节点存储,提高了数据读取率。当读取某个数据时,可以根据一致的哈希算法来确定数据的存储和读取节点。在数据d和节点总数n的基础上,通过一致哈希算法计算数据d对应的哈希值(相当于门牌号),根据这个哈希值可以找到对应的节点。一致哈希算法的优点是,当节点数量发生变化(减少或增加)时,不需要重新计算哈希值,保证了在存储或读取数据时能够正确、快速地找到对应的节点。
数据的均匀分布
当数据由多个客户端节点存储时,需要确保数据的均匀分布。比如服务器数量少,可能会导致部分服务器存储数据多,承受压力大,部分服务器相对空空闲。解决方法是将一台服务器虚拟成多台服务器,在计算服务器对应的哈希值时,给IP地址字符串添加多个“后缀”,如:10 . 0 . 0 . 1 # 1 10 . 0 . 0 . 1 # 2 10 . 0 . 0 . 0 . 1 # 3...这样,一台物理服务器被虚拟化为多台服务器。
数据热备份
在实现数据热备份之前,需要了解一致哈希算法。在计算多个服务器的IP地址哈希值时,这些哈希值按顺时针方向从小到大排序,形成“服务器节点环”。顺时针看“服务器环”,当一个客户端在第一个服务器上存储数据时,第一个服务器负责将数据复制到第二个服务器上,以此类推,也就是说“服务器环”中的每个节点都是前一个节点的热备份节点。同时,服务器存储两种类型的数据,一种是自己的业务数据,另一种是前一个节点的热备数据。
六、影响缓存性能因素6.1 序列化访问本地缓存。对于JVM语言,您可以在堆内缓存和堆外缓存之间进行选择。因为内部存储是直接以对象的形式,所以不需要序列化,而外部存储是字节类型,所以需要序列化和反序列化。序列化一般需要分析对象的结构,解析对象的结构会带来很大的CPU消耗,所以一般的序列化(比如fastJson)会缓存对象解析的对象结构,以减少CPU消耗。这里没有列出具体的序列化性能比较,但是可以通过单击链接来查看。
6.2 命中率一般来说,缓存命中率越高,使用缓存的好处越高,应用的性能越好(响应时间越短,吞吐量越高),抗并发能力越强。那么影响缓存命中率的因素是什么呢?
业务场景和业务需求
缓存适合“重复读较多”的业务场景,反之,使用缓存的意义其实并不大,命中率会很低;业务需求决定了对时效性的要求,直接影响到缓存的过期时间和更新策略。时效性要求越低,就越适合缓存。在相同 key 和相同请求数的情况下,缓存时间越长,命中率会越高;互联网应用的大多数业务场景下都是很适合使用缓存的。缓存的设计粒度和策略
通常情况下,缓存的粒度越小,命中率会越高;当缓存单个对象的时候(例如:单个用户信息),只有当该对象对应的数据发生变化时,我们才需要更新缓存或者让移除缓存。而当缓存一个集合的时候(例如:所有用户数据),其中任何一个对象对应的数据发生变化时,都需要移除缓存(也可以直接更新)。假设其他地方也需要获取该对象对应的数据时(比如其他地方也需要获取单个用户信息),如果缓存的是单个对象,则可以直接命中缓存,反之,则可能无法直接命中;缓存的更新/过期策略也直接影响到缓存的命中率;一般有如下几种方式:(1)固定的失效时间和被动失效;
(2)感知数据变化,主动更新;
(3)感知数据变化并主动更新。并设置失效时间为被动失效;
(4)根据数据的冷热特性制定策略,如热数据主动失效重装、冷数据失效不重装等。
但是当数据发生变化时,直接更新缓存的命中率要高于移除缓存(或者让缓存过期)的命中率,当然系统复杂度也会更高。
缓存容量和基础架构
有限的缓存容量很容易导致缓存失效和过时(目前大多数缓存框架或中间件都采用LRU算法)。同时,缓存技术的选择也很重要。例如,使用内置本地缓存更容易出现独立瓶颈,而使用分布式缓存更容易扩展。所以需要规划系统容量,考虑是否可以扩展。另外,不同的缓存框架或者中间件的效率和稳定性也是不同的。
其他因素
缓存失效的处理:缓存节点失效时,需要避免缓存失效,将影响降到最低。业内的典型方式是通过一致的哈希算法或节点冗余。
从上面可以看出,为了提高缓存的效益,应用程序需要尽可能直接通过缓存获取数据,避免缓存失效。业务需求、缓存粒度、缓存策略、技术选择等方面都要综合考虑和权衡。尽可能关注访问频率高、时效性要求低的热点业务,通过缓存预加载(预热)、增加存储容量、调整缓存粒度、更新缓存等手段提高命中率。
6.3 缓存清空策略通过上面的介绍,我们知道缓存策略对缓存的性能有很大的影响。那么,缓存策略要解决哪些问题,有哪些选择呢?
面临的问题
主存容量远大于CPU缓存,磁盘容量远大于主存,所以无论哪一级缓存都面临着同样的问题:当有限容量缓存的空 idle 空周期全部用完,需要向缓存中添加新的内容时,如何选择并丢弃部分原始内容,从而腾出空周期来放这些新内容?
解决办法
有几种算法可以解决这个问题,如最少使用算法(LRU)、先进先出算法(先进先出)、最近最少使用算法(LFU)、非最近使用算法(NMRU)等。这些算法在不同级别的缓存上执行时效率和成本不同,需要根据具体情况选择最合适的。以下是每种算法的简要介绍:
FIFO(first in first out)先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。LFU(less frequently used)最少使用策略,这个缓存算法使用一个计数器来记录条目被访问的频率。通过使用LFU缓存算法,最低访问数的条目首先被移除。这个方法并不经常使用,因为它无法对一个拥有最初高访问率之后长时间没有被访问的条目缓存负责。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。LRU(least recently used)最近最少使用策略,这个缓存算法将最近使用的条目存放到靠近缓存顶部的位置。当一个新条目被访问时,LRU 将它放置到缓存的顶部。当缓存达到极限时,较早之前访问的条目将从缓存底部开始被移除。这里会使用到昂贵的算法,而且它需要记录“年龄位”来精确显示条目是何时被访问的。此外,当一个 LRU 缓存算法删除某个条目后,“年龄位”将随其他条目发生改变。策略算法主要比较元素最近一次被 get 使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。自适应缓存替换算法(ARC):在 IBM Almaden 研究中心开发,这个缓存算法同时跟踪记录 LFU 和 LRU,以及驱逐缓存条目,来获得可用缓存的最佳使用。最近最常使用算法(MRU):这个缓存算法最先移除最近最常使用的条目。一个 MRU 算法擅长处理一个条目越久,越容易被访问的情况。七、高并发场景常见缓存问题一般来说,在缓存时间和密钥相同的情况下,并发性越高,缓存的好处就越高,即使缓存时间很短。然而,在高并发应用程序场景中,通常会出现以下三个常见问题。
7.1 缓存穿透问题问题描述
场景:查询某个不存在的数据。因为缓存未命中时是被动写入的,而且为了容错,如果从存储层找不到数据,就不会写入缓存,这样会导致不存在的数据每次被请求时都会在存储层被查询,从而失去缓存的意义。流量大的时候,DB可能会挂。如果有人频繁使用不存在的密钥攻击我们的应用,这就是一个漏洞。
解决办法
方法 1. 在封装的缓存 SET 和 GET 部分增加个步骤,如果查询一个 KEY 不存在,就以这个 KEY 为前缀设定一个标识 KEY;以后再查询该 KEY 的时候,先查询标识 KEY,如果标识 KEY 存在,就返回一个协定好的值(如:&&),然后 APP 做相应的处理(如:检查 KEY 是否合法,是否需要查询 DB,是否需要设置缓存等),这样缓存层就不会被穿透。当然这个验证 KEY 的失效时间不能太长。方法 2. 如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,一般只有几分钟。方法 3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。7.2 缓存并发问题问题描述
有时候,如果一个网站有高并发访问,如果一个缓存失败,可能会发生多个进程同时查询DB,同时设置缓存的情况。如果并发量真的很大,也可能造成DB压力过大,缓存更新频繁。
解决办法
可以锁定缓存查询,如果KEY不存在,锁定,然后将DB检入缓存,然后解锁;其他进程找到锁就等,解锁后返回数据或者进入DB查询。
7.3 缓存失效问题问题描述
这个问题的主要原因是高并发。通常,当我们设置缓存的到期时间时,其中一些可能会设置为1分钟和5分钟。当并发性较高时,可能会同时生成多个缓存,到期时间也相同。此时可能会造成到期时间到期时,这些缓存会同时失效,所有请求都会转发给DB,DB可能压力过大。
解决办法
一个简单的方案是分散缓存到期时间。比如我们可以在原有失效时间的基础上增加一个随机值,比如1-5分钟随机,这样每个缓存失效时间的重复率都会降低,很难造成集体失效事件。
总结至此,我们已经介绍完了缓存的内容。相信本文可以帮助我们了解缓存的基本工作原理和常见缓存问题的解决方案。
1.《缓存 缓存技术原理浅析》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《缓存 缓存技术原理浅析》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/junshi/649426.html