一、前言

一些数据我们没有必要每次查询时都去查询到数据库,特别是高 QPS的系统,每次都去查询数据库,对于数据库来说就是灾难。我们使用缓存时,我们的业务系统大概的调用流程如下图:

二、缓存穿透

1、概念

缓存穿透是指查询一个一定不存在的数据,这将导致这个不存在的数据每次请求都要到存储层去查询,在流量大时,可能DB就挂掉了,如果有人利用不存在的key频繁攻击我们的应用,会导致系统无法正常访问。

2、解决思路

1)缓存空值

当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间。通常这个时间较短(如5分钟),这个词可能不是热点词汇,较短的缓存时间可以节省内存空间;如果是恶意攻击,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
弊端:

  • 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 ( 如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
  • 不宜与正常值共用空间,否则当空间不足时,缓存系统的LRU算法可能会先剔除正常值,再剔除空值——这个漏洞可能会受到攻击。
  • 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5分钟,如果此时存储层添加了这个数据,此段时间会出现缓存层和存储层数据不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。

2)布隆过滤器(Bloom Filter)

布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。

对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再对持久层查询。
弊端:

Bloom Filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性

  • 存在误判,可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,那么可以通过建立一个白名单来存储可能会误判的元素。
  • 删除困难。一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。可以采用Counting Bloom Filter。

3)代码过滤

接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截等;

三、缓存击穿

1、概念

缓存击穿实际上是缓存雪崩的一个特例,微博有一个热门话题的功能,用户对于热门话题的搜索量往往在一些时刻会大大的高于其他话题,这种我们成为系统的“热点“,由于系统中对这些热点的数据缓存也存在失效时间,在热点的缓存到达失效时间时,此时可能依然会有大量的请求到达系统,没有了缓存层的保护,这些请求同样的会到达db从而可能引起故障。击穿与雪崩的区别即在于击穿是对于特定的热点数据来说,而雪崩是全部数据。

2、解决思路

1)热点数据永不过期

从缓存上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的重新构建,将数据刷新,也就是“逻辑”过期.

弊端:

  • 不保证一致性
  • 代码复杂度增大(每个value都要维护一个timekey)
  • 占用一定的内存空间(每个value都要维护一个timekey)。

2)使用互斥锁(mutex key)

就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据。
如果是单机,可以用synchronized或者lock来处理,如果是分布式环境可以用分布式锁就可以了(分布式锁,可以用memcache的add, redis的setnx, zookeeper的添加节点操作)。

弊端:

  • 降低了系统的qps
  • 代码复杂性增加
  • 有出现死锁的可能性, 存在线程池阻塞的风险
  • “分布式缓存加锁”通常是一个反模式,如果持有锁的实例不稳定导致没及时释放,就会浪费这个锁,直到锁过期。

3)"提前"使用互斥锁(mutex key)

方法同使用互斥锁,但在value内部设置1个超时值(timeout1), timeout1比实际的缓存到期时间(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout并重新从数据库加载数据并设置到cache中。
弊端:

  • 同使用互斥锁

4)多级缓存

对于热点数据进行二级缓存,且过期时间错开,则请求基本不会直接击穿缓存层到达数据库。

四、缓存雪崩

1、概念

缓存雪崩的情况是说,当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了、大量热点缓存同时过期导致大量缓存击穿,会有大量的请求进来直接打到DB上面。

2、解决思路

缓存击穿中的解决思路在缓存雪崩同样适用,但是互斥锁在大量热点缓存同时过期时需要同时大量构建新的缓存,可能会造成服务器性能问题。

1)设置不同的失效时间

为了避免这些热点的数据集中失效,那么我们在设置缓存过期时间的时候,我们让他们失效的时间错开。比如在一个基础的时间上加上或者减去一个范围内的随机值。

2)redis高可用

优化redis性能,搭建集群

3)数据预热

可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

4)限流降级

限流算法

  • 计数
  • 滑动窗口
  • 令牌桶Token Bucket
  • 漏桶 leaky bucket

限流组件设置限流数值,超出的请求会走开发好的降级组件,返回配置好的默认值。

5)排队加锁

在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

Last modification:November 29, 2020
如果觉得我的文章对你有用,请随意赞赏