缓存可以有效地加速应用的读写速度,同时也能够下降后端负载,对平常应用的开发相当重要。下面会介绍缓存使
用技巧和设计方案,包含以下内容:缓存的收益和成本分析、缓存更新策略的选择和使用场景、缓存粒度控制法、穿透问题优化、无底洞问题优化、雪崩问题优化、热点key重建优化。html
缓存的收益和成本分析
下图左侧为客户端直接调用存储层的架构,右侧为比较典型的缓存层+存储层架构。java
缓存加入后带来的收益和成本。node
收益:
①加速读写:由于缓存一般都是全内存的,而存储层一般读写性能不够强悍(例如MySQL),经过缓存的使用能够
有效地加速读写,优化用户体验。
②下降后端负载:帮助后端减小访问量和复杂计算(例如很复杂的SQL语句),在很大程度下降了后端的负载。
成本:
①数据不一致性:缓存层和存储层的数据存在着必定时间窗口的不一致性,时间窗口跟更新策略有关。
②代码维护成本:加入缓存后,须要同时处理缓存层和存储层的逻辑,增大了开发者维护代码的成本。
③运维成本:以Redis Cluster为例,加入后无形中增长了运维成本。
缓存的使用场景基本包含以下两种:
①开销大的复杂计算:以MySQL为例子,一些复杂的操做或者计算(例如大量联表操做、一些分组计算),若是不
加缓存,不但没法知足高并发量,同时也会给MySQL带来巨大的负担。
②加速请求响应:即便查询单条后端数据足够快(例如select*from tablewhere id=),那么依然可使用缓 存,以Redis为例子,每秒能够完成数万次读写,而且提供的批量操做能够优化整个IO链的响应时间。
缓存更新策略
缓存中的数据会和数据源中的真实数据有一段时间窗口的不一致,须要利用某些策略进行更新,下面会介绍几种主要 的缓存更新策略。 ①LRU/LFU/FIFO算法剔除:剔除算法一般用于缓存使用量超过了预设的最大值时候,如何对现有的数据进行剔 除。例如Redis使用maxmemory-policy这个配置做为内存最大值后对于数据的剔除策略。 ②超时剔除:经过给缓存数据设置过时时间,让其在过时时间后自动删除,例如Redis提供的expire命令。若是业 务能够容忍一段时间内,缓存层数据和存储层数据不一致,那么能够为其设置过时时间。在数据过时后,再从真实数据 源获取数据,从新放到缓存并设置过时时间。例如一个视频的描述信息,能够容忍几分钟内数据不一致,可是涉及交易 方面的业务,后果可想而知。 ③主动更新:应用方对于数据的一致性要求高,须要在真实数据更新后,当即更新缓存数据。例如能够利用消息系 统或者其余方式通知缓存更新
三种常见更新策略的对比:
有两个建议:
①低一致性业务建议配置最大内存和淘汰策略的方式使用。程序员
②高一致性业务能够结合使用超时剔除和主动更新,
这样即便主动更新出了问题,也能保证数据过时时间后删除脏数据。redis
缓存粒度控制
缓存粒度问题是一个容易被忽视的问题,若是使用不当,可能会形成不少无用空间的浪费,网络带宽的浪费,代码
通用性较差等状况,须要综合数据通用性、空间占用比、代码维护性三点进行取舍。
缓存比较经常使用的选型,缓存层选用Redis,存储层选用MySQL。算法
假如我如今须要对视频的信息作一个缓存,也就是须要对select * from video where id=?的每一个id在redis里作一份缓存,这样cache层就能够帮助我抗住不少的访问量(注:这里不讨论一致性和架构等等问题,只讨论缓存的粒度问题)。
咱们假设视频表有100个属性(这个真有,有些人可能不可思议),那么问题来了,须要缓存什么维度呢,也
就是有两种选择吧:sql
catch(id)=select * from video where id=#id catch(id)=select importantColumn1, importantColumn2 .. importantColumnN from video where id=#id 12
其实这个问题就是缓存粒度问题,咱们在缓存设计应该佮预估和考虑呢?下面咱们将从通用性、空间、代码维
护三个角度进行说明。编程
所有数据和部分数据比较
二者的特色是显而易见的:
数据类型 通用性 空间占用(内存空间 + 网络码率) 代码维护
所有数据 高 大 简单
部分数据 低 小 较为复杂后端
数据类 | 通用性 | 空间占用(内存空间 + 网络码率) | 代码维护 |
---|---|---|---|
所有数据 | 高 | 大 | 简单 |
部分数据 | 低 | 小 | 较为复杂 |
通用性
若是单从通用性上看、所有数据是最优秀的,可是有个问题就是是否有必要缓存所有数据,任 务之后会有这样
的需求,可是从经验上看除了很是重要的信息,哪些不重要的字段基本不会再绣球里出现,也就是说着中通用性,
一般都是想象出来的。太多人以为通用性是最重要的。vid拿一些基本信息,回想专辑明星,因而加了全局的,通
用性很重要,可是要想清楚。缓存
空间占用:
很显然,缓存所有数据,会占用大量的内存,有人会说,不就费一点内存吗,能有多少钱?并且已经有人习惯
了把缓存当作下水道来使用,什么都框框的往里面放,可是我这里要说内存并非免费的,能够说是很珍贵的资
源。instagram21->4G的例子就说明了这个道理,好的程序员能够帮助公司节约大量的资源。
代码维护:
代码维护性,所有数据的优点更加明显,而部分数据一旦要加新字段就会修改代码,并且还须要对原来的数据
进行刷新。
总结:
缓存粒度问题是一个容易被忽视的问题,若是使用不当,可能会形成不少无用空间的浪费,可能会形成网络带
宽的浪费,可能会形成代码通用性较差等状况,必须学会综合数据通用性、空间占用比、代码维护性 三点评估取舍因素权衡使用。
缓存穿透
缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,一般出于容错的考虑,若是从存储层查不
到数据则不写入缓存层。
一般能够在程序中分别统计总调用数、缓存层命中数、存储层命中数,若是发现大量存储层空命中,可能就是出现
了缓存穿透问题。形成缓存穿透的基本缘由有两个。第一,自身业务代码或者数据出现问题,第二,一些恶意攻
击、爬虫等形成大量空命中。下面咱们来看一下如何解决缓存穿透问题。
1.缓存空对象:如图下所示,当第2步存储层不命中后,仍然将空对象保留到缓存层中,以后再访问这个数据将会
从缓存中获取,这样就保护了后端数据源。
缓存空对象会有两个问题:第一,空值作了缓存,意味着缓存层中存了更多的键,须要更多的内存空间(若是是攻
击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过时时间,让其自动剔除。第二,缓存层和存
储层的数据会有一段时间窗口的不一致,可能会对业务有必定影响。例如过时时间设置为5分钟,若是此时存储层
添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时能够利用消息系统或者其余方式清除掉
缓存层中的空对象。
2.布隆过滤器拦截
以下图所示,在访问缓存层和存储层以前,将存在的key用布隆过滤器提早保存起来,作第一层拦截。例如:一个
推荐系统有4亿个用户id,每一个小时算法工程师会根据每一个用户以前历史行为计算出推荐数据放到存储层中,可是
最新的用户因为没有历史行为,就会发生缓存穿透的行为,为此能够将全部推荐数据的用户作成布隆过滤器。若是
布隆过滤器认为该用户id不存在,那么就不会访问存储层,在必定程度保护了存储层。
缓存空对象和布隆过滤器方案对比
无底洞优化
为了知足业务须要可能会添加大量新的缓存节点,可是发现性能不但没有好转反而降低了。 用一句通俗的话解释就
是,更多的节点不表明更高的性能,所谓“无底洞”就是说投入越多不必定产出越多。可是分布式又是不能够避免
的,由于访问量和数据量愈来愈大,一个节点根本抗不住,因此如何高效地在分布式缓存中批量操做是一个难点。
无底洞问题分析:
①客户端一次批量操做会涉及屡次网络操做,也就意味着批量操做会随着节点的增多,耗时会不断增大。
②网络链接数变多,对节点的性能也有必定影响。
如何在分布式条件下优化批量操做?咱们来看一下常见的IO优化思路:
- 命令自己的优化,例如优化SQL语句等。
减小网络通讯次数。
下降接入成本,例如客户端使用长连/链接池、NIO等。
这里咱们假设命令、客户端链接已经为最优,重点讨论减小网络操做次数。下面咱们将结合Redis Cluster的一
些特性对四种分布式的批量操做方式进行说明。
①串行命令:因为n个key是比较均匀地分布在Redis Cluster的各个节点上,所以没法使用mget命令一次性获
取,因此一般来说要获取n个key的值,最简单的方法就是逐次执行n个get命令,这种操做时间复杂度较高,
它的操做时间=n次网络时间+n次命令时间,网络次数是n。很显然这种方案不是最优的,可是实现起来比较
简单。
②串行IO:Redis Cluster使用CRC16算法计算出散列值,再取对16383的余数就能够算出slot值,同时Smart客户
端会保存slot和节点的对应关系,有了这两个数据就能够将属于同一个节点的key进行归档,获得每一个节点的key子
列表,以后对每一个节点执行mget或者Pipeline操做,它的操做时间=node次网络时间+n次命令时间,网络次数是
node的个数,整个过程以下图所示,很明显这种方案比第一种要好不少,可是若是节点数太多,仍是有必定的性
能问题。
③并行IO:此方案是将方案2中的最后一步改成多线程执行,网络次数虽然仍是节点个数,但因为使用多线程网络
时间变为O(1),这种方案会增长编程的复杂度。
④hash_tag实现:Redis Cluster的hash_tag功能,它能够将多个key强制分配到一个节点上,它的操做时间=1次网
络时间+n次命令时间。
四种批量操做解决方案对比
雪崩优化
因为缓存层承载着大量请求,有效地保护了存储层,可是若是缓存层因为某些缘由不能提供服务,因而全部的请求
都会达到存储层,存储层的调用量会暴增,形成存储层也会级联宕机的状况。
预防和解决缓存雪崩问题,能够从如下三个方面进行着手:
- 保证缓存层服务高可用性。若是缓存层设计成高可用的,即便个别节点、个别机器、甚至是机房宕掉,依然能够提供服务,例如前面介绍过的Redis Sentinel和Redis Cluster都实现了高可用。
- 依赖隔离组件为后端限流并降级。在实际项目中,咱们须要对重要的资源(例如Redis、MySQL、HBase、外部接口)都进行隔离,让每种资源都单独运行在本身的线程池中,即便个别资源出现了问题,对其余服务没有影响。可是线程池如何管理,好比如何关闭资源池、开启资源池、资源池阀值管理,这些作起来仍是至关复杂的。
- 提早演练。在项目上线前,演练缓存层宕掉后,应用以及后端的负载状况以及可能出现的问题,在此基础上作一些预案设
热点key重建优化
开发人员使用“缓存+过时时间”的策略既能够加速数据读写,又保证数据的按期更新,这种模式基本可以知足绝大
部分需求。可是有两个问题若是同时出现,可能就会对应用形成致命的危害:
当前key是一个热点key(例如一个热门的娱乐新闻),并发量很是大。
重建缓存不能在短期完成,多是一个复杂计算,例如复杂的SQL、屡次IO、多个依赖等。在缓存失效的瞬
间,有大量线程来重建缓存,形成后端负载加大,甚至可能会让应用崩溃。
要解决这个问题也不是很复杂,可是不能为了解决这个问题给系统带来更多的麻烦,因此须要制定以下目
标:
减小重建缓存的次数
数据尽量一致
较少的潜在危险
①互斥锁:此方法只容许一个线程重建缓存,其余线程等待重建缓存的线程执行完,从新从缓存获取数据即
可,整个过程如图所示。
②永远不过时
永远不过时”包含两层意思: 从缓存层面来看,确实没有设置过时时间,因此不会出现热点key过时后产生的问
题,也就是“物理”不过时。 从功能层面来看,为每一个value设置一个逻辑过时时间,当发现超过逻辑过时时间后,
会使用单独的线程去构建缓存。
从实战看,此方法有效杜绝了热点key产生的问题,但惟一不足的就是重构缓存期间,会出现数据不一致的状况,
这取决于应用方是否容忍这种不一致。
两种热点key的解决方法