Redis缓存穿透的解决思路

192 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

1.什么是缓存穿透

缓存穿透是指客户端发送的请求在redis缓存和数据库中都无法查询到数据,如果有大量请求到来,那么redis无法被命中的情况下,大量的请求将会访问数据库,造成数据库崩溃。值得注意的是,这时的redis还是好好的

缓存穿透的条件总结如下:

  1. redis和数据库不存在请求所需的数据。
  2. 大量请求持续访问数据库,造成服务器压力增大。

2.解决缓存穿透的思路

  1. 对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
  2. 设置可访问的名单(白名单):使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问
  3. 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
  4. 进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

3.实践

下面的代码实践是演示缓存空值来应对Redis缓存穿透的问题。 以往我们查询商铺信息,并将信息缓存到redis的代码业务逻辑如下图所示:

image.png 现在,我们要对我们的业务逻辑做一点小修改。我们发现,以往我们在数据库中查询得到数据,就将数据缓存到redis中,并将数据返回给用户,如果查询不到数据,就给用户返回异常。而避免缓存穿透的重点在于避免大量数据直接访问数据库,为此我们业务逻辑修改后,如下图所示:

image.png

说明:

  1. 大家可以先看注释5,当数据库不存在该数据的时候,我们将空值保存到缓存中,而这个空值不是直接写java的null,我们这里是用一个空字符串,即""。CACHE_NULL_TTL是我们自定义的常量,是过期时间。毕竟缓存的是空值,除了避免缓存穿透外,没有其他太大的意义。给它一个缓存有效期,那么后期没有人访问的时候,将这个空值缓存剔除。
  2. 注释2那里已经判断了从redis中是否有获取数据了,为什么后面还要再有一个if判断?StrUtil.isNotBlank判断为true的标准是StrUtil.isNotBlank("abc")。像StrUtil.isNotBlank(""),StrUtil.isNotBlank(null),StrUtil.isNotBlank("\t\n")都是false。所以如果注释2没执行的话,说明redis里面缓存的可能是空值"",我们在再进一步判断是否等于null。如果你从redis能取出东西,但是又无法返回,而且不等于null,那么说明你取得的是空值,再这里就可以返回了,不然你还会去查询数据库。
  3. 缓存穿透代码的逻辑重点在于当数据库查询不到数据的时候,将空值存进redis后,当有请求再来的时候,要判断是否拿到了空值,拿到了空值就返回,不要再去查询数据库了。
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
    return shopService.queryById(id);
}
@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public Result queryById(Long id) {
    //String key = "cache:shop:" + id;
    String key = RedisConstants.CACHE_SHOP_KEY + id;
    //1.从redis查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        //3.redis存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    //判断命中的是否是redis缓存的空值
    if (shopJson != null){
        //返回错误信息
        return Result.fail("店铺信息不存在!");
    }
    //4.redis不存在,根据id查询数据库
    Shop shop = getById(id);
    //5.数据库不存在,返回错误
    if (shop == null){
        //将空值写入redis
        stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
        return Result.fail("店铺不存在");
    }
    //6.数据库存在,写入redis
    stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
    //7.返回
    return Result.ok(shop);
}