缓存、缓存更新策略、主动更新策略、缓存穿透、缓存雪崩、缓存击穿(热点key问题)

缓存

  • 缓存是数据交换的缓冲区(cache),是数据存储临时的地方,一般读写性能较高
  • 好处
    • 降低后端负载
    • 提高读写效率,降低响应时间
  • 成本
    • 一致性成本
    • 代码维护(为了一致性)
    • 运维成本(缓存一般是集群的)

redis缓存

在这里插入图片描述

  • redis缓存执行流程
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        String key = "cache:shop:"+id;
        //1、从Redis查询id
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断redis是否存在数据
        if(StrUtil.isNotBlank(shopJson)) {
            //3、存在
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //4、不存在
        Shop shop = getById(id);
        //5、存在返回错误
        if(shop == null){
            return Result.fail("店铺不存在");
        }
        //6、存在写入reids
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
        //7、返回
        return Result.ok(shop);
    }
}

缓存更新策略

  • 缓存更新策略是为了解决缓存不足的问题
    • 内存淘汰:redis的内存淘汰机制,当内存不足自动淘汰部分数据,下次查询时更新缓存
      • 一致性差、无维护成本
    • 超时删除:给缓存数据添加过期时间,到期删除缓存数据,下次查询更新缓存
      • 一致性一般,维护成本低
    • 主动更新:编写业务逻辑,在修改数据库的同时,更新缓存
      • 一致性好
  • 场景
    • 低一致性需求:使用内存淘汰机制,基本不会改变的数据
    • 高一致性需求:主动更新,经常改变的数据

主动更新策略

  • 缓存调用者,在更新数据库的同时更新缓存(推荐)

    • 删除缓存与更新缓存

      • 删除缓存:每次更新数据库时让缓存失效,查询时在更新缓存(推荐)
      • 更新缓存:每次更新数据库都更新缓存,无效写操作多
    • 保证缓存与数据库操作同时成功或者失败

      • 单体系统,将缓存与数据库操作放在一个事务
      • 分布式系统:利用TCC等分布式事务方案
    • 先删除缓存,在更新数据库、先更新据库,在删除缓存

      • 都会造成一致性问题,后者概率低,需要加锁也就是上面说的事务

  • 缓存与数据库合为一个服务,有服务来维护一致性,调用者调用该服务,无需关心缓存一致性问题(实现起来复杂)

  • 调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库(实现起来复杂)

缓存穿透

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

  • 缓存空对象
  • 布隆过滤
  • 增强id复杂度
  • 做好数据格式校验
  • 加强用户格式校验
缓存穿透现象
  • 客户端访问了一个数据库不存在的数据

在这里插入图片描述

缓存空对象处理缓存穿透
  • 当数据没查到数据时,我们将null数据设置到缓存中,并设置过期时间
  • 好处
    • 实现简单,维护方便
  • 缺点
    • 额外内存消耗
    • 可能造成短期不一致(突然插入该数据),解决就是将插入的数据存入缓存

在这里插入图片描述

布隆过过滤器处理缓存穿透
  • 布隆过滤器通过算法,将数据库的数据二进制位(占用较少空间)存在过滤器中
    • 存在:数据库一定存在该数据
    • 不存在:数据库可能存在、也可能不存在该数据
  • 好处
    • 内存占用少
  • 缺点
    • 实现复杂

在这里插入图片描述

缓存雪崩

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

  • 给不同的key的过期时间添加随机值
  • 利用redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

在这里插入图片描述

缓存击穿

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务的key突然消失了,无数请求访问会在瞬间给数据库带来巨大的压力

  • 缓存击穿之后,同一时间节点,可能有多个线程都在重建缓存,解决方案

    • 互斥锁:发现有线程在重建缓存,新来线程休眠一会在获取锁

      • 优点

        • 没有额外内存消耗
        • 保证一致性
        • 实现简单
      • 缺点

        • 线程需要等待,性能受影响
        • 可能有死锁的风险

    • 逻辑过期:不给key设置过期时间,在value设置一个过期时间(逻辑时间),如果发现时间已经过期,当前线程会返回旧的数据,然后开启一个新的线程去重建缓存,在缓存没有重建后之前,别的线程访问也是旧的缓存数据

      • 优点
        • 线程无需等待,性能较好
      • 缺点
        • 不能保证一致性
        • 有额外内存消耗
        • 实现负复杂

缓存穿透实现过程

  • 1、从Redis查询id
  • 2、判断redis是否存在数据
  • 3、存在,返回数据给客户端
  • 4、不存在,查询数据库
  • 5、存在返回错误
  • 6、存在写入reids
    private Shop queryWithPassThrough(Long id) {
        String key = "cache:shop:"+id;
        //1、从Redis查询id
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断redis是否存在数据
        if(StrUtil.isNotBlank(shopJson)) {
            //3、存在
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //4、不存在
        Shop shop = getById(id);
        //5、存在返回错误
        if(shop == null){
            return null;
        }
        //6、存在写入reids
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
        //7、返回
        return shop;
    }

互斥锁实现缓存击穿过程

  • 1、从Redis查询id
  • 2、判断redis是否存在数据
  • 3、存在,返回数据给客户端
  • 4、不存在,实现缓存重建
    • 4、1 获取互斥锁
    • 4、2 判断是否获取成功
    • 4、3 失败,休眠并重试
  • 5、存在返回错误
  • 6、存在写入reids
  • 7、释放互斥锁
  • 8、返回数据给客户端
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        //缓存穿透
        //Shop shop = queryWithPassThrough(id);

        //互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id);
        if(shop == null){
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    public Shop queryWithMutex(Long id){
        String key = "cache:shop:"+ id;
        //1、从Redis查询id
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断redis是否存在数据
        if(StrUtil.isNotBlank(shopJson)) {
            //3、存在
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //4、不存在,实现缓存重建
        //4、1 获取互斥锁
        String lockKey = "lock:shop:"+id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            //4、2 判断是否获取成功
            if (!isLock) {
                //4、3 失败,休眠并重试
                Thread.sleep(50);
                queryWithPassThrough(id);
            }
            //4、4 成功,根据id查询数据库
            shop = getById(id);
            //模拟创建延时
            Thread.sleep(200);
            //5、存在返回错误
            if (shop == null) {
                return null;
            }
            //6、存在写入reids
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }finally {
            //7、释放互斥锁
            unLock(lockKey);
        }
        //8、返回
        return shop;
    }

    private Shop queryWithPassThrough(Long id) {
        String key = "cache:shop:"+id;
        //1、从Redis查询id
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断redis是否存在数据
        if(StrUtil.isNotBlank(shopJson)) {
            //3、存在
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //4、不存在
        Shop shop = getById(id);
        //5、存在返回错误
        if(shop == null){
            return null;
        }
        //6、存在写入reids
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
        //7、返回
        return shop;
    }

    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }
}

逻辑过期时间实现缓存击穿过程

  • 需要我们在项目启动前手动设置热点key
  • 1、从Redis查询id
  • 2、判断redis是否存在数据
  • 3、未命中,返回null
  • 4、命中,将json反序列化未对象
  • 5、判断是否过期
    • 5.1、未过期,返回数据
    • 5.2、已过期,需要缓存重建
  • 6、缓存重建
    • 6、1 获取互斥锁
    • 6、2 判断是否获取锁成功
    • 6、3 成功,开启独立线程,实现缓存重建
    • 6、4 失败,返回过期商品信息
//逻辑过期时间工具类
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    private  void saveShop2Redis(Long id, long expireSeconds) throws InterruptedException {
        //1、查询店铺数据
        Shop shop = getById(id);
        //模拟延迟
        Thread.sleep(200);
        //2、封装过期时间逻辑
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //3、写入redis
        stringRedisTemplate.opsForValue().set("cache:shop:"+id, JSONUtil.toJsonStr(redisData));
    }

    private Shop queryWithLogicalExpire(Long id) {
        String key = "cache:shop:"+id;
        //1、从Redis查询id
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断redis是否存在数据
        if(StrUtil.isBlank(shopJson)) {
            //3、未命中
            return null;
        }
        //4、命中,将json反序列化未对象
        RedisData redisData = JSONUtil.toBean(shopJson,RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //5、判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //5.1、未过期,返回店铺信息
            return shop;
        }
        //5.2、已过期,需要缓存重建
        //6、缓存重建
        //6、1 获取互斥锁
        String lockKey = "lock:shop:"+id;
        boolean isLock = tryLock(lockKey);

        //6、2 判断是否获取锁成功
        if(isLock){
            //6、3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    saveShop2Redis(id,20L);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }
        //6、4 失败,返回过期商品信息
        return shop;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值