《Redis 缓存异常与更新策略:穿透、雪崩和击穿》文章封面
技术笔记约 10 分钟

Redis 缓存异常与更新策略:穿透、雪崩和击穿

梳理缓存穿透、缓存雪崩与缓存击穿的形成原因,并比较空值缓存、互斥锁和逻辑过期等方案。

#Redis#缓存#Java
文章目录0%
  1. 01缓存穿透
  2. 02什么是缓存穿透
  3. 03为什么会有缓存穿透
  4. 04解决方案
  5. 05最佳实践
  6. 06缓存雪崩
  7. 07什么是缓存雪崩
  8. 08解决方案
  9. 09缓存击穿
  10. 10什么是缓存击穿
  11. 11解决方案

昨天学习了Redis中的缓存穿透、缓存雪崩、缓存击穿。趁热打铁,总结一下。

缓存穿透

什么是缓存穿透

当客户端进行请求访问时,用户所带的数据(非法数据)在数据库中和Redis中都不存在,这些请求都会到达数据库,导致数据库瘫痪。

为什么会有缓存穿透

  • 普通用户阴差阳错地请求
  • 攻击者故意构造大量不存在的key进行攻击

解决方案

1.缓存空数据

  • 做法:当数据库查询发现没有这个数据时,直接向Redis中存入一个““,并设置较短的有效期(2-5分钟)。

  • 示例

    # 解决缓存穿透:缓存空对象,设置有效期
    stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
    #判断是否为空值
    if (shopJson=="") {
       return null;
    }

2.布隆过滤器

  • 做法:将所有存在的 key 预先存入布隆过滤器。查询时先过布隆过滤器,不存在的直接拦截返回

3.参数校验

  • 做法:在请求入口处进行非法的参数校验

最佳实践

请求 → 参数校验(拦截非法参数) → 布隆过滤器(拦截不存在的 key) → 缓存查询(存在则返回) → 数据库查询 → 不存在则缓存空值

三道防线层层递进,各司其职。

缓存雪崩

什么是缓存雪崩

同一时段内,大量的key同时失效或者Redis宕机,所有请求涌入数据库,带来巨大压力。

解决方案

  • 给key的有效期添加随机值
  • 利用Redis集群提高Redis的可用性(未学)
  • 给缓存业务添加降级限流策略(未学)
  • 给业务添加多级缓存(未学)

缓存击穿

什么是缓存击穿

当某一个热点key(请求Redis比较多的key)有效期失效,此时所有请求涌向数据库,给服务器带来压力。

解决方案

互斥锁

  • 做法:当缓存没有命中时,建立互斥锁,此时查询数据库获取数据,进行缓存重建并写入缓存,之后释放锁,建立锁之后其他线程在查询缓存发现未命中会尝试建立锁,但由于已经上一个线程已经建立了无法建立,只能等待后再次重试,直接Redis命中结束。
  • 示例
String lockKey = "lock:shop:"+id;
Shop shop = null;
try {
            boolean isLock = tryLock(lockKey);
            //是否获取成功
            if (!isLock){
                //获取失败休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //获取成功

            //不存在需查询数据库
            //模拟重建延迟
            Thread.sleep(200);
            shop = getById(id);
            //数据库不存在直接返回404错误
            if (shop==null){
                //解决缓存穿透:缓存空对象,设置有效期
               stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //数据库存在写入Redis然后返回
            //将Java对象转化为json
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (InterruptedException e) {

            throw new RuntimeException(e);
        } finally {
            //释放互斥锁
            unlock(lockKey);
        }
  • 优点:由于缓存重建时其他线程只能等待,所以能够保持数据的一致性

  • 缺点

​ 1.线程需要等待,会占用服务器资源

​ 2.可能有死锁的风险

逻辑过期

  • 做法:在缓存中添加一个过期时间字段,该key不设置有效时间,当有线程来访问Redis一定命中,这时仅仅需要判断逻辑时间是否过期,若过期,会尝试获取互斥锁,获取成功后会另开一个线程由这个线程去重新请求数据库进行缓存重建,之后释放锁。原有线程返回旧数据即可。若这时有其他线程来访问,也会尝试获取互斥锁,获取失败会发现别的线程已经进行了缓存重建,此线程仅仅返回旧数据即可。
  • 示例
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject jsonObject = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(jsonObject, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //发现未过期,直接返回即可
            return shop;
        }
        //过期了,缓存重建
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        //获取互斥锁
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            //成功,开始独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unlock(lockKey);
                }

            });
        }