Redis:黑马点评项目之商品缓存

一、缓存更新策略

缓存可以减轻数据库压力,但也会存在数据库与缓存不一致的问题;
如果数据库数据更新,缓存没有更新,那查到的就是缓存中的旧数据;

  1. 内存淘汰:不用自己维护,利用redis内存淘汰机制,当内存不足时淘汰部分数据,下次查询时更新缓存;在一定程度上可以保证一致性;但是这种一致性不是我们能控制的,淘汰哪一部分数据,什么时候淘汰,不确定;好处是没有维护成本;
  2. 超时剔除:给缓存数据添加过期时间TTL,到期自动删除缓存,下次查询更新缓存;这个一致性的强度取决于时间长短;一致性一般,维护成本也低;
  3. 主动更新:自己编写业务逻辑;在修改数据库同时,改缓存;维护成本高;
    根据业务场景选择对应的缓存更新策略:

在这里插入图片描述

二、主动更新策略

1、**Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存;**用的最多;

Cache Aside Pattern:

1、删除缓存还是更新缓存?
更新缓存,数据库更新n次,缓存就更新n次,如果这n次没有什么查询,那无效写操作较多;
删除缓存,更新数据库时让缓存失效,查询时再更新缓存;用的较多

2、如何保证缓存与数据库的原子性,同时成功或者失败?
单体系统,将缓存与数据库操作放在一个事务里,利用事务本身特性保证;
分布式系统:缓存操作和数据库操作很可能是不同的服务;利用TCC分布式事务方案

3、先操作缓存还是数据库?
都可以。但综合分析,先操作数据库,再操作缓存比较妥当。
先删除缓存,再操作数据库;
如果有线程安全问题,线程1与线程2同时对缓存和数据库操作,由于缓存的读写速度远高于数据库的读写速度,最终造成缓存与数据库不一致的概率还是较高的;

先操作数据库,再删缓存;在此操作上,可以在最后写缓存加上一个过期时间;
在这里插入图片描述

2、Read/Write Through Pattern : 缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题;
3、Write Behind Caching Pattern : 调用者会操作缓存,由其他线程将缓存数据持久化到数据库;但是这种方法不能保证最终一致;

在这里插入图片描述

三、缓存穿透

客户端请求的数据在缓存和数据库都不存在,这样缓存永远不会生效,这些请求都会落到数据库;一般外部攻击,不断用不存在的数据请求数据库,最终造成数据库压力过大崩溃;

解决方案:
1、缓存空对象
在这里插入图片描述
2、布隆过滤:
内存占用少,没有多余的key;实现复杂,存在误判可能
在这里插入图片描述

在这里插入图片描述

/**
     * 获取店铺详情(通过设置null解决缓存穿透问题)
     *
     * @param id
     * @return {@link Result}
     */
private Result getShopByIdWithCacheCross(Long id) {
        // 1、先查询redis
        String shopJson = redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        // 在redis有,返回,将json串转为
        if (StrUtil.isNotBlank(shopJson)) {
            // 这里的isNotBlank 方法 为null,为"",为”\t“都返回false
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 判断命中的是否是空字符串
        if (shopJson != null) {
            // 如果不是null空值,那一定是空字符串
            return Result.fail("店铺不存在");
        }
        // 2、redis没有,再查数据库
        Shop shop = getById(id);
        // 3、数据库没有,返回错误
        if (Objects.isNull(shop)) {
            // 解决缓存穿透,缓存一个空字符串
            redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("店铺不存在");
        }
        // 将查到的数据存入redis,设置过期时间为30min
        redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

四、缓存雪崩

同一时段内,缓存key大面积失效或者redis服务宕机,导致请求落到数据库上,带来巨大压力;

解决方案:

  1. 给不同的key的过期时间,设置一个随机值;可以提前把一些导入到缓存中,在ttl后面可以跟上一个随机数,让过期时间分散在各个时间段,避免缓存同一时间大面积失效;
  2. **宕机情况:**利用redis集群,哨兵机制,提高服务的可用性;主从可以实现数据的同步;
  3. 给缓存业务添加降级限流策略;快速失败,降级服务;
  4. 给业务添加多级缓存;nginx缓存、redis缓存、层层加缓存;
    在这里插入图片描述

五、缓存击穿

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

解决方案:

1、利用互斥锁;
缺点:锁未释放,互相等待;其他线程就会一直等待,阻塞,导致性能问题;
在这里插入图片描述

2、逻辑过期;
在这里插入图片描述

在这里插入图片描述

六、用互斥锁解决缓存击穿问题

利用setNx命令;这个命令只有key不存在才会设置成功;

在这里插入图片描述

/**
     * 获取店铺详情(通过互斥锁解决缓存击穿问题)
     *
     * @param id
     * @return {@link Result}
     */
    private Shop getShopByIdWithCacheStave(Long id) {
        // 1、先查询redis
        String shopJson = redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        // 在redis有,返回,将json串转为
        if (StrUtil.isNotBlank(shopJson)) {
            // 这里的isNotBlank 方法 为null,为"",为”\t“都返回false
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 判断命中的是否是空字符串
        if (shopJson != null) {
            // 如果不是null空值,那一定是空字符串
            return null;
        }
        // 2.1、redis没有,未命中,尝试获取锁  注意,lockKey与商铺key不一样
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        // 2.2判断是否获取了锁,如果未拿到,重试,循环,设置休眠时间
        Shop shop;
        try {
            if (!tryLock(lockKey)) {
                // 设置休眠时间
                Thread.sleep(10);
                return getShopByIdWithCacheStave(id);
            }
            // 2.3拿到了锁,再查数据库
            shop = getById(id);
            // todo 模拟重建的测试
            Thread.sleep(200);
            // 3、数据库没有,返回null
            if (Objects.isNull(shop)) {
                // 解决缓存穿透,缓存一个空字符串
                redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 将查到的数据存入redis,设置过期时间为30min
            redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 4、try-catch-finally 无论抛不抛异常,最后都要释放锁
            realeseLock(lockKey);
        }
        return shop;
    }
 /**
     * 获取锁
     *
     * @param key
     * @return {@link boolean}
     */
    private boolean tryLock(String key) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1", 2, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(result);
    }

    /**
     * 释放锁
     *
     * @param key
     * @return {@link boolean}
     */
    private void realeseLock(String key) {
        redisTemplate.delete(key);
    }

    /**
     * 重建缓存数据,RedisData
     *
     * @param id
     * @param expireTime
     * @return {@link boolean}
     */
    @Override
    public boolean saveHotData(Long id, Long expireTime) throws InterruptedException {

        String key = RedisConstants.CACHE_SHOP_KEY + id;
        Shop shop = getById(id);
        // 重建,模拟
        Thread.sleep(20L);
        if (Objects.nonNull(shop)) {
            RedisData data = new RedisData();
            // 设置比当前时间晚几个秒
            data.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
            data.setData(shop);
            redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data));
            return Boolean.TRUE;
        } else {
            return Boolean.FALSE;
        }

    }

七、利用逻辑过期解决缓存击穿

在这里插入图片描述

/**
     * 获取店铺详情(通过逻辑过期解决缓存击穿问题)
     *
     * @param id
     * @return {@link Result}
     */
   private Shop getShopByIdWithLogicalExpire(Long id) {
        // 1、先查询redis
        String shopJson = redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        // redis没有,未命中,返回null
        if (StrUtil.isBlank(shopJson)) {
            // 未命中,返回空
            return null;
        }
        // 2.1、redis有,命中,判断过期时间
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        // 2.2 过期时间在当前时间之后,未过期,返回shop
        RedisData data = JSONUtil.toBean(shopJson, RedisData.class);
        Shop toShop = JSONUtil.toBean((JSONObject) data.getData(), Shop.class);
        if (data.getExpireTime().isAfter(LocalDateTime.now())) {
            // 过期时间在当前时间之后,未过期,返回shop
            return toShop;
        }
        // 2.3 过期了,获取互斥锁,判断是否获取互斥锁
        boolean isGetLock = tryLock(lockKey);
        // 3.1 有互斥锁,重建,新开启一个线程,根据id查询数据库,将数据库数据写入redis
        if (isGetLock) {

            CACHE_EXPIRE_EXECUTOR.submit(() -> {
                try {
                    this.saveHotData(id, 20L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    realeseLock(lockKey);
                }
            });

        }
        // 3.2 没有互斥锁。返回旧的商品信息
        return toShop;
    }

八、缓存工具封装

在这里插入图片描述

     private final StringRedisTemplate redisTemplate;

    /**
     * 构造函数注入redisTemplate
     * @param redisTemplate
     */
    public RedisCacheClient(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 将任意对象序列化json对象并存储在String类型的key中,并且可以设置过期时间
     */
    public void setExpireTime(String key, Object value, Long expireTime, TimeUnit timeUnit){
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),expireTime,timeUnit);
    }

    /**
     * 将任意对象序列化json对象并存储在String类型的key中,并且可以设置逻辑过期时间,处理缓存击穿问题
     */
    public void setLogicalExpireTime(String key, Object value, Long expireTime, TimeUnit timeUnit){
        RedisData data = new RedisData();
        // 设置逻辑过期时间,比当前时间晚 timeUnit 分钟/秒/小时
        // 传进来的expireTime不能确定是秒,所以把它转成秒
        data.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expireTime)));
        data.setData(value);
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(data));
    }

    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,利用缓存控制的方式解决缓存穿透问题
     * @param keyPrefix key的前缀
     * @param id id为泛型,不确定它的类型,不一定是long,只有用到的时候才会确定
     * @param objectType 具体要用到的对象类型
     * @param dbCallback 要根据id查询具体返回数据库对象的函数式方法
     * @param time 过期时间
     * @param unit 时间单位
     * @return {@link R}
     */
    public <R,ID> R getObjectWithCacheCross(String keyPrefix, ID id, Class<R> objectType, Function<ID,R> dbCallback , Long time, TimeUnit unit) {
        // KEY
        String key = keyPrefix + id;
        // 1、先查询redis
        String shopJson = redisTemplate.opsForValue().get(key);
        // 在redis有,返回,将json串转为
        if (StrUtil.isNotBlank(shopJson)) {
            // 这里的isNotBlank 方法 为null,为"",为”\t“都返回false
            return JSONUtil.toBean(shopJson, objectType);
        }
        // 判断命中的是否是空字符串
        if (shopJson != null) {
            // 如果不是null空值,那一定是空字符串
            return null;
        }
        // 2、redis没有,再查数据库
        R r = dbCallback.apply(id);
        // 3、数据库没有,返回错误
        if (Objects.isNull(r)) {
            // 解决缓存穿透,缓存一个空字符串
            redisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 将查到的数据存入redis,设置过期时间为30min
        this.setExpireTime(key, r, time, unit);
        return r;
    }

    /**
     * 过期时间线程池
     */
    private static final ExecutorService CACHE_EXPIRE_EXECUTOR = Executors.newFixedThreadPool(10);


    /**
     * 根据指定的key查询缓存,并反序列化为指定类型,利用逻辑过期的方式解决缓存击穿问题
     * @param keyPrefix key的前缀
     * @param id id为泛型,不确定它的类型,不一定是long,只有用到的时候才会确定
     * @param objectType 具体要用到的对象类型
     * @param dbCallback 要根据id查询具体返回数据库对象的函数式方法
     * @param time 过期时间
     * @param unit 时间单位
     * @return {@link R}
     */
    public <R,ID> R getObjectWithLogicalExpire(String keyPrefix, ID id, Class<R> objectType, Function<ID,R> dbCallback , Long time, TimeUnit unit) {
        // key
        String key = keyPrefix + id;
        // 1、先查询redis
        String shopJson = redisTemplate.opsForValue().get(key);
        // redis没有,未命中,返回null
        if (StrUtil.isBlank(shopJson)) {
            // 未命中,返回空
            return null;
        }
        // 2.1、redis有,命中,判断过期时间
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        // 2.2 过期时间在当前时间之后,未过期,返回shop
        RedisData data = JSONUtil.toBean(shopJson, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) data.getData(), objectType);
        if (data.getExpireTime().isAfter(LocalDateTime.now())) {
            // 过期时间在当前时间之后,未过期,返回shop
            return r;
        }
        // 2.3 过期了,获取互斥锁,判断是否获取互斥锁
        boolean isGetLock = tryLock(lockKey);
        // 3.1 有互斥锁,重建,新开启一个线程,根据id查询数据库,将数据库数据写入redis
        if (isGetLock) {
            CACHE_EXPIRE_EXECUTOR.submit(() -> {
                try {
                    // 缓存重建
                    // 1.1 更新数据库,这里不知道具体用到的逻辑,用函数先apply,执行
                    R r1 = dbCallback.apply(id);
                    // 1.2再写入缓存
                    this.setLogicalExpireTime(lockKey,r1,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    realeseLock(lockKey);
                }
            });

        }
        // 3.2 没有互斥锁。返回旧的商品信息
        return r;
    }


    /**
     * 获取锁
     *
     * @param key
     * @return {@link boolean}
     */
    private boolean tryLock(String key) {
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1", 2, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(result);
    }

    /**
     * 释放锁
     *
     * @param key
     * @return {@link boolean}
     */
    private void realeseLock(String key) {
        redisTemplate.delete(key);
    }
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值