黑马点评学习笔记-Redis练手项目——day2(缓存篇)

  在今天的课程设计的都是缓存相关的知识,想要加深理解,记忆八股可以查看我的另一篇博客,详细记载着黑马的java八股-redis篇黑马java八股篇之Redis篇-优快云博客

给店铺类型查询业务添加缓存

一开始店铺查询是需要使用mp的查询数据库实现的,由于这个列表基本上不会更改,所以很适合使用缓存

在Redis中使用String方法存储缓存以及查询

   @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result queryTypeLits() {
        //查询商铺
        String shopTypeList = stringRedisTemplate.opsForValue().get(CACHE_SHOP_TYPE);
        //获取商铺列表

        if(StringUtils.isNotBlank(shopTypeList)){
            List<ShopType> list = JSONUtil.toList(shopTypeList, ShopType.class);
            return Result.ok(list);
        }
        //如果是空的直接查找数据库
        List<ShopType> shopTList = query().orderByAsc("sort").list();
        //如果数据不存在需要返回错误结果
        if(shopTList==null){
            return Result.fail("商家列表不存在!");
        }
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_TYPE, JSONUtil.toJsonStr(shopTList));
        return Result.ok(shopTList);
    }

使用List实现如下:具体需要使用ArrayList将所有查询到的一个一个json转换为对象

@Slf4j
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result queryShopType() {

        // 1.从 Redis 中查询商铺缓存
        List<String> shopTypeJsonList = stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_LIST_KEY, 0, -1);

        // 2.判断 Redis 中是否有该缓存
        if (shopTypeJsonList != null && !shopTypeJsonList.isEmpty()) {
            // 2.1.若 Redis 中存在该缓存,则直接返回
            ArrayList<ShopType> typeList = new ArrayList<>();
            for (String str : shopTypeJsonList) {
                typeList.add(JSONUtil.toBean(str, ShopType.class));
            }
            //也可以用下面的stream流的方式,其实大差不差,都是遍历,每个数据都转换类型后再操作
//            List typeList = shopTypeJsonList.stream().map((shopTypeJson)->{
//                ShopType shopType = JSONUtil.toBean(shopTypeJson, ShopType.class);
//                return shopType;
//            }).collect(Collectors.toList());
            return Result.ok(typeList);
        }
        // 2.2.Redis 中若不存在该数据,则从数据库中查询
        List<ShopType> typeList = query().orderByAsc("sort").list();

        // 3.判断数据库中是否存在
        if (typeList == null || typeList.isEmpty()) {
            // 3.1.数据库中也不存在,则返回 false
            return Result.fail("分类不存在!");
        }

        // 3.2数据库中存在,则将查询到的信息存入 Redis
        for (ShopType shopType : typeList) {
            stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_LIST_KEY, JSONUtil.toJsonStr(shopType));
        }
        //下面是stream流的方式
//        List shopTypeJson = typeList.stream().map((shopType)-> {
//            String jsonStr = JSONUtil.toJsonStr(shopType);
//            return jsonStr;
//        }).collect(Collectors.toList());
//        stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_LIST_KEY,shopTypeJson);

        // 3.3返回
        return Result.ok(typeList);
    }
}

如果是使用Zset的话唯一的区别就是把opsForList变成opsForZSet。

缓存更新策略

而主动更新中又区分有三个

操作缓存和数据库时有三个问题需要考虑

删除缓存还是更新缓存?

  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多(发生可能更大,不采用)
  • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(发生的概率更小)

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

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

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

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

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

给查询的店铺的缓存添加超时剔除和主动更新的策略

新增的功能是设置超时时间以满足超时剔除的要求

修改店铺是先修改数据库再删除缓存.

首先就是修改查询逻辑,在存储缓存的同时还要让缓存设置超时时间,用超时剔除保底

        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //有先写入在返回
        return Result.ok(shop);

随后就是在更新的时候,采取先更新数据库,再删除缓存(这样发生不一致问题的几率更加小(要先查询看看店铺id是否存在,否则会发生空指针问题)

    @Override
    public Result update(Shop shop) {
        if(shop.getId()==null){
            return Result.fail("店铺id不存在");
        }
        //首先就是直接1更新咯,随后就是删除咯
        updateById(shop);
        //删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());
        return Result.ok(shop);
    }

在查询一次之后再查询发现并没有查询数据库,而是缓存直接返回

缓存穿透!

缓存穿透理论知识

这里可以看我博客中的详细黑马redis八股汇总 ,更详细更易理解

缓存穿透实际解决

穿透的风险在哪?-》在缓存未命中中再查询数据库发现数据库也查不到。

-》改成数据库不存在的话将空值写入redis

-》而且再命中之后还要判断缓存中存储的值是不是空值(不是则返回,是则直接结束)

这里采取的措施是使用存储null值

@Override
public Result queryById(Long id) {

    //通过缓存查询
    String shopString = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    if(StringUtils.isNotBlank(shopString)){
        //如果有直接返回
        Shop shop = JSONUtil.toBean(shopString, Shop.class);
        //如果是空值(null)的话,isNotBlank不会放行
        return Result.ok(shop);
    }
    if(shopString!=null){
        return Result.fail("店铺不存在");
    }
    //首先查询,如果没有就查询数据库
    Shop shop=getById(id);
    //查询数据库
    //没有返回false
    if(shop==null){
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
        return Result.fail("不存在该商店");
    }
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
    //有先写入在返回
    return Result.ok(shop);
}

前两种是被动措施,后面的是主动的措施

封装工具类,其实实际上就是只需要根据shop的代码改造即可,首先就是

  • 需要传入查询数据库的回调函数,因为不知道要查询的是哪个类的数据库
  • 需要传入id类型,不知道id是不是Long类型还是其他类型,以及要返回的类的类型

防止缓存穿透的查询函数


    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 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;
    }

防止缓存击穿-》互斥锁查询函数

  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;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值