缓存系统的三个问题

随着互联网系统发展的逐步完善,提高系统的qps,目前的绝大部分系统都增加了缓存机制从而避免请求过多的直接与数据库操作从而造成系统瓶颈,极大的提升了用户体验和系统稳定性。

缓存在带来性能提升的同时,本身也是存在一些问题的。

缓存雪崩

定义

缓存雪崩指缓存服务器重启或者大量缓存集中在某一个时间段内失效,给后端数据库造成瞬时的负载升高的压力,甚至压垮数据库的情况。

解决方案

线程互斥

只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据才可以,每个时刻只有一个线程在执行请求,减轻了db的压力,但缺点也很明显,降低了系统的qps。

交错失效时间

这种方法时间比较简单粗暴,既然在同一时间失效会造成请求过多雪崩,那我们错开不同的失效时间即可从一定长度上避免这种问题,在缓存进行失效时间设置的时候,从某个适当的值域中随机一个时间作为失效时间即可。

降级

针对于整体服务,整体负荷超出整体负载承受能力时,延迟或暂停非重要服务,例如日志收集服务,保证重要服务正常运行。

熔断

针对于单个服务,当某一服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施。直接关闭该服务(比较暴力)或者保证部分请求成功,另一部分直接返回失败(不占用服务资源),eg.如果5秒钟之内连续请求失败次数达到20次,那么触发熔断机制,过滤60%的请求。

限流

限流可以认为服务降级的一种,限流就是限制系统的访问量已达到保护系统的目的。

常见的限流算法有:计数器、漏桶和令牌桶算法。

计数器:

一个典型的应用场景就是规定在固定的时间内,只允许通过固定数量的请求,如规定在一分钟内只允许处理100个请求。

  • 基本思想:有一个counter。当一个请求到来的时候,首先判断现在是否是处于新的时间段里。如果是在新的时间段了,就可以把counter清0,然后对当前这个请求进行计数加一操作。假如没有处于新的时间段,则判断当前的counter的数量是否超过规定的数量,如100。超过了就丢弃请求,否则对计数器加一,允许该请求通过。

  • 存在问题:精度不够,请求分布不平均。当在时间的临界区的时候,可能会出现问题。例如,规定一分钟处理100个请求,第一秒就有100个请求,也会造成系统压力,后面59秒系统空闲。

漏筒算法:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴;
  • 如果桶是空的,则不需流出水滴;
  • 可以以任意速率流入水滴到漏桶;
  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

令牌桶算法:

一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。

  • 令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。
  • 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。
  • 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。
  • 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

缓存穿透

定义

缓存穿透指的是使用不存在的key进行大量的高并发查询,这导致缓存无法命中,每次请求都要穿透到后端数据库系统进行查询,使数据库压力过大,甚至使数据库服务被压死。

解决方案

我们通常将空值缓存起来,再次接收到同样的查询请求时,若命中缓存并且值为空,就会直接返回,不会透传到数据库,避免缓存穿透。当然,有时恶意袭击者可以猜到我们使用了这种方案,每次都会使用不同的参数来查询,这就需要我们对输入的参数进行过滤,例如,如果我们使用ID进行查询,则可以对ID的格式进行分析,如果不符合产生ID的规则,就直接拒绝,或者在ID上放入时间信息,根据时间信息判断ID是否合法,或者是否是我们曾经生成的ID,这样可以拦截一定的无效请求。

当然还有一种更常见的解决方案就是使用布隆过滤器

布隆过滤器

原理

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

假设我们现在获得了一个size=10000的布隆过滤器,分配给我们的其实是10000个二进制位,假设k=8,那么当新添加huyanshi这个元素时,通过8个不同的hash函数,可以拿到8个index值,假设为1,2300,222,11...,我们就将这8个index为下标的位置,置为1.

当我们想要检索huyanshi是否存在时,再次用8个hash函数获得8个index,如果这8个index的位置都为1,那么这个元素就很可能存在.如果其中有一位为0,则该元素一定不存在.

布隆过滤器的原理比较简单,但是主要难点是在如何设计一个长度合理的数组,以及如何设计多个哈希函数可以将数据比较好地映射到整个数组中。

如何选择哈希函数个数以及布隆过滤器长度

很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

如何选择适合业务的 k 和 m 值呢,这里直接贴一个公式:

m=(nlnp)/(ln2)2m = -(n * lnp) / (ln2)^2

k=(m/n)ln2k = (m /n) * ln2

k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率

缓存击穿

定义

缓存击穿指的是使用不存在的key进行大量的高并发查询,这导致缓存无法命中,每次请求都要击穿到后端数据库系统进行查询,使数据库压力过大,甚至使数据库服务被压死。这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key

解决方案

二级缓存

对于热点数据进行二级缓存,并对于不同级别的缓存设定不同的失效时间,则请求不会直接击穿缓存层到达数据库。

以上这些解决方案都已经有了比较成熟的第三方包可以调用。

参考文章:

https://juejin.im/post/5b604b9ef265da0f62639001

https://zhuanlan.zhihu.com/p/43263751