谈谈缓存(下)

我们在 谈谈缓存(上)中介绍了缓存的一些基本概念,和使用上面的一些方式。在这篇文章中,我们会着重说下在具体场景下的解决方案,以及在缓存设计中需要注意的一些关键点。

具体场景下的解决方案

应对热点缓存

所谓热点缓存就是访问非常频繁的那些缓存,比如一个电商系统的秒杀页面,或者是微博的热门话题页等,对于这类页面所请求的数据,如果每次都去远程缓存系统中获取,可能会因为访问量太大导致远程缓存系统出现请求过多、负载过高或者带宽过高等问题,最终导致缓存响应慢,使客户端请求超时。

处理这类问题,一种方案是增加本地缓存,从而避免访问远程缓存。另一种方案就是采取缓存复制,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台服务器压力。

我们都知道Memcached采用了一致性hash算法来路由请求,一个key映射到一个节点上,如果想将同一份数据同时缓存到不同的节点上,要么改变路由算法,要么缓存多个key。

如果是改变路由算法,可以将一致性hash降级为轮询,这样做的一个缺点是,在刚切换的时候会降低整个系统的缓存命中率,给数据库带来比较大的压力。所以,对于能预见到的热点数据,可以在一开始的时候就采用轮询的算法。

缓存多key就是将原来的key-value发布为key1-value、key2-value … key100-value … ,我们可以把这里的key1、key2 … key100 … 看做是原始key的别名。对于大量读操作而言,通过client端路由策略,随意返回一个key{N}即可。

写操作和删除操作则相对麻烦一些,当做删除动作时,会删除所有的别名key,而写操作,也要写所有别名key。无论哪种,都是一个批量操作,怎么保证都成功呢?

首先,我们要确定是否能够容忍部分失败导致的数据不一致问题,如果可以容忍,那么我们的系统就可以简单一些。在不能够容忍不一致的情况下,我们可以通过定时任务来执行操作。当有写操作或删除操作时,这个动作通过消息队列发送给定时任务来更新缓存,只有所有的操作都成功了才代表最终的成功,否则会重试。

此外,这里还有一个细节需要注意,就是key1、key2 … key100 … 不能够设置成相同的过期时间,否则会出现所有缓存副本同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同缓存副本的过期时间是指定范围内的随机值。

应对大型缓存

大型缓存就是缓存的值很大,由于Redis是单线程实现,所以这种情况可能导致redis的吞吐量急剧下降。这个时候我们可以考虑使用多线程实现的缓存,比如Memcached。但是,我们在 谈谈缓存(上) 中介绍过,缓存到Memcached中的数据最好不要超过1M的大小。为了减小value 的大小,我们可以对其进行压缩,或者是采用维度化。

维度化就是把一个大而全的数据,拆成按维度划分的几组数据,比如一个商品可以拆成基础属性、图片列表、规格参数、商品介绍等。如果不做维度化,假如商品的某一项属性发生了变更,就要把这些数据都更新一遍,这个成本还是很高的,而且,不同维度的数据,可能更新的频率都不大一样,维度化后能减少服务很大的压力。

缓存架构设计的几个细节

超时时间

在使用远程缓存(如Redis、Memcached)时,一定要对操作的超时时间进行设置,这是非常关键的。一般我们设计缓存作为加速数据库读取的手段,也会对缓存操作做降级处理,因此推荐使用更短的缓存超时时间,经验值一般是100毫秒以内就可以了。

有的同学在使用缓存的时候忽略了操作超时的设置,或者超时时间设的很长,这些都有可能带来很严重的问题,比如线程数过高,甚至是内存溢出等。

究其原因,就是缓存连接数达到最大限制后,应用就无法再连接缓存,然而,又由于超时时间设置得较大,导致访问缓存的服务都在等待缓存操作返回。这个时候,缓存负载较高,处理不完所有的请求,所有的服务都在等待缓存操作返回,服务这时等待没有超时,就不能降级继而访问数据库。这在BIO模式下线程池就会撑满,在NIO模式下一样会使服务的负载增加,服务响应变慢,甚至使服务被压垮。

缓存预热

提前把数据读入到缓存的做法就是缓存预热处理。

对于一些访问量比较大的请求,如果不做缓存预热,会因为缓存无法提供热门数据,而导致大量操作被穿透到底层数据库服务器上,这个风险是无需多说的。做预热处理时,我们要注意下面几个细节问题:

(1)要有监控机制确保预热数据都写成功了,如果是部分数据成功,可能会对高峰期业务有一定的影响。

(2)数据预热最好配备回滚方案,遇到紧急情况需要回滚时便于操作。

(3)对要预热的数据量做好容量评估,在容量允许的范围内预热全量,否则预热访问量高的。

(4)预热过程中需要注意是否会因为批量数据库操作或慢sql等引发数据库性能问题。

缓存穿透

从字面意思理解,缓存没有起到该有的作用,我们的请求到达缓存后没有查询到相应的数据,于是请求继续打到存储系统,就像把缓存层穿透了一样。出现这种问题,一般是由两种情况造成的,第一种情况是被访问的数据确实不存在,另一种情况是数据虽然存在,但是生成缓存数据的成本较高,需要耗费大量的时间和资源。下面我们分别针对这两种情况看下它们所带来的问题和怎么解决。

存储数据不存在

如果在存储系统中确实没有存储某项数据,这个时候又恰好有请求来查询该数据,因为在缓存中拿不到结果,每次都要再去存储系统中查找一遍,最终的结果当然还是返回数据不存在。在这种场景下缓存没有能够分担存储系统的访问压力,因此,一旦这种类型的访问量过大,就可能将存储系统拖垮。

通常情况下,正常的业务不太会读取不存在的数据,我们这里要预防的往往是一些非正常访问,比如黑客的攻击等。解决方法可以参考下面两种:

  1. 给定一个默认值放到缓存中,比如从存储系统查到一个空列表,那就在缓存放一个空集,如果某一项没有查到,就放一个null值等。
  2. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

综合来看,方案一实现简单,推荐使用该种方式。

生成缓存数据需要大量的时间和资源

这种情况下,问题都是出在缓存刚刚失效的时候,在缓存失效的一瞬间,新的缓存数据不能立即重建,缓存系统不能够发挥作用,访问压力全部集中到了存储系统上。

如果是量不大的数据,我们可以考虑全量缓存,并且设定数据永不过期,然后使用定时任务定期去更新缓存。

但是,对于量比较大的数据,其实是没有特别好的方案的,我们唯一能做的就是加好监控,发现问题后及时处理。

缓存雪崩

当缓存失效或者是过期后,业务系统需要再次访问存储系统、再次进行计算,来重新生成缓存,这个过程耗费的时间从几十毫秒到上百毫秒不等。对于高并发的系统来说,在这么短的时间内也会有成百上千的请求打过来,这些请求会给存储系统造成巨大的性能压力,严重的甚至会导致数据库宕机,从而使得整个系统崩溃。

因此,预防缓存雪崩的方案往往都是为了减少同一时间到达存储系统的访问量。

过期时间随机化

第一种方案就是将缓存失效时间分散开,对不同key的过期时间进行随机设置,例如,将过期时间设置为60秒+random(5),也就是将过期时间随机设置成60~65秒,这样最大程度减少了同一时间缓存失效的数量。但是,如果单个key的访问量也足够大,就需要再配合其他的方案了。

更新锁机制

对于设定了过期时间的缓存,在更新操作的时候进行加锁保护,保证在同一时间只有一个线程请求到存储层,其他的线程发现已有线程在执行更新操作后,要么等待,要么直接返回默认值。

我们这里说的加锁,通常指的是分布式锁,因为能够发生雪崩效应的系统必然是有上百台甚至更多的服务器在提供服务,即使单台机器只有一个线程更新缓存,整个集群加起来也会有上百个线程同时请求更新缓存。我们可以考虑使用ZooKeeper或Redis来实现分布式锁。

当然,如果机器不是很多,或者比较确定存储系统能够抗住同机器数量的请求量,我们是没必要使用分布式缓存的,这种东西能不用就不用,系统搞得太复杂了出问题的概率也就相应的变大了。

缓存不过期机制

这里的“不过期”指的是在将数据放入缓存系统的同时,没有设置过期时间,也就是“物理”不过期,这样保证了不会出现热点key过期的问题。对于缓存数据的更新问题,可以考虑下面的两种方案:

  1. 后台程序起一个定时任务来更新缓存。当数据量不大时,可以把数据全量放到缓存里面,如果数据量较大,就没办法将所有的数据都放到缓存里了,那么,在某一个时刻,就会存在业务线程读取不到数据的情况。解决这个问题的方法有两种:

    • 后台线程除了定时去更新缓存外,还要频繁的去读取缓存,当读不到数据时,就要及时的更新缓存。这里读取缓存的时间间隔不能太长,通常不要超过1秒,在不影响正常业务的情况下,间隔时间越短越好。
    • 另外一个方案是,后台线程还是只负责定时更新缓存。当业务线程读取不到缓存时,通过消息队列发送一条通知给后台另外一个线程更新缓存。
  2. 应对缓存不过期的另一个方案是把过期时间存在key对应的value里,每次拿到数据后逻辑判断下该数据是否过期,如果发现数据已经过期了,就触发一个后台的异步线程进行缓存的刷新。

    如果这个时候其他的业务线程也发现数据过期,并且已有线程在进行缓存刷新操作,就直接返回旧数据。刷新线程在获取最新数据的时候可能会有两种结果,成功或者失败,如果失败了,这里采取的操作是对原缓存进行”续费“,也就是缓存中的旧值不变,但是过期时间在此刻的基础上延后一定时间,在续费的这段时间里,所有的业务线程会将该数据看做是新数据,不会触发更新操作。这样做的好处是避免了在更新失败后的不断重试。

雪崩后的恢复

如果真的很不幸,发生了上面说的缓存雪崩问题,这个时候一般重启系统是没用的,只能去慢慢将缓存重建,开始的时候需要运维控制流量,等后台系统启动完毕后再循序渐进的放开流量。 流量控制刚开始可以为10%,然后20%,然后50%,然后80%,最后全量,当然具体的比例,尤其是初始比例,还要看后端承受能力和前端流量的比例,各个系统并不相同。

另外一种方案就是先进行缓存的预热,等缓存热起来后再发布后台系统即可,这样也省去了运维的工作。但是此方案仅仅适用于缓存中key空间不是很大的情况,如果key空间较大,开发预热工具是比较困难的。

缓存宕机

这应该是最坏的一种情况,当缓存宕机后,所有的请求会打到存储系统,这样一来大流量会将其压垮。应对这种情况,有三种方案:

  1. 业务线程不请求存储系统,而是打个日志并设置一个默认值。
  2. 业务线程按照一定概率决定是否请求存储系统。
  3. 业务线程检查存储系统运行情况,如果良好则请求存储系统。

方案1最简单,我们知道存储系统可能扛不住业务的全部流量,索性就不请求存储系统,而是等待Cache恢复。但这时存储系统的利用率为0,显然不是最优方案,而且当请求的Value不容易设置默认值时,这个方案就不行了。

方案2可以让一部分线程请求存储系统,这部分请求要保证不能击垮存储系统,在设置这个概率时可以保守些。

方案3是一种更为智能的方案,如果存储系统运行良好,当前线程请求,如果存储系统过载,则不请求,这样让存储系统处于一种宕机与不宕机的临界状态,最大限度挖掘存储系统性能。这种方案要求存储系统提供一个性能评估接口返回Yes和No,Yes表示系统良好,可以请求;No表示系统情况不妙,不要请求。这个接口将被频繁调用,必须高效。

方案3的关键在于如何评估一个系统的运行状况。一个系统中当前主机的性能参数有CPU负载、内存使用率、Swap使用率、GC频率和GC时间、各个接口平均响应时间等,性能评估接口需要根据这些参数返回Yes或者No,如何提供一套统一的系统性能实时评估方案和工具,不是一个简单的事情。综合来看,方案2还是比较靠谱的。

综上,我们介绍了在高并发的场景下使用缓存可能会遇到的一些问题,以及一些相应的解决方案,希望对你也有一定的收获。

完。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值