当前位置: 首页 > news >正文

黑马点评项目02——商户查询缓存(缓存穿透、缓存雪崩、缓存击穿)以及细节

在这里插入图片描述

1.添加redis缓存

在这里插入图片描述
StringRedisTemplate 使用的是这个哈,有人可能有疑问,存放的是字符串吗,商铺值应该是个对象才对啊,在细节中解析
代码:

@Override
public Result queryById(Long id) {//查询redis,若存在则转换成对象后返回String key = CACHE_SHOP_KEY + id;String shopJson = stringRedisTemplate.opsForValue().get(key);if (StringUtils.isNotBlank(shopJson)) {Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}//不存在则查询数据库,然后转成以json串存⼊redis后,返回Shop shop = shopMapper.selectById(id);if(shop==null){return Result.fail("店铺不存在");}stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));return Result.ok(shop);
}

2.API 细节解析 json串与对象相互转换

stringRedisTemplate.opsForValue().get(key)的返回值是一个String,如果查不到,返回null,为了以防万一,HuTool工具判断 StringUtils.isNotBlank(shopJson),可以确保是确实是一个商铺。在这里插入图片描述
命中缓存,转换为将String json串转换为对象, Shop shop = JSONUtil.toBean(shopJson, Shop.class); 注意这个API,字符串转化为Shop;
不命中缓存,查数据库返回商铺Shop shop = shopMapper.selectById(id),
此时注意了,不能直接把对象放进去,要放进去一个json,也注意这个API。
stringRedisTemplate.opsForValue() .set(key,JSONUtil.toJsonStr(shop))

3.Redis缓存和数据库一致性策略

Cache Aside(旁路缓存)策略(适合读多写少)
在这里插入图片描述
注意:写的时候先更新数据库,这样也可能发生不一致问题,只是几率相对较小,一个解决策略就是加上延迟双删
在这里插入图片描述
在这里插入图片描述
另外,Cache Aside 策略适合读多写少的场景,不适合写多的场景,因为当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。

 //先更新数据库,再删除缓存shopMapper.updateById(shop);stringRedisTemplate.delete(CACHE_SHOP_KEY+ id);

4. 缓存穿透

缓存穿透 :是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:

4.1 缓存空对象

在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的。现在,我如果查询到数据库没有这个对象时,我就往Redis存放(id:‘’)空字符串,下次你再访问,给你空字符串,根本过不了isBank()
在这里插入图片描述
缺点:
可能存在短时间不一致问题;占用内存
注意:缓存空值要设置较短的过期时间(如 5~10 分钟)

4.2 布隆过滤

直接拦截了,只要数据库中没有,当然可能会存在误判,不过概率较小!!!
4.3 其他方案
在这里插入图片描述

5. 缓存雪崩

缓存雪崩:是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。就是缓存Redis废了
在这里插入图片描述

6. 缓存击穿(例如:优惠劵信息id)

缓存击穿是指:某个热点 key访问频率极高)突然失效,大并发请求在同一时间全部打到数据库,短时间内数据库可能被压垮。

在这里插入图片描述

6.1 互斥锁

在这里插入图片描述

    /*模拟加锁*/private boolean tryLock(String key){Boolean b = stringRedisTemplate.opsForValue().setIfAbsent(key, "", 30, TimeUnit.SECONDS);return BooleanUtil.isTrue(b);}private void unlock(String key){stringRedisTemplate.delete(key);}

stringRedisTemplate.opsForValue().setIfAbsent(key, "", 30, TimeUnit.SECONDS) 拿到了锁就返回true在这里插入图片描述
Boolean.TRUE.equals(success)或者 BooleanUtil.isTrue(success)来判断
互斥锁逻辑

 public Result queryById(Long id) {// 1. 从 Redis 查询缓存String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 2. 如果缓存命中,直接返回  必须有实际东西才可以if(StrUtil.isNotBlank(shopJson)){log.info("shopJson缓存中有:{}",shopJson);Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}// 3. 如果缓存中是空字符串或者占位符(说明数据库中查过确实不存在),返回错误if (shopJson != null) {return Result.fail("店铺信息不存在(缓存空值)");}// 4. 缓存未命中,准备查询数据库前先尝试加锁,防止缓存击穿String lockKey = "shop:lock" + id;boolean lock = tryLock(lockKey); // 尝试加锁 true 该线程拿到了锁Shop shop;try {if (lock) {// 5. 获取锁成功,查询数据库shop = getById(id);// 6. 数据库中也不存在,返回错误(此处未缓存空值,依赖布隆拦截)if (shop == null) {// return 之前会进入finallyreturn Result.fail("店铺不存在");}// 7. 查询成功,写入缓存String jsonStr = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, 3L, TimeUnit.MINUTES);} else {// 8. 获取锁失败,稍等后递归重试(等待其他线程完成缓存填充)Thread.sleep(50);// return 之前会进入finallyreturn queryById(id);}} catch (InterruptedException e) {throw new RuntimeException(e);} finally{// 9. 只有拿到锁的线程才释放锁,避免误删其他线程的锁if (lock) {unlock(lockKey);}}// 10. 返回结果return Result.ok(shop);}

6.2 逻辑过期

既然是高并发访问那干脆就直接redis里面一直都不要删除了,再加个逻辑过期时间,过期的话就开个独立线程去更新数据写入redis,在没更新完之前访问到的都是redis里面的旧数据。
在这里插入图片描述
具体实现见:逻辑过期解决缓存击穿

我只是讲一下难以实现的技术点:
1、需要封装一个实体类+过期时间一起构成RedisData对象,有两种实现方式。
第一种:泛型

@Data
public class RedisData<T> {private LocalDateTime expireTime;private T data;
}

第二种:Object

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RedisData {private LocalDateTime expireTime;private Object data;
}

第一种就是序列化麻烦一些,不过更规范,api记住

     // 1. 查询 Redis 缓存String json = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(json)) {// 缓存未命中return null;}// 2. 反序列化为带逻辑过期的数据结构RedisData<?> redisData = JSONUtil.toBean(json, RedisData.class);/*  JSONObject dataJson = (JSONObject) redisData.getData(); // 先转 JSONObjectT data = dataJson.toBean(type);*/T data = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();

接下来就是判断是否过期了,如果没有过期,直接返回data;如果过期了,尝试获取锁,注意有个细节,如果获取锁后一定要再判断一下是否从缓存中得到是否为空,为空,说明被删掉了,返回之前找的旧data,再判断这时候是不是不过期了,这样就少一次IO,不过期,说明有其他线程刚刚更新过了。
在这里插入图片描述
如果确实是过期,交给其他线程重建,

// 缓存重建线程池(用于异步更新缓存)
private static final ExecutorService CACHE_REBUILD_EXECUTOR =Executors.newFixedThreadPool(10);
  // 6. 异步线程池重建缓存CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查询数据库T fresh = dbFallback.apply(id);// 模拟重建缓存Thread.sleep(200);// 重新写入缓存(逻辑过期)this.setWithLogicalExpire(key, fresh, time, unit);} catch (Exception e) {e.printStackTrace();} finally {// 释放锁unlock(lockKey);}});

相关文章:

  • 解析C++排序算法
  • vue-seamless-scroll 结束从头开始,加延时后滚动
  • 影楼精修-AI追色算法解析
  • 定点小数 不需要指数部分 不采用移码
  • 网络渗透基础:信息收集
  • Animate CC CreateJS 技术50道测试题目
  • Python应用while嵌套循环
  • MySQL 索引和事务
  • 【JavaScript 高级】事件循环机制详解
  • 如何站在指标体系之巅看智能数据建模产品(GAI)
  • docker常见考点
  • JS入门——JS引入方式
  • 收集飞花令碎片——C语言(数组+函数)
  • 简单三步FastAdmin 开源框架的安装
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | Split Landing Page(拆分展示页)
  • 小白畅通Linux之旅-----Linux日志管理
  • 软件无线电技术之基带QPSK 调制技术+扩频技术
  • GB/T 14833-2020 合成材料运动场地面层检测
  • 【2025年电工杯数学建模竞赛A题】光伏电站发电功率日前预测问题+完整思路+paper+源码
  • 德思特新闻 | 德思特与es:saar正式建立合作伙伴关系
  • 一般网站首页做多少mb/百度站长app
  • 做竞价推广的网站要求/建站系统cms
  • 长沙做网站哪家公司好/网址如何被快速收录
  • 9e做网站/湖南省人民政府
  • 成都高新区国土规划建设局网站/怎样推广
  • 建设网站怎么克隆/seo搜索引擎优化技术教程