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

黑马点评项目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);}});
http://www.xdnf.cn/news/9739.html

相关文章:

  • 有关于常量的一节知识
  • JAVA学习 DAY1 初识JAVA
  • 生成式引擎的认知霸权:为什么传统内容失效?
  • 【AI非常道】二零二五年三月,AI非常道
  • 黑马程序员C++核心编程笔记--1 程序的内存模型
  • AniGS - 基于单张图像的动态高斯化身重建
  • 62、【OS】【Nuttx】编码规范解读(十)
  • 电缆中性点概念
  • webstrorm 提示(This file does not belong to the project)此文件不属于该项目
  • 深度学习-模型训练的相关概念
  • 【spring】spring中的retry重试机制; resilience4j熔断限流教程;springboot整合retry+resilience4j教程
  • java中自定义注解
  • WildDoc:拍照场景下的文档理解——数据真香
  • ETL怎么实现多流自定义合并?
  • 信奥之计算原理与排列组合
  • 人工智能在智慧物流中的创新应用与未来趋势
  • mybatis plus的源码无法在idea里 “download source“
  • 勾股数的性质和应用
  • JS逆向 【QQ音乐】sign签名| data参数加密 | AES-GCM加密 | webpack实战 (上)
  • Dify案例实战之智能体应用构建(一)
  • wewin打印机 vue版本 直接用
  • ABF膜介绍
  • 免杀二 内存函数与加密
  • QTest应用迷城
  • 鸿蒙完整项目-仿盒马App(一)首页静态页面
  • 极坐标下 微小扇环 面积微元
  • 数据库如何优化,尤其是历史温度数据计算品均值,实现小时,天,月的查询计算
  • Android和iOS DNS设置方式
  • C++链式调用与Builder模式
  • 【LightRAG:轻量级检索增强生成框架】