Redis作为最常用的缓存中间件,在使用过程中,必然会遇到过;**缓存穿透、缓存雪崩、缓存击穿。**这三个可以说是Redis缓存使用过程中,最常见的问题,且也是面试中最常闻到的问题。
1、缓存穿透
**缓存穿透:**是指查询了一个必然不存在的数据,缓存中和数据库中都不会存在的数据。由于缓存中没有缓存,所以每次请求都会请求到数据库。这就可能存在别人恶意攻击的风险,比如拿个数据库中必然不存在的id(-1,或者非常大的),每次请求都会请求到数据库。会导致数据库的负载增大,甚至打挂数据库。
解决方案
缓存空对象
缓存空对象:是指当在数据库中也查询不到这个对象时,在缓存中增加一个key-null的键值。
但是缓存空的对象会有两个问题:
- value为null,也会占用内存空间,由于对不存在的key,也做了缓存,可能会导致缓存中存在大量的不存在值的key,会占用大量的内存空间。可以采用设置缓存时间(一个较短的时间)的方式,让其自动剔除。
- 缓存和数据库可能存在一段时间的数据不一致的情况,也就是说在缓存中key的过期时间内,数据库中可能添加了这个数据,那么在此key过期这段时间内,就存在缓存和数据库不一致的情况。这种情况可以采用消息队列或者其他方式清掉缓存的空对象即可。
布隆过滤器拦截
在访问缓存和数据库之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。当收到一个key的请求时,先在布隆过滤器中判断是否存在这个key,如果不存这个key,则直接返回给客户端。如果存在可以进入缓存、数据库中。这种情况适用于数据相对固定的情况。
布隆过滤器实际上就是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否存在集合中。可以得知一个元素一定不存在,但是不能知道一个元素一定存在。布隆过滤器存在一定的失误率和删除困难的缺点。
2、缓存击穿
缓存击穿是指一个key在缓存中不存在,但是数据库中存在的情况。最常见的情况就是,一个key非常热门,在短时间有有很高的并发在请求。这时如果这个由于过期时间或者其他原因失效了,那么此时会有大量的请求会请求到数据库,可能会短时间压垮数据库。
解决方案
1、分布式互斥锁
只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完毕,重新从缓存中获取数据即可。set(key,value,timeout)
2、key永不过期
- 在物理层面不设置热点key的过期时间
- 从功能上,为每个value设置一个逻辑上的过期时间,当发现超过逻辑过期时间后,使用单独的线程去更新缓存。
3、两种方案对比
- 分布式互斥锁:这种方法实现简单,但是存在一定的隐患,如果查询数据库和重建缓存的时间过长,可能会存在死锁或者线程池阻塞的风险,高并发情况下吞吐量大大降低!但是这种方法能够较好的降低后端存储负载,并在数据一致性上做的比较好。
- **永不过期:**这种方案由于没有设置key的过期时间,其实也就不存在热点key的问题,但是会存在数据不一致的情况,且代码的复杂度会大大增大。
3、缓存雪崩
由于缓存承载了大量的请求,有效的保护了数据库。但是如果缓存由于某些原因(比如:机器宕机)或者大量的key由于超时时间相同在同一时刻失效(大批key失效/热点数据失效),大量请求直接请求到数据库中,数据库压力陡增导致系统雪崩。
解决方案
- 可以把缓存设置成高可用的,即使个别节点、个别机器宕机,依然可以继续提供服务。利用Sentinel和Redis Cluster。
- 采用多级缓存,本地缓存作为一级缓存,redis作为二级缓存,不同级别的缓存设置不同的过期时间,即使某级缓存失效了,还有其他级别缓存兜底。
- 给key的过期时间附上随机值,尽量让不同的key的过期时间不同。