一 缓存穿透
缓存不起作用,每次请求都直接访问数据库,称为缓存穿透。
场景:大量请求的key不在缓存中。或者甚至大量请求的key不在数据库中,因此也无法写入缓存,下一次请求仍然会直接访问数据库。流量大时数据库可能会宕机。出现这种情况往往是遭到了非法攻击。
解决方案
1 接口检验:可以在最外层先做一层校验:用户鉴权、数据合法性校验等。
2 缓存空值:简单粗暴的方法。当访问缓存和DB都没有查询到值时,可以将请求的key和空值写进缓存,但是设置较短的过期时间,该时间需要根据产品特性来设置。
3 布隆过滤器:使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。
二 缓存击穿
在缓存中的热点key过期的一瞬间,有大量访问这个key的请求打进来,缓存不起作用,直接到达数据库,称为缓存击穿。
解决方案
1 热点数据不过期:简单粗暴。
2 自动更新:redis支持查询某个key剩余有效时间,所以我们只需要设定一个时间差,比如3分钟,请求的时候查询有效时间,如果小于设定值(3分钟),那么刷新这个key的有效时间,刷新这个操作可以使用异步实现。
缺点:如果在key失效的前三分钟内,没有任何请求,还是会存在缓存击穿的问题,所以这种方式不是特别推荐。
3 定时刷新:
一 写一个定时任务查询快要过期的key,更新数据,刷新有效时间。
缺点:比较消耗服务器性能。
二 利用延迟队列,将key存入缓存的同时发送一个延迟队列(按指定时间消费),时间小于缓存中key的过期时间,到了指定时间,消费者刷新key的有效时间再发送一个延迟队列,以此循环。
缺点:如果中间件宕机,还是会存在缓存击穿的问题。
4 加互斥锁(推荐):在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就进入阻塞,等到第一个线程将数据写入缓存后,其他线程从缓存中查询。具体操作:为数据库查询操作(添加缓存)加锁,所有线程都要阻塞等待第一个线程从数据库查询数据并存到缓存,即便是其他线程经过双重检验,可以再次从缓存中取数据,也要等待 锁 的释放。
它不需要额外的服务器开销(定时任务),也不需要额外的资源消耗(中间件),只是让线程串行。
首先:本地锁-JVM锁,比如synchronized锁/ReentrantReadWriteLock读写锁(尽管使用双重检验锁)。
本地锁在分布式场景下的问题:本地锁只能锁住当前服务的jvm进程,在分布式场景下,没法锁住所有服务的jvm进程,在某个微服务部署了多个服务时,可能会查询多次(次数不确定,与第一个请求将数据添加到缓存的时间有关)。
然后:Redis 分布式锁,加锁时要按 key 维度去加锁,占坑式抢锁,如果设置成功就执行逻辑,否则自旋等待锁的释放。
三 缓存雪崩
当缓存中有大面积的key在同一时间过期时,对于这些key的请求在这一瞬间都会直接到达数据库,造成雪崩。
解决方案
1 过期时间打散:设计随机过期时间,给原来过期时间加上一个随机值,使每一个key的过期时间分散开来,不会在同一时间全部失效。
2 热点数据不过期:将一些常用的数据设置成为永久有效。
3 加互斥锁:同缓存击穿。
四 布隆过滤器
使用场景:检索元素是否存在大集合中、去重。
注意:当布隆过滤器判断结果为存在时,有一定的误判概率。
结构:位数组+哈希函数
布隆过滤器的特点是:
判断不存在的key,一定不存在;判断存在的key,有可能不存在,误判的概率和哈希函数的个数以及位数组的长度有关。
优点是空间利用率和查询效率较高。
缺点是返回结果存在误判,存放的数据不易删除。
降低这种误判率的思路:
增加位数组的长度,增加哈希函数的个数,进而降低哈希冲突概率。
写入过程
将给定元素经过hash计算写入到位数组中。
假设有一个位数组和三个hash函数,当我们要写入值时,过程如下:
key1经过三个hash函数计算得到位数组的下标索引为:0,2,5;然后,位数组对应的位置置1.
key2经过三个hash函数计算得到位数组的下标索引为:1,3,6;然后,位数组对应的位置置1.
key3经过三个hash函数计算得到位数组的下标索引为:0,4,7;然后,位数组对应的位置置1.
完成了数据的写入。
查询过程
判断给定元素是否在集合中。
比如,查询key4是否在集合中,首先hash函数计算key4对应位数组的下标为7,8,9,然后检查是否都为1。
本例中,下标不全为1,因此,布隆过滤器判定key4 一定不在集合中。
再比如,查询key5是否在集合中,首先hash函数计算key5对应位数组的下标为1,3,5,然后检查是否都为1。
本例中,由于存入key1 key2时设置对应下标为1 3 5的比特位全为1,因此,布隆过滤器判定key5 在集合中。
但是显然key5并不在集合中。因此,布隆过滤器有一定的误判概率,判断存在的key,有可能不存在。
布隆过滤器实现
参考:布隆过滤器 - 有代码示例
1 java手动实现:
对于java而言,需要一个位数组、几个哈希函数、add方法、contains方法。
位数组可以使用BitSet,自定义Hash类,实现hash方法,定义Hash数组。
2 利用Google开源的Guava自带的布隆过滤器:
mvn中添加依赖,创建BloomFilter时可以设置存储元素的最大数量以及容忍误判的概率。
调用put方法、mightContains方法
缺点是只能单机使用,容量扩展也比较困难。
3 redis中的布隆过滤器:
dockerhub中搜rebloom, redislabs/rebloom是redis的布隆过滤器的module
创建布隆过滤器 BF.RESERVE,可以设置容错率、过滤器容量、容量扩展系数。
调用BF.ADD(BF.MADD)、 BF.EXISTS(BF.MEXISTS) , M代表multi.
参考:
带你搞明白什么是缓存穿透、缓存击穿、缓存雪崩
缓存穿透、缓存击穿、缓存雪崩解决方案 - 有代码示例