缓存更新策略

内存淘汰超时剔除主动更新
redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是:

业务场景

  • 低一致性需求:使用内存淘汰
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案

主动更新策略

用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,有如下几种解决方案

  • Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案

  • Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理

  • Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

问题考虑

  • 删除缓存还是更新缓存?

更新缓存:每次更新数据库都更新缓存,假如更新数据库100次,缓存也更新100次,但期间没人查询,会导致无效写操作较多
删除缓存:更新数据库时让缓存失效,查询时再更新缓存

  • 如何保证缓存与数据库的操作的同时成功或失败?

单体系统,将缓存与数据库操作放在一个事务
分布式系统,利用TCC等分布式事务方案

  • 先操作缓存还是先操作数据库?

先删除缓存,再操作数据库
先操作数据库,再删除缓存

在这里插入图片描述

数据库20和缓存10数据不一致了,这种出行问题可能性较大

在这里插入图片描述

缓存10和数据库20出现了不一致,这种出现问题可能较小,因为写缓存时间非常短,这期间出现一个并发的线程插入可能较小。


难题

1.缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

解决方案:

  • 缓存空对象(会占空间)
    在这里插入图片描述

  • 布隆过滤(实现复杂)
    在这里插入图片描述


2.缓存击穿

在这里插入图片描述

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。(秒杀抢卷业务)

常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

互斥锁

在这里插入图片描述

保证一致性,但可能死锁,性能受影响

逻辑过期

在这里插入图片描述

无需等待,但需额外内存消耗,不能保证一致性


3.缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

商品查询缓存实例

解决缓存穿透

public <R,ID> R queryWithPassThrough(
        String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(json)) {
        // 3.存在且字符串不为空,直接返回
        return JSONUtil.toBean(json, type);
    }
    // 判断命中的是否是空值,如果为空值证明缓存和数据库都没有该店铺信息,这就是缓存穿透的第一种解决办法
    if (json != null) {
        // 返回一个错误信息
        return null;
    }

    // 4.缓存不存在,根据id查询数据库
    R r = dbFallback.apply(id);
    // 5.不存在,返回错误
    if (r == null) {
        // 将空值写入redis
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        // 返回错误信息
        return null;
    }
    // 6.存在,写入redis
    this.set(key, r, time, unit);
    return r;
}

解决缓存击穿

互斥锁(这里还要考虑了缓存穿透问题)

public <R, ID> R queryWithMutex(
        String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        // 3.缓存存在(命中)且不为空值,直接返回
        return JSONUtil.toBean(shopJson, type);
    }
    // 判断命中的是否是空值,空值代表就是缓存和数据库都没有该店铺信息了
    if (shopJson != null) {
        // 返回一个错误信息
        return null;
    }

    // 4.实现缓存重建,防止缓存击穿问题
    // 4.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    R r = null;
    try {
        boolean isLock = tryLock(lockKey);
        // 4.2.判断是否获取成功
        if (!isLock) {
            // 4.3.获取锁失败,休眠并重试
            Thread.sleep(50);
            // 自旋
            return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
        }
        // 4.4.获取锁成功,根据id查询数据库
        r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis,防止缓存穿透
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }finally {
        // 7.释放锁
        unlock(lockKey);
    }
    // 8.返回
    return r;
}

逻辑时间

因为缓存不存在过期,只有逻辑过期(查不到热点,证明数据库也没有了),所以直接返回空,解决了缓存穿透问题,就不用考虑了

public <R, ID> R queryWithLogicalExpire(
        String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isBlank(json)) {
        // 3.存在,直接返回
        return null;
    }
    // 4.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5.判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())) {
        // 5.1.未过期,直接返回店铺信息
        return r;
    }
    // 5.2.已过期,需要缓存重建
    // 6.缓存重建
    // 6.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    // 6.2.判断是否获取锁成功
    if (isLock){
        // 6.3.成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                // 查询数据库
                R newR = dbFallback.apply(id);
                // 重建缓存
                this.setWithLogicalExpire(key, newR, time, unit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }finally {
                // 释放锁
                unlock(lockKey);
            }
        });
    }
    // 6.4.返回过期的商铺信息
    return r;
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐