redis(11)-缓存设计

缓存穿透
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层,整个过程分为如下3步:
1) 缓存层不命中。
2) 存储层不命中, 不将空结果写回缓存。
3) 返回空结果。

缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
缓存穿透问题可能会使后端存储负载加大, 由于很多后端存储不具备高并发性, 甚至可能造成后端存储宕掉。 通常可以在程序中分别统计总调用数、 缓存层命中数、 存储层命中数, 如果发现大量存储层空命中, 可能就是出现了缓存穿透问题。
造成缓存穿透的基本原因有两个。 第一, 自身业务代码或者数据出现问题, 第二, 一些恶意攻击、 爬虫等造成大量空命中。 下面我们来看一下如何解决缓存穿透问题。
1.缓存空对象

当第2步存储层不命中后, 仍然将空对象保留到缓存层中, 之后再访问这个数据将会从缓存中获取, 这样就保护了后端数据源。
缓存空对象要注意添加过期时间,防止大量的空对象占据内存。还要注意后端如果添加了这个数据,要重写缓存,防止数据不一致。
2.布隆过滤器拦截
这里不详细介绍了。

缓存无底洞
为了满足业务需要添加大量新的节点,但是性能反而下降了,这就是缓存无底洞的现象。那么为什么会产生这种现象呢, 通常来说添加节点使得集群性能应该更强了, 但事实并非如此。 键值数据库由于通常采用哈希函数将key映射到各个节点上, 造成key的分布与业务无关, 但是由于数据量和访问
量的持续增长, 造成需要添加大量节点做水平扩容, 导致键值分布到更多的节点上, 所以批量操作通常需要从不同节点上获取, 相比于单机批量操作只涉及一次网络操作, 分布式批量操作会涉及多次网络时间。
问题原因
·客户端一次批量操作会涉及多次网络操作, 也就意味着批量操作会随着节点的增多, 耗时会不断增大。
·网络连接数变多, 对节点的性能也有一定影响。
用一句通俗的话总结就是, 更多的节点不代表更高的性能, 所谓“无底洞”就是说投入越多不一定产出越多。 但是分布式又是不可以避免的, 因为访问量和数据量越来越大, 一个节点根本抗不住, 所以如何高效地在分布式缓存中批量操作是一个难点。
具体的解决方案
·命令本身的优化, 例如优化SQL语句等。
·减少网络通信次数。
·降低接入成本, 例如客户端使用长连/连接池、 NIO等。这里我们假设命令、 客户端连接已经为最优, 重点讨论减少网络操作次数。

以Redis批量获取n个字符串为例, 有三种实现方法。
·客户端n次get: n次网络+n次get命令本身。
·客户端1次pipeline get: 1次网络+n次get命令本身。
·客户端1次mget: 1次网络+1次mget命令本身。

1.串行命令
由于n个key是比较均匀地分布在Redis Cluster的各个节点上, 因此无法使用mget命令一次性获取, 所以通常来讲要获取n个key的值, 最简单的方法就是逐次执行n个get命令, 这种操作时间复杂度较高, 它的操作时间=n次网络时间+n次命令时间, 网络次数是n。 很显然这种方案不是最优的, 但是实现起来比较简单。

2.串行IO
Redis Cluster使用CRC16算法计算出散列值, 再取对16383的余数就可以算出slot值, 同时我们提到过Smart客户端会保存slot和节点的对应关系, 有了这两个数据就可以将属于同一个节点的key进行归档, 得到每个节点的key子列表, 之后对每个节点执行mget或者Pipeline操作, 它的操作时间
=node次网络时间+n次命令时间, 网络次数是node的个数,很明显这种方案比第一种要好很多, 但是如果节点数太多, 还是有一定的性能问题。

3.并行IO
此方案是将方案2中的最后一步改为多线程执行, 网络次数虽然还是节点个数, 但由于使用多线程网络时间变为O(1) , 这种方案会增加编程的复杂度。

雪崩优化
由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务,或者是缓存突然集体过期,导致所有请求都会达到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。 缓存雪崩的英文原意是stampeding herd(奔逃的野牛) , 指的是缓存层宕掉后, 流量会像奔逃的野牛一样, 打向后端存储。
解决方案
1) 保证缓存层服务高可用性。 和飞机都有多个引擎一样, 如果缓存层设计成高可用的, 即使个别节点、 个别机器、 甚至是机房宕掉, 依然可以提供服务, 例如前面介绍过的Redis Sentinel和Redis Cluster都实现了高可用。
2) 依赖隔离组件为后端限流并降级。 无论是缓存层还是存储层都会有出错的概率, 可以将它们视同为资源。 作为并发量较大的系统, 假如有一个资源不可用, 可能会造成线程全部阻塞(hang) 在这个资源上, 造成整个系统不可用。 降级机制在高并发系统中是非常普遍的: 比如推荐服务中, 如果个性化推荐服务不可用, 可以降级补充热点数据, 不至于造成前端页面是开天窗。 在实际项目中, 我们需要对重要的资源(例如Redis、 MySQL、HBase、 外部接口) 都进行隔离, 让每种资源都单独运行在自己的线程池中, 即使个别资源出现了问题, 对其他服务没有影响。 但是线程池如何管理, 比如如何关闭资源池、 开启资源池、 资源池阀值管理, 这些做起来还是相当复杂的。 这里推荐一个Java依赖隔离工具Hystrix,Hystrix是解决依赖隔离的利器, 只适用于Java应用,所以这里不会详细介绍。
3) 提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定。
4) 对于所有的key设置不同的过期时间,不发生某个时间点存在大量的key过期事件。

缓存击穿
和雪崩类似,雪崩指的是缓存突然不能提供服务,类似环境因素,或设置缓存集体过期,这里的击穿指的是某一热点key突然过期而导致存储层接受大流量的访问,导致cpu,内存压力过大。
1)使用互斥锁,某个热点key突然过期,后端多个线程查询存储层,导致存储量压力过大,这里可以使用互斥锁,redis提供的setnx,即第一个线程执行查询操作,后续线程阻塞直到第一个线程完成任务,之后再去缓存中获取数据。这样就能防止耗费资源的任务多次执行。

你可能感兴趣的:(redis)