微信号:QunarTL

介绍:Qunar技术沙龙是去哪儿网工程师小伙伴以及业界小伙伴们的学习交流平台.我们会分享Qunar和业界最前沿的热门技术趋势和话题;为中高端技术同学提供一个自由的技术交流和学习分享平台.

缓存技术原理浅析

2018-10-31 09:00 童健

点击上方蓝字关注带来好运哦!!

童健


个人介绍:童健,2018 年加入去哪儿网技术团队。目前在火车票事业部/技术部。个人对分布式微服务架构、高并发的系统很感兴趣,对编写 CleanCode 有执着的追求。

一、前言

应用中使用缓存技术,往往可以大大减少计算量,有效提升响应速度,让有限的资源服务更多的用户。但是,似乎还没有一种缓存方案可以满足所有的业务场景,我们需要根据自身的特殊场景和背景,选择最适合的缓存方案,尽量以最小的成本最快的效率达到最优的目的。本文将从多个方面对缓存进行分析,以便作为选择缓存方案的考量。

二、文章要点

  • 理解缓存的基础概念

  • 了解 CPU 缓存

  • 分布式缓存原理

  • 了解影响缓存效率的因素

  • 高并发中缓存问题的解决方案

三、缓存的理解

3.1 狭义的理解

缓存指的是 CPU 缓存,当 CPU 要读取一个数据时,首先从 CPU 缓存中查找,找到就立即读取并送给 CPU 处理;没有找到,就从速率相对较慢的内存中读取并送给 CPU 处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。

3.2 广义的理解

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

3.3 缓存的优点

如下,一个 Web 应用架构一般有如下几层:

在此架构的不同层级之间,都可以存在缓存。比如:

  1. 给数据库加上缓存可以减少文件系统 I/O;

  2. 给应用程序加上缓存能够减少对数据库的查询;

  3. 给 Web 服务器加上缓存能够减少应用服务器请求;

  4. 给客户端浏览器加上缓存能够减少对网站的访问。

  5. 计算机在 CPU 和主存之间添加了高速缓存以加快读取速率;

  6. 操作系统磁盘中一般也添加了缓存,可以减少磁盘机械操作。

总结来说,缓存在如下三个方面做了提升:

  1. 性能——将相应数据存储起来以避免数据的重复创建、处理和传输,可有效提高性能。比如将不改变的数据缓存起来,例如国家列表等,这样能明显提高 Web 程序的反应速度;

  2. 稳定性——同一个应用中,对同一数据、逻辑功能和用户界面的多次请求时经常发生的。当用户基数很大时,如果每次请求都进行处理,消耗的资源是很大的浪费,也同时造成系统的不稳定。例如,Web 应用中,对一些静态页面的呈现内容进行缓存能有效的节省资源,提高稳定性。而缓存数据也能降低对数据库的访问次数,降低数据库的负担和提高数据库的服务能力;

  3. 可用性——有时,提供数据信息的服务可能会意外停止,如果使用了缓存技术,可以在一定时间内仍正常提供对最终用户的支持,提高了系统的可用性。

四、CPU 缓存简介

CPU 缓存(Cache Memory)是位于 CPU与 内存之间的临时存储器,它的容量比内存小的多,但是交换速率却比内存要快得多。缓存的出现主要是为了解决 CPU 运算速率与内存读写速率不匹配的矛盾,因为 CPU 运算速率要比内存读写速率快很多,这样会使 CPU 花费很长时间等待数据到来或把数据写入内存。在缓存中的数据是内存中的一小部分,但这一小部分是短时间内 CPU 即将访问的,当 CPU 调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速率。由此可见,在 CPU 中加入缓存是一种高效的解决方案,这样整个内存储器(缓存+内存)就变成了既有缓存的高速率,又有内存的大容量的存储系统了。 缓存基本上都是采用 SRAM 存储器,存储器在计算机内部的组织方式如下图所示:

越往上,存储器的容量越小、成本越高、速度越快。由于 CPU 和主存之间巨大的速度差异,系统设计者被迫在 CPU 寄存器和主存之间插入了一个小的 SRAM 高速缓存存储器称为 L1 缓存,大约可以在 2-4 个时钟周期(计算机中最小的时间单位)内访问。再后来发现 L1 高速缓存和主存之间还是有较大差距,又在 L1 高速缓存和主存之间插入了 L2 缓存,大约可以在 10 个时钟周期内访问。后面还新增了 L3 等,于是,在这样的模式下,在不断的演变中形成了现在的存储体系。

五、分布式缓存原理

5.1 本地缓存

本地缓存可能是大家用的最多的一种缓存方式了,如 Ehcache、Guava Cache 等,它是在应用中的缓存组件,其最大的优点是应用和 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.1#3... 这样,一台物理服务器就被虚拟化成多台服务器。

数据的热备份

实现数据的热备份之前,需要了解一致性哈希算法,计算多台服务器的 IP 地址哈希值时,是将这些哈希值从小到大按顺时针排序组成一个“服务器节点环”。以顺时针方向看“服务器环”,当有客户端把数据存储在第1台服务器上后,第1台服务器负责把该数据拷贝一份给第 2 台服务器,以此类推,也就是说“服务器环”上的每一个节点,都是上一个节点的热备份节点。同时,一个服务器上存了两类数据,一类是自身的业务数据,一类是上一节点的热备数据。

六、影响缓存性能因素

6.1 序列化

访问本地缓存,对于 JVM 语言,有堆内和堆外缓存可以进行选择。由于对内直接以对象的形式进行存储,不需要考虑序列化,而堆外是以字节类型进行存储,就需要进行序列化和反序列化。 序列化一般需要解析对象的结构,而解析对象结构,会带来较大的 CPU 消耗,所以一般的序列化(比如 fastJson)均会缓存对象解析的对象结构,来减少 CPU 的消耗。 具体序列化性能对比这里就不做罗列,可点击 link 这里查看。

6.2 命中率

通常来讲,缓存的命中率越高则表示使用缓存的收益越高,应用的性能越好(响应时间越短、吞吐量越高),抗并发的能力越强。那么影响缓存命中率因素有哪些呢?

业务场景和业务需求

  1. 缓存适合“重复读较多”的业务场景,反之,使用缓存的意义其实并不大,命中率会很低;

  2. 业务需求决定了对时效性的要求,直接影响到缓存的过期时间和更新策略。时效性要求越低,就越适合缓存。在相同 key 和相同请求数的情况下,缓存时间越长,命中率会越高;

  3. 互联网应用的大多数业务场景下都是很适合使用缓存的。

缓存的设计粒度和策略

  1. 通常情况下,缓存的粒度越小,命中率会越高;

  2. 当缓存单个对象的时候(例如:单个用户信息),只有当该对象对应的数据发生变化时,我们才需要更新缓存或者让移除缓存。而当缓存一个集合的时候(例如:所有用户数据),其中任何一个对象对应的数据发生变化时,都需要移除缓存(也可以直接更新)。假设其他地方也需要获取该对象对应的数据时(比如其他地方也需要获取单个用户信息),如果缓存的是单个对象,则可以直接命中缓存,反之,则可能无法直接命中;

  3. 缓存的更新/过期策略也直接影响到缓存的命中率;

  4. 一般有如下几种方式:

      (1)固定过期时间,被动失效;

      (2)感知数据变更,主动更新;

      (3)感知数据变更,主动更新。并设置过期时间被动失效兜底;

      (4)按照数据冷热性制定策略,如热数据主动失效并 reload,冷数据只失效不 reload 等。

然而,当数据发生变化时,直接更新缓存的值会比移除缓存(或者让缓存过期)的命中率更高,当然,系统复杂度也会更高。

缓存容量和基础设施

缓存的容量有限,则容易引起缓存失效和被淘汰(目前多数的缓存框架或中间件都采用了 LRU 算法)。同时,缓存的技术选型也是至关重要的,比如采用应用内置的本地缓存就比较容易出现单机瓶颈,而采用分布式缓存则比较容易扩展。所以需要做好系统容量规划,并考虑是否可扩展。此外,不同的缓存框架或中间件,其效率和稳定性也是存在差异的。

其他因素

缓存故障处理:当缓存节点发生故障时,需要避免缓存失效并最大程度降低影响,业内比较典型的做法就是通过一致性 Hash 算法,或者通过节点冗余的方式。

以上可见,想要提高缓存收益,需要应用尽可能的通过缓存直接获取数据,并避免缓存失效。需要在业务需求,缓存粒度,缓存策略,技术选型等各个方面去通盘考虑并做权衡。尽可能的聚焦在高频访问且时效性要求不高的热点业务上,通过缓存预加载(预热)、增加存储容量、调整缓存粒度、更新缓存等手段来提高命中率。

6.3 缓存清空策略

通过前面介绍,我们知道缓存策略对于缓存的性能具有很大的影响。那么,缓存策略是为了解决什么问题,又有哪些方案可选呢?

面临的问题

主存容量远大于 CPU 缓存,磁盘容量远大于主存,因此无论是哪一层次的缓存都面临一个同样的问题:当容量有限的缓存的空闲空间全部用完后,又有新的内容需要添加进缓存时,如何挑选并舍弃原有的部分内容,从而腾出空间放入这些新的内容。

解决方案

解决这个问题的算法有几种,如最久未使用算法(LRU)、先进先出算法(FIFO)、最近最少使用算法(LFU)、非最近使用算法(NMRU)等,这些算法在不同层次的缓存上执行时拥有不同的效率和代价,需根据具体场合选择最合适的一种。下面针对每一种算法做一个简单介绍:

  1. FIFO(first in first out)先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。

  2. LFU(less frequently used)最少使用策略,这个缓存算法使用一个计数器来记录条目被访问的频率。通过使用LFU缓存算法,最低访问数的条目首先被移除。这个方法并不经常使用,因为它无法对一个拥有最初高访问率之后长时间没有被访问的条目缓存负责。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。

  3. LRU(least recently used)最近最少使用策略,这个缓存算法将最近使用的条目存放到靠近缓存顶部的位置。当一个新条目被访问时,LRU 将它放置到缓存的顶部。当缓存达到极限时,较早之前访问的条目将从缓存底部开始被移除。这里会使用到昂贵的算法,而且它需要记录“年龄位”来精确显示条目是何时被访问的。此外,当一个 LRU 缓存算法删除某个条目后,“年龄位”将随其他条目发生改变。策略算法主要比较元素最近一次被 get 使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。

  4. 自适应缓存替换算法(ARC):在 IBM Almaden 研究中心开发,这个缓存算法同时跟踪记录 LFU 和 LRU,以及驱逐缓存条目,来获得可用缓存的最佳使用。

  5. 最近最常使用算法(MRU):这个缓存算法最先移除最近最常使用的条目。一个 MRU 算法擅长处理一个条目越久,越容易被访问的情况。

七、高并发场景常见缓存问题

通常来讲,在相同缓存时间和 key 的情况下,并发越高,缓存的收益会越高,即便缓存时间很短。而高并发应用场景下一般会引发以下常见的三个问题。

7.1 缓存穿透问题

问题描述

出现场景:指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能 DB 就挂掉了。要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决方案

  • 方法 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 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

总结

到这里,关于缓存的内容就介绍完毕了,相信通过本文可以帮助我们理解缓存的基本工作原理,了解常见缓存问题的解决思路。

 
Qunar技术沙龙 更多文章 Qunar首届CodeReview大赛报名开始啦! 阿里巴巴为什么选择Apache Flink? 2017年应届生回归大会圆满结束 一个可靠的原创定时任务解决方案 【基本功】 前端安全系列之二:如何防止 CSRF 攻击?
猜您喜欢 为什么要开始写作 在豪言互联网将消失前,谷歌狂砸190亿买了“它” 利用Roslyn构建一个简单的C#交互脚本引擎 荒野大间谍,BBC激情上演 最近碰到的三个Case