缓存穿透、雪崩、击穿的概念与解决方案(附代码)

缓存穿透

  • 指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远都不会生效,这些请求都会打到数据库
  • 一般都是不怀好意的人故意对系统进行攻击引发的缓存穿透
  • 常见解决方案一:当数据库查询不存在时缓存一个空对象。这样会造成额外的内存消耗,可以通过设置 TTL 时间缓解
  • 常见解决方案二:布隆过滤器。内存占用较少,但实现复杂,且存在误判可能
  • 上述两个都是被动方案,我们也可以主动的防止被缓存穿透
    • 增加 id 复杂度,避免被猜中,然后进行 id 的校验,不满足条件直接 pass
    • 加强用户的权限校验,限制用户访问接口的次数
    • 做好热点参数的限流

通过空对象方式解决商铺信息缓存穿透问题

/**
 * 根据id查询商铺信息
 *
 * @param id 商铺id
 * @return 商铺详情数据
 */
@Override
public Result queryById(Long id) {
    // 1. 查询 redis 商铺缓存
    String redisShopKey = "cache:shop" + id;
    String shopJson = stringRedisTemplate.opsForValue().get(redisShopKey);
    // 2. 查到了,直接返回
    if (StrUtil.isNotBlank(shopJson)) {

        return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
    }
    // 3. 查不到
    // 3.1 如果查不到时并不是 null ,说明是我们缓存的空值,直接返回错误信息
    if (null != shopJson) {
        return Result.fail("店铺不存在!");
    }
    // 3.2 查不到时是 null ,继续查询数据库
    Shop shop = shopMapper.selectById(id);
    // 4. 查不到
    if (shop == null) {
        // 4.1 将空值写入 redis
        stringRedisTemplate.opsForValue().set(redisShopKey, "", 1L, TimeUnit.MINUTES);

        // 4.2 返回错误信息
        return Result.fail("店铺不存在!");
    }
    // 5. 查到了
    // 5.1 写入 redis
    stringRedisTemplate.opsForValue().set(redisShopKey, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);

    // 5.2 返回数据
    return Result.ok(shop);
}

缓存雪崩

  • 是指在同一时段内大量的缓存同时失效或 Redis 服务宕机,导致大量请求直接到达数据库,带来巨大压力
  • 常见解决方案一:对热点数据进行缓存预热,并给不同的 key 设置 TTL 随机值,避免大量缓存同时失效
  • 常见解决方案二:利用 Redis 集群提高服务的可用性

缓存击穿

  • 也叫热点 Key 问题。指的是一个被高并发访问的 key 由于各种原因(数据更新、过期等)被删除了,在缓存重建期间无数请求瞬间给数据库带来巨大的冲击
  • 常见解决方案一:提前对热点数据进行缓存,并设置永不过期
  • 常见解决方案二:互斥锁。保证了一致性,但线程需要等待,牺牲了一定的可用性
    互斥锁解决缓存击穿问题
    通过互斥锁方式解决商铺信息缓存击穿问题
    在这里插入图片描述
  • 上面的流程图不全,我们需要在获取锁之后从 Redis 中查询商铺缓存并判断是否命中,未命中再继续,命中直接返回
  • 我们需要根据获取锁与否做不同的操作,java 的锁在获取不到的情况下只会等待,所以我们需要其他方式实现互斥锁
  • 互斥锁说白了就是只有一个线程可以获取到,其他线程在释放锁之前只能等待,我们可以利用 Redis 的 setnx 实现这种效果
  • setnx 就是当 key 不存在时才能插入,我们随意定义一个 key ,获取锁就相当于第一个插入该 key ,锁的释放就是删除该 key
/**
 * 基于 setnx 实现互斥锁
 *
 * @return 是否获取到互斥锁
 */
private boolean getLock(String key) {
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "", 5, TimeUnit.SECONDS);
    // 这里不能直接返回,会自动拆箱,如果为 null 会报空指针异常
    return BooleanUtil.isTrue(aBoolean);
}

/**
 * 释放 setnx 锁
 */
private void unLock(String key) {
    stringRedisTemplate.delete(key);
}

/**
 * 根据id查询商铺信息
 *
 * @param id 商铺id
 * @return 商铺详情数据
 */
@Override
public Result queryById(Long id) {
    // 1. 查询 redis 商铺缓存
    String redisShopKey = "cache:shop" + id;
    String shopJson = stringRedisTemplate.opsForValue().get(redisShopKey);
    // 2. 查到了,直接返回
    if (StrUtil.isNotBlank(shopJson)) {

        return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
    }
    // 3. 查不到
    // 3.1 如果查不到时并不是 null ,说明是我们缓存的空值,直接返回错误信息
    if (null != shopJson) {
        return Result.fail("店铺不存在!");
    }
    // 3.2 查不到时是 null ,尝试获取互斥锁
    String lockKey = "lock:shop:" + id;
    Shop shop;
    try {
        boolean isLock = getLock(lockKey);
        // 3.3 如果没获取到,则休眠 50ms 并重试
        if (!isLock) {
            Thread.sleep(50);
            queryById(id);
        }
        // 3.4 获取到
        // 3.4.1 查询 redis 商铺缓存,查到了直接返回
        if (StrUtil.isNotBlank(stringRedisTemplate.opsForValue().get(redisShopKey))) {

            return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
        }
        // 3.4.2 没查到,继续查询数据库
        shop = shopMapper.selectById(id);
        // 4. 查不到
        if (shop == null) {
            // 4.1 将空值写入 redis
            stringRedisTemplate.opsForValue().set(redisShopKey, "", 1L, TimeUnit.MINUTES);

            // 4.2 返回错误信息
            return Result.fail("店铺不存在!");
        }
        // 5. 查到了,写入 redis
        stringRedisTemplate.opsForValue().set(redisShopKey, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        // 6. 释放锁(释放资源的操作要放入 finally)
        unLock(lockKey);
    }

    // 7. 返回数据
    return Result.ok(shop);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值