缓存常见的三个问题:缓存雪崩、缓存击穿、缓存穿透
这三个问题一旦发生,会导致大量的请求积压到数据库层。如果请求的并发量很大,就会导致数据库宕机或是故障。
缓存雪崩
- 缓存中有大量数据同时过期,导致大量请求无法得到处理,请求直接打到数据库,导致数据库压力激增
- Redis 缓存实例发生故障宕机了,无法处理请求,请求全部打到数据库
解决方案
- 更新策略在时间上做到比较均匀,比如可以在过期时间上加个随机数,避免大量数据同时过期
- 多台机器做主从复制或者多副本,实现高可用
- 热点数据尽量分散到不同的机器上
- 实现熔断、降级、限流机制,对系统进行负载能力控制
缓存击穿
某个访问特别频繁的热点数据,在某一时刻缓存失效了,这时候刚好有大量的并发请求访问这个数据,由于缓存不存在,请求全部打到数据库了,导致数据库压力激增
解决方案
- 启动一个线程定时的更新缓存的数据
- 给热点数据做过期时间续期操作,避免这些数据失效
- 热点数据不设置过期时间,这种做法感觉有点极端
- 增加互斥锁,一个线程请求到数据之后拿到锁,其他线程的请求必须等待这个请求处理完才能继续,这种方案其实会降低吞吐量。
缓存穿透
大量并发查询缓存中不存在的 key(可能是数据库和缓存的数据被删除了,也有可能是专门的恶意攻击),导致都直接将压力透传到数据库
解决方案
- 缓存空值或者缺省值,这样第一次不存在也会被加载会记录,下次拿到有这个 key
- 布隆过滤器或 RoaringBitmap 判断数据是否存在,避免大量请求访问不存在的 key
- 前端进行请求检测,把恶意的请求(例如请求参数不合理、请求参数是非法值、请求字段不存在)直接过滤掉
布隆过滤器
布隆过滤器由一个初值都为 0 的 bit 数组和 N 个哈希函数组成,可以用来快速判断某个数据是否存在。当我们想标记某个数据存在时(例如,数据已被写入数据库),布隆过滤器会通过三个操作完成标记:
- 首先,使用 N 个哈希函数,分别计算这个数据的哈希值,得到 N 个哈希值
- 然后,我们把这 N 个哈希值对 bit 数组的长度取模,得到每个哈希值在数组中的对应位置
- 最后,我们把对应位置的 bit 位设置为 1,这就完成了在布隆过滤器中标记数据的操作
当需要查询某个数据时,我们就执行刚刚说的计算过程,先得到这个数据在 bit 数组中对应的 N 个位置。紧接着,我们查看 bit 数组中这 N 个位置上的 bit 值。只要这 N 个 bit 值有一个不为 1,这就表明布隆过滤器没有对该数据做过标记,所以,查询的数据没有在数据库中保存。
当然,布隆过滤器也会有误判:
- 由于采用固定bit的数组,使用多个哈希函数映射到多个bit上,有可能会导致两个不同的值都映射到相同的一组bit上。虽然有误判,但对于业务没有影响,无非就是还存在一些穿透而已,但整体上已经过滤了大多数无效穿透请求。
- 误判本质是因为哈希冲突,降低误判的方法是增加哈希函数 + 扩大整个bit数组的长度,但增加哈希函数意味着影响性能,扩大数组长度意味着空间占用变大,所以使用布隆过滤器,需要在误判率和性能、空间作一个平衡。但我们在使用开源的布隆过滤器时比较简单,通常会提供2个参数:预估存入的数据量大小、要求的误判率,输入这些参数后,布隆过滤器会有自动计算出最佳的哈希函数数量和数组占用的空间大小,直接使用即可。
使用方式:
- 布隆过滤器可以放在缓存和数据库的最前面:把Redis当作布隆过滤器时(4.0提供了布隆过滤器模块,4.0以下需要引入第三方库),当用户产生业务数据写入缓存和数据库后,同时也写入布隆过滤器,之后当用户访问自己的业务数据时,先检查布隆过滤器,如果过滤器不存在,就不需要查询缓存和数据库了,可以同时降低缓存和数据库的压力。
- 一个极端情况下,如果整个bit数组都是1或者大部分都是1的场景,这说明布隆过期已经基本被填满了,也说明超出了布隆过滤器一开始预期的大小,所以布隆过滤器是需要事先预知总容量大小与误判率预期的,否则就会出现误判率极高,基本等于没有作用的情况