【Redis|实战篇2】黑马点评|商户查询缓存

本文记录在学习Redis过程中的关键技术实践与踩坑思考,包含个人在实际学习中的具体过程遇到的问题 以及知识点总结
希望可以给一起学习的大家带来帮助ヽ( ̄▽ ̄)ノ

文章目录

2.商户查询缓存

2.1什么是缓存

缓存就是数据交换的缓冲区,是存储数据的临时地方,一般读写性能较高

缓存的作用

  • 降低后端负载
  • 提高读写效率,降低响应时间

缓存的成本

  • 数据一致性成本
  • 代码维护成本
  • 运维成本
2.2添加商户缓存

具体流程

ShopController

java 复制代码
/**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id) {
        return shopService.queryById(id);
    }

ShopServiceImpl

java 复制代码
@Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 根据id查询
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //1.从Redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        //2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.若存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }

        //4.若不存在根据id查询数据库
        Shop shop = getById(id);
        //5.若不存在返回错误
        if(shop == null){
            return Result.fail("店铺不存在");
        }
        //6.若存在返回数据并将其存入Redis
        stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop));
        return Result.ok(shop);
    }

为啥判断String时用isNotBlank

  • str != null:只能防 null,防不住 ""。如果 Redis 存了个空串,转 JSON 会报错。
  • !str.isEmpty():防不住 null,会报空指针异常 (NPE)。
  • StrUtil.isNotBlank(str)同时防住了 null""" "。只要里面有一点点有效字符,它才认为是"有数据"
2.3添加商户类型缓存

ShopTypeController

java 复制代码
@GetMapping("list")
public Result queryTypeList() {
    return typeService.queryTypeList();
}

ShopTypeServiceImpl

java 复制代码
@Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryTypeList() {
        //1.从Redis中查询店铺类型
        String key = "cache:shop:type";
        String shopTypeJSON = stringRedisTemplate.opsForValue().get(key);

        //2.判断缓存是否存在
        if(StrUtil.isNotEmpty(shopTypeJSON)){
            //3.存在,直接返回
            List<ShopType> typeList = JSONUtil.toList(shopTypeJSON, ShopType.class);
            return Result.ok(typeList);
        }
        //4.不存在,查询数据库
        // 使用 MyBatis-Plus 查询所有数据,通常店铺类型需要按排序字段(sort)升序排列
        List<ShopType> typeList = this.list(new LambdaQueryWrapper<ShopType>()
                .orderByAsc(ShopType::getSort));
        //5.若不存在,返回错误
        if(typeList == null || typeList.size() == 0){
            return Result.fail("店铺类型不存在");
        }
        //6.若存在,写入Redis,返回数据
        String jsonStr = JSONUtil.toJsonStr(typeList);
        stringRedisTemplate.opsForValue().set(key,jsonStr,30, TimeUnit.MINUTES);
        return Result.ok(typeList);
    }

List<ShopType> typeList = this.list(new LambdaQueryWrapper<ShopType>().orderByAsc(ShopType::getSort));

  1. 调用mybatisplus的list方法,执行查询并返回 List<ShopType> 集合
  2. new LambdaQueryWrapper<ShopType>()穿件Lambda构造器
  3. .orderByAsc(ShopType::getSort) 构建排序语句
2.4缓存更新策略

为解决缓存数据的不一致,有以下策略:

业务场景

  • 低一致性要求:使用内存淘汰机制,eg:店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超市剔除作为兜底方案,eg:店铺详情查询的缓存

主动更新也有3种策略

最常用的就是第一种

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

  1. 删除缓存还是更新缓存

    • 更新缓存:每次更新数据库都需要更新缓存,无效写操作比较多 ❌️
    • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存 ✅️
  2. 如何保证缓存和数据库的操作同时成功或失败

    • 单体系统:将缓存和数据库操作放在一个事务
    • 分布式系统:利用TCC等分布式事务方案
  3. 先操作缓存还是数据库

    操作Redis比操作数据库快得多,两种方法都有可能造成数据不一致,但是先操作数据库的这个方法发生不一致的可能性更低

2.5实现商铺缓存与数据库的双写一致

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

  1. 根据id查询店铺时,若缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时期限
  2. 根据id修改店铺时,先修改数据库再删除缓存

在查询操作这里添加超时期限

ShopController

java 复制代码
/**
 * 更新商铺信息
 * @param shop 商铺数据
 * @return 无
 */
@PutMapping
public Result updateShop(@RequestBody Shop shop) {
    return shopService.update(shop);
}

更新操作

java 复制代码
/**
     * 更新店铺
     * @param shop
     * @return
     */
    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id == null){
            return Result.fail("店铺id不能为空");
        }
        //1.更新数据库
        update(shop);
        //2.删除缓存
        stringRedisTemplate.delete("cache:shop:" + shop.getId());
        return Result.ok();
    }
2.6缓存穿透的解决思路

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

常见的解决方案有两种:

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:
      • 额外的内存消耗
      • 可能造成短期的不一致
  • 布隆过滤
    • 优点:内存占用少,没有多余key
    • 缺点:
      • 实现复杂
      • 存在误判可能
2.7解决商铺查询的缓存穿透问题

使用缓存空对象的方法解决

修改一下根据id查询的方法

java 复制代码
/**
     * 根据id查询
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //1.从Redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        //2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.若存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //判断命中的是否为空值
        if(shopJson != null){
            return Result.fail("店铺信息不存在");
        }

        //4.若不存在根据id查询数据库
        Shop shop = getById(id);
        //5.若不存在返回错误
        if(shop == null){
            //将空值写入Redis
            stringRedisTemplate.opsForValue().set("cache:shop:" + id,"",30,TimeUnit.MINUTES);
            return Result.fail("店铺不存在");
        }
        //6.若存在返回数据并将其存入Redis
        stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),2, TimeUnit.MINUTES);
        return Result.ok(shop);
    }
java 复制代码
//判断命中的是否为空值
if(shopJson != null){
return Result.fail("店铺信息不存在");
}

为啥不能改成shopJSON == "",因为==比对的是地址,""和Redis返回的空串是两个不同的对象,地址不一样

2.8缓存雪崩问题

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

解决方案

  • 给不同的key的TTL添加随机值
  • 利用Redis集群提供服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存
2.9缓存击穿问题

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

解决方案

  • 互斥锁

  • 逻辑过期


2.10利用互斥锁解决缓存击穿

需求:修改根据id查询商铺的业务,基于互斥锁方式解决缓存击穿问题

可以使用Redis的setnx实现互斥锁,(如果键不存在才设置)

java 复制代码
/**
     * 根据id查询
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //缓存穿透
//        Shop shop = queryWithPassThrough(id);

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

        return Result.ok(shop);
    }

    /**
     * 解决缓存穿透
     * @param id
     * @return
     */
    public Shop queryWithPassThrough(Long id){
        //1.从Redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        //2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.若存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //判断命中的是否为空值
        if(shopJson != null){
            return null;
        }

        //4.若不存在根据id查询数据库
        Shop shop = getById(id);
        //5.若不存在返回错误
        if(shop == null){
            //将空值写入Redis
            stringRedisTemplate.opsForValue().set("cache:shop:" + id,"",30,TimeUnit.MINUTES);
            return null;
        }
        //6.若存在返回数据并将其存入Redis
        stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),2, TimeUnit.MINUTES);
        return shop;
    }

    /**
     * 互斥锁解决缓存击穿
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id){
        //1.从Redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        //2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.若存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //判断命中的是否为空值
        if(shopJson != null){
            return null;
        }

        //实现缓存重建
        //1)获取互斥锁
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            //2)判断是否获取成功
            if(!isLock){
                //3)失败则休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }

            //4.若不存在根据id查询数据库
            shop = getById(id);
            //5.若不存在返回错误
            if(shop == null){
                //将空值写入Redis
                stringRedisTemplate.opsForValue().set("cache:shop:" + id,"",30,TimeUnit.MINUTES);
                return null;
            }
            //6.若存在返回数据并将其存入Redis
            stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),2, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //释放互斥锁
            unLock(lockKey);
        }


        //返回
        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);
    }
2.11利用逻辑过期解决缓存击穿问题
java 复制代码
/**
     * 逻辑过期解决缓存击穿
     * @param id
     * @return
     */
    public Shop queryWithLogicalExpire(Long id){
        //1.从Redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:" + id);
        //2.判断是否存在
        if(StrUtil.isBlank(shopJson)){
            //3.若不存在,直接返回
            return null;
        }

        //4.命中,把JSON反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        //需要先转成JSONObject再反序列化,否则可能无法正确映射Shop的字段
        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过期 缓冲重建
        //5.2.1判断是否获取锁成功
        String lockKey = "lock:shop:" + id;
        boolean isLock = tryLock(lockKey);
        if(isLock){
            //5.2.2成功,开启独立线程
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //重建缓存
                    this.saveShopToCache(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }
        //5.2.3失败,返回过期的店铺信息

        //6.若存在返回数据并将其存入Redis
        stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop),2, TimeUnit.MINUTES);

        //返回
        return shop;
    }

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 将数据保存到缓存中
     * @param id            商铺id
     * @param expireSeconds 逻辑过期时间
     */
    public void saveShopToCache(Long id, Long expireSeconds) {
        // 从数据库中查询店铺数据
        Shop shop = this.getById(id);
        // 封装逻辑过期数据
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 将逻辑过期数据存入Redis中
        stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(redisData));
    }
相关推荐
霖霖总总2 小时前
[Redis小技巧11]Redis Key 过期策略与内存淘汰机制:深度解析与实战指南
数据库·redis
猹叉叉(学习版)2 小时前
【ASP.NET CORE】 9. 托管服务
数据库·笔记·后端·c#·asp.net·.netcore
百万蹄蹄向前冲3 小时前
支付宝 VS 微信 小程序差异
前端·后端·微信小程序
huohuopro3 小时前
idea使用教程
java·ide·intellij-idea
NGC_66113 小时前
ArrayList扩容机制
java·前端·算法
HalvmånEver10 小时前
7.高并发内存池大页内存申请释放以及使用定长内存池脱离new
java·spring boot·spring
凤山老林10 小时前
SpringBoot 使用 H2 文本数据库构建轻量级应用
java·数据库·spring boot·后端
清汤饺子11 小时前
用 Cursor 半年了,效率还是没提升?是因为你没用对这 7 个功能
前端·后端·cursor