【Redis】编码解决商品查询的缓存穿透问题

一、核心思路

在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的

现在的逻辑中:如果这个数据不存在,我们不会返回404 ,还是会把空的数据写入到Redis中,并且将value设置为空,当再次发起查询时,就会命中缓存。但是并不是到这就结束了,既然将空值写到redis了,就会导致我们从redis中命中时,命中的就不一定是商铺信息了,还有可能是空值,因此命中后还需要对结果做判断。

判断这个value是否是null,如果是null,则是之前写入的数据,证明是缓存穿透数据,直接返回null即可;如果不是,则直接返回商品信息。

1653327124561


二、代码实现

RedisConstants.java

设置空值的有效期

public static final Long CACHE_NULL_TTL = 2L;

ShopServiceImpl.java

@Override
public Result queryById(Long id) {
    // 1.从redis查询商铺缓存,查询的时候需要选择一个数据结构,应该选哈希还是string呢?这里存的商铺肯定是一个对象,存储一个对象很多同学会说选哈希。
    // 没错,选哈希完全可以,因为之前演示的时候都是用哈希来演示,这里就用string来演示。
    // 店铺key的选择要确保唯一,现在传店铺,每一个店铺都要有一个唯一的key,店铺的id就是唯一的,因此在这里直接使用id作为key。当然我们需要一个前缀
    String key = RedisConstants.CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        // 3.存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    // 4.不存在,根据id查询数据库
    Shop shop = getById(id);
    // 5.查询数据库不存在,直接返回错误
    if (shop == null) {
        //\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
        // 将空值写入redis,并且有效期不能像真实数据那么长(30分钟)
        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);
}

这样一来,下次来查询的时候,如果还是查询这个空值,那么它其实就能够命中了,但是这个命中的不是店铺数据,而是字符串。

我们通过 StrUtil.is 判断的时候,只有字符串中真正有数据的时候才是true,剩下的 null、""、"\t\n(换行)",都属于false。

也就意味着只有在有商铺数据的情况下它才是true,"" 也是false。

image-20240526121201752

再往下,它就直接去查数据库了,那肯定不行,我们还需要再判断一下是否是空值。命中的如果是空值,那么我们还需要返回一个结果,而不能是直接查数据库。

完整代码如下,命中空值返回错误,命中数据就返回数据,什么都没命中null,就去查询数据库,这样就可以解决缓存穿透的一个问题了。

@Override
public Result queryById(Long id) {
    // 1.从redis查询商铺缓存,查询的时候需要选择一个数据结构,应该选哈希还是string呢?这里存的商铺肯定是一个对象,存储一个对象很多同学会说选哈希。
    // 没错,选哈希完全可以,因为之前演示的时候都是用哈希来演示,这里就用string来演示。
    // 店铺key的选择要确保唯一,现在传店铺,每一个店铺都要有一个唯一的key,店铺的id就是唯一的,因此在这里直接使用id作为key。当然我们需要一个前缀
    String key = RedisConstants.CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        // 3.存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    //\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
    // 判断命中的是否是空值
    if (shopJson != null) {
        // 返回一个错误信息
        return Result.fail("店铺信息不存在!")
    }
    //\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

    // 4.不存在,根据id查询数据库
    Shop shop = getById(id);
    // 5.查询数据库不存在,直接返回错误
    if (shop == null) {
        //\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
        // 将空值写入redis,并且有效期不能像真实数据那么长(30分钟)
        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);
}

三、代码实现

重启代码,当我们查询一个命中的数据,那肯定是没问题的

image-20240526121721612

那我们现在查询一个不存在的数据试一试,例如 0,返回店铺不存在

image-20240526121822462

返回控制台,可以发现有一个查询店铺的SQL语句的

image-20240526121905605

清空控制台,打开浏览器,再查询一次,再来查看控制台,可以发现没有查询,也就说明我们的请求根本没有到达数据库,也就是没有请求压力,这就是因为我们建立了空值在redis中。

image-20240526122020738

打开redis,可以发现redis中已经存储了两个了,其中 0 就是空,此时就解决了缓存穿透的问题了。

image-20240526122050818


四、总结

缓存穿透产生的原因是什么?

  • 用户请求的数据在缓存中和数据库中都不存在,这样的请求一定会到达我们的数据库。如果不断发起这样的请求,就会给数据库带来巨大压力

缓存穿透的解决方案有哪些?

  • 缓存null值(我们采用的方式)
  • 布隆过滤(采用一种哈希算法)

上面两种是我们之前提到过的,但是缓存穿透任然不仅仅靠这两种解决,这两种其实属于一种被动的方案:人家已经来穿透你,然后你想办法去弥补。事实上我们也可以主动采取一些措施去解决缓存穿透。

  • 增强id的复杂度,避免被猜测id规律,这样一来它就不太容易输入一些自己编的id了。
  • 当你id有复杂度,有一定的规律,就可以加强基础的这种参数格式的校验了
  • 加强用户权限校验,一些业务并不是任何人都能去访问我,例如有些功能需要先登录,登录后我们还可以对用户做限流,这样的用户访问我们的时候它有一个什么样频率的限制
  • 做好热点参数的限流,例如商品访问很多、空值等也可以做限流(SpringCloud的里面的)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值