Redis-高并发缓存

232 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

缓存简介

缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。

Redis是运行于内存中,一般作为数据库缓存来使用。

缓存使用场景:

当数据发生变更(增删改),我们要删除缓存还是更新缓存?

  • 如果每次更新数据库都更新缓存,无效写操作较多
  • 所以要使用删除缓存,更新数据库时让缓存失效,查询时再更新缓存

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

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

数据发生变更时,先操作缓存还是先操作数据库?

  • 这两种操作在并发状态下(如果没加锁),都会导致共享资源问题,但是先操作数据库这方案,发生共享问题的可能性会更小,所以:先操作数据库,再删除缓存

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。如果有人恶意请求数据库不存在的数据,则会导致数据库负载过重。

解决方法一:设置缓存空对象(容易实现,但是会造成一定的内存消耗)

解决方法二:布隆过滤器(存在误判的可能,不一定全部拦截成功)

上面两种是防止缓存穿透的两种方法,下面这些是主动避免缓存穿透的方法:

  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流
@Override
    public Result queryById(Long id) {
        // 根据id查询商品缓存
        String json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        // 判断是否命中
        if (StrUtil.isNotBlank(json)){
            // 命中:直接返回
            Shop shop = JSONUtil.toBean(json, Shop.class);
            return Result.ok(shop);
        }

        // 判断命中的是否是空值(缓存穿透)
        if (json != null){
            return Result.fail("店铺信息不存在");
        }

        // 未命中:查询数据库、写到内存
        Shop shop = getById(id);
        if (shop == null) {
            // 缓存穿透预防  设置空值
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("店铺不存在!");
        }
        // 设置缓存
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 返回
        return Result.ok(shop);

    }

缓存雪崩

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

解决方案:

  • 给不同的Key的TTL添加随机值(防止Key同时失效)
  • 利用Redis集群提高服务的可用性(哨兵机制)
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

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

常见的解决方案:

  • 互斥锁
  • 逻辑过期

互斥锁实现:

 @Override
    public Result queryById(Long id) {

        //   -----------缓存穿透----------
        String json = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        if (StrUtil.isNotBlank(json)){
            Shop shop = JSONUtil.toBean(json, Shop.class);
            return Result.ok(shop);
        }

        if (json != null){
            return null;
        }

        // 未命中:查询数据库、写到内存, 实现**缓存重建**

        // 1.获取互斥锁、判断是否成功:  成功:查数据库, 失败:休眠重试
        Shop shop = null;
        String lockKey = "lock:shop:" + id;
        try {
            boolean isLock = tryLock(lockKey);
            if (!isLock){
                Thread.sleep(50);

                return queryById(id);
            }

            shop = getById(id);
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return Result.fail("店铺不存在!");
            }
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (InterruptedException e) {
            e.printStackTrace();

        } finally {
            // 2. 释放锁
            unLock(lockKey);
        }

        return Result.ok(shop);
    }