Java架构直通车:Redis缓存穿透/击穿/雪崩

缓存穿透

在高并发下,查询一个不存在的值时,缓存不会被命中,导致大量请求直接落到数据库上。
比如下面的代码:
首先查询缓存,如果没有该缓存,查询数据库,然后再放入缓存。

String categoryVOSStr=redisOperator.get("subCat:"+rootCatId);
if (StringUtils.isBlank(categoryVOSStr)){
       categoryVOS=categoryService.getSubCatList(rootCatId);
       if (categoryVOS!=null&&categoryVOS.size()!=0)
             redisOperator.set("subCat:"+rootCatId,JsonUtils.objectToJson(categoryVOS));
 }

如果现在查询的rootCatId在数据库中有,比如说rootCatId为1,那么之后再次查询的时候,就会在缓存中查找key为:subCat:1的缓存。

不过如果查询的rootCatId在数据库中不存在,比如非法用户进行攻击,大量请求会请求rootCatId为999,此时数据库中又没有999这个Id,那么缓存也不会产生,大量的请求会直接打在db上,造成宕机,从而影响整个系统。 这种现象就叫做缓存穿透。

缓存穿透解决方案

  • 方案1:缓存空数据

    缓存穿透也比较好解决,最简单的方法如下:
           把空的数据也缓存起来,比如空字符串,空对象,空数组或者list。(可以给这些空数据设定一个过期时间。)
    String categoryVOSStr=redisOperator.get("subCat:"+rootCatId);
    if (StringUtils.isBlank(categoryVOSStr)){
          categoryVOS=categoryService.getSubCatList(rootCatId);
          redisOperator.set("subCat:"+rootCatId,JsonUtils.objectToJson(categoryVOS));
    }
    

    这样做的缺陷是:
    如果缓存的空数据过多,内存可能吃不消,黑客也可能根据这一点来攻击服务器。

  • 方案2:布隆过滤器

    第二种避免缓存穿透的方式即为使用BloomFilter。将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
    它需要在缓存之前再加一道屏障,里面存储目前数据库中存在的所有key,如下图所示:

å¨è¿éæå¥å¾çæè¿°

布隆过滤器的原理和15. 检验毒药差不多:
检验毒药使用位运算,每一位用于一只小鼠,所以如果只有3只小鼠,能用于2的3次方个瓶子的检验。

布隆过滤器也是这样运算的,不过中间做了一个Hash运算(Hash可能冲突),所以布隆过滤器不是百分百可信的,

布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:

å¨è¿éæå¥å¾çæè¿°

Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:

å¨è¿éæå¥å¾çæè¿°

  • 方案3:使用互斥锁排队

实际上就是加锁后,做限流或者排队了。(在缓存击穿中也常用)。

可以参考:使用双重检验锁

缓存击穿

在高并发下,对一个特定的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,导致大量请求直接落到数据库上。

  1. 缓存永不过期

        最简单的方法当然是缓存永远不过期,这样会产生一个问题,当缓存占用内存过大后,也会触发缓存的淘汰机制。
     
  2. 限流或者加锁
     

    常用方法之一是限流,常见的限流算法有滑动窗口,令牌桶算法和漏桶算法,或者直接使用队列、加锁等,在layering-cache里面我主要使用分布式锁来做限流。

    layering-cache:
            采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。 请求优先从 L1 缓存获取数据,如果 L1缓存未命中则加锁,只有 1 个线程获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。
    这种方式,主要是通过避免缓存同时失效并结合锁机制实现。所以,当数据更新时,只能淘汰 L1 缓存,不能同时将 L1 和 L2 中的缓存同时淘汰。L2 缓存中可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案可能会造成额外的缓存空间浪费。

    或者使用redis或者zookeep提供的互斥锁可以解决击穿的问题。

缓存雪崩

在高并发下,大量的缓存key在同一时间失效,导致大量的请求落到数据库上。
缓存雪崩一般可以采用:

  • 缓存永不过期
  • 过期时间错开:可以采用一个随机时间来设置过期时间。
  • 多缓存结合:不一定只使用redis缓存,可以再使用其他的比如memcache等等。
  • 缓存预加载。

这里说下layering-cache的缓存预加载:
       在 layering-cache里面二级缓存会配置两个时间,expireTime是缓存的过期时间,preloadTime 是缓存的刷新时间(预加载时间)。每次二级缓存被命中都会去检查缓存的过期时间是否小于刷新时间,如果小于就会开启一个异步线程预先去更新缓存,并将新的值放到缓存中,有效的保证了热点数据"永不过期"。这里预先更新缓存也是需要加锁的,并不是所有的线程都会落到库上刷新缓存,如果没有获取到锁就直接结束当前线程。

       在缓存总量和并发量都很大的时候,这个时候缓存如果同时失效,缓存预热将是一个非常慢长的过程,就比如说服务重启或新上线一个新的缓存。这个时候我们可以采用切流的方式,让缓存慢慢预热,如开始切10%流量,观察没有异常后,再切30%流量,观察没有异常后,再切60%流量,然后全量。这种方式虽然有点繁琐,但是一旦遇到异常我们可以快速的切回流量,让风险可控。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值