Redis之商品缓存

文章目录

  • 什么是缓存
  • 添加Redis缓存
  • 缓存更新策略
  • 缓存穿透
    • 缓存空对象
    • 布隆过滤器
  • 缓存雪崩
    • 给不同的key的TTL添加随机值
    • 利用Redis集群提高服务的可用性
    • 给缓存业务添加降级限流策略
    • 给业务添加多级缓存
  • 缓存击穿
    • 互斥锁
    • 逻辑过期
  • 缓存工具封装
    • 方法1 写入redis
    • 方法2 设置逻辑过期
    • 方法3 解决缓存穿透
    • 方法4 解决缓存击穿

什么是缓存


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

缓存的作用:

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

缓存的成本:

  • 数据一致性成本
  • 代码维护成本
  • 运维成本

添加Redis缓存


一图胜过千言万语~
image.png

/*** 根据id查询商铺信息* @param id 商铺id* @return 商铺详情数据*/
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {return Result.ok(shopService.queryById(id));
}@Override
public Object queryById(Long id) {String shopKey = CACHE_SHOP_KEY + id;// 从redis查询商店缓存String shopJson = stringRedisTemplate.opsForValue().get(shopKey);// 判断缓存是否存在if(StrUtil.isNotBlank(shopJson)){// 缓存存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}// 缓存不存在,查询数据库Shop shop = getById(id);// 店铺不存在, 返回错误if(shop == null){return Result.fail("商铺不存在");}// 店铺存在, 写入redusstringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop));// 返回return Result.ok(shop);
}
  1. 从redis中查询是否存在商铺缓存
  • stringRedisTemplate.opsForValue().get(shopKey)
  1. 缓存存在, 将数据转换成Java对象并返回
  • Shop shop = JSONUtil.toBean(shopJson, Shop.class)
  1. 缓存不存在, 先查库看商铺是否存在
  • Shop shop = getById(id)
  1. 商铺不存在, 直接返回错误提示
  • return Result.fail("商铺不存在")
  1. 商铺存在, 写入redis再返回商铺信息
  • stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop))
  • return Result.ok(shop)

缓存更新策略


image.png

业务场景:

  • 低一致性要求: 使用内存淘汰机制. 例如店铺类型的查询缓存
  • 高一致性要求: 主动更新, 并以超时剔除作为兜底方案. 例如店铺详情查询的缓存
  • 读操作:
    • 缓存命中则直接返回
    • 缓存未命中则查数据库, 并写入缓存, 设定超时时间
  • 写操作:
    • 先写数据库, 然后再删除缓存
    • 要确保数据库与缓存操作的原子性

更新商铺信息

/*** 更新商铺信息* @param shop 商铺数据* @return 无*/
@PutMapping
public Result updateShop(@RequestBody Shop shop) {// 写入数据库return shopService.updateShop(shop);
}@Override
@Transactional
public Result updateShop(Shop shop) {Long id = shop.getId();if(id == null){return Result.fail("商铺ID不能为空");}// 更新数据库updateById(shop);// 删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY + id);return Result.ok();
}
  1. 判断商铺ID是否为空, 防止NPE
  2. 更新数据库, 直接调用MP的方法, 最后在删除缓存
  3. 添加@Transactional注解, 保持事务一致性, 防止缓存与数据库不一致

缓存穿透


缓存穿透:
指客户端请求的数据在缓存中和数据库都不存在,
这样缓存永远不会生效, 这些请求都会发送到数据库

image.png

解决方案:

缓存空对象

  • 优点: 实现简单 维护方便
  • 缺点: 额外内存消耗 可能造成短期数据不一致

布隆过滤器

  • 优点: 内存占用较少 没有多余key
  • 缺点: 实现复杂 存在误判可能

image.png

@Override
public Object queryById(Long id) {String shopKey = CACHE_SHOP_KEY + id;// 从redis查询商店缓存String shopJson = stringRedisTemplate.opsForValue().get(shopKey);// 判断缓存是否存在if(StrUtil.isNotBlank(shopJson)){// 缓存存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}// 判断命中的是否是空值
***	if(shopJson != null){							<***为多加的代码,防止缓存穿透>
***		// 命中空值,直接返回
***		return Result.fail("商铺不存在");
***	}// 缓存不存在,查询数据库Shop shop = getById(id);// 店铺不存在, 返回错误if(shop == null){// 将空值写入redis
***		stringRedisTemplate.opsForValue().set(shopKey, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return Result.fail("商铺不存在");}// 店铺存在, 写入redusstringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);// 返回return Result.ok(shop);
}

缓存雪崩


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

image.png

解决方案:

给不同的key的TTL添加随机值

利用Redis集群提高服务的可用性

给缓存业务添加降级限流策略

给业务添加多级缓存

缓存击穿


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

image.png

解决方案:

互斥锁

  • 优点:
    • 没有额外的内存消耗
    • 保证一致性
    • 实现简单
  • 缺点:
    • 线程需要等待 性能受到影响
    • 可能有死锁的风险

逻辑过期

  • 优点:
    • 现成无需等待
    • 性能较好
  • 缺点:
    • 不保证一致性
    • 有额外的内存消耗
    • 实现复杂

互斥锁解决缓存击穿
image.png

互斥锁解决缓存击穿

// 缓存击穿
public Shop queryWithMutex(Long id){String shopKey = CACHE_SHOP_KEY + id;// 从redis查询商店缓存String shopJson = stringRedisTemplate.opsForValue().get(shopKey);// 判断缓存是否存在if(StrUtil.isNotBlank(shopJson)){// 缓存存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}// 判断命中的是否是空值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);// 模拟重建延时Thread.sleep(1000);// 店铺不存在, 返回错误if(shop == null){// 将空值写入redisstringRedisTemplate.opsForValue().set(shopKey, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 店铺存在, 写入redusstringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, 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);
}
  1. 判断缓存是否存在, 存在直接返回, 不存在先判断是否为空值, 空值返回null, 不是空值则继续
  2. 实现缓存重建, 尝试拿到锁, 失败则休眠一段时间之后再次尝试拿锁~
  3. 成功拿到锁之后查询数据库, 判断数据是否存在, 不存在则写入空字符串到redis, 防止缓存穿透
  4. 数据存在则将数据写入redis, 下次查询直接命中缓存, 无需抢锁再查库, 最后缓存重建成功, 释放锁

使用逻辑过期解决缓存击穿问题
image.png

使用逻辑过期解决缓存击穿问题

// 数据预热 / 缓存重建
public void saveShopRedis(Long id, Long expireSeconds){// 1. 查询店铺数据Shop shop = getById(id);// 2. 封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));// 3. 写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}// 独立线程重建缓存(线程池)
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 逻辑过期解决缓存击穿
public Shop queryWithLogicalExpire(Long id){String shopKey = CACHE_SHOP_KEY + id;// 1. 从redis查询商店缓存String shopJson = stringRedisTemplate.opsForValue().get(shopKey);// 2. 判断缓存是否存在if(StrUtil.isBlank(shopJson)){// 3. 缓存不存在,直接返回return null;}// 4. 命中, 需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5. 判断是否过期if(expireTime.isAfter(LocalDateTime.now())){// 5.1 未过期, 直接返回return shop;}// 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 {// 重建缓存this.saveShopRedis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放互斥锁unLock(lockKey);}});}// 6.4 返回过期的商铺信息return shop;
}
  1. 判断redis是否存在缓存数据
  • stringRedisTemplate.opsForValue().get(shopKey)
  1. 判断缓存是否存在 if(StrUtil.isBlank(shopJson))
  2. 缓存不存在, 返回 null
  3. 缓存存在, 将缓存反序列化为Java对象
  • RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class)
  • Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class)
  • LocalDateTime expireTime = redisData.getExpireTime() -> 逻辑过期时间
  1. 判断逻辑时间是否过期 if(expireTime.isAfter(LocalDateTime.now()))
  2. 未过期直接返回缓存信息 return shop
  3. 过期则进行缓存重建, 要获取锁, boolean isLock = tryLock(lockKey)
  4. 抢锁成功则进行缓存重建
  • Shop shop = getById(id)
  • RedisData redisData = new RedisData()
  • String cacheKey = CACHE_SHOP_KEY + id
  • redisData.setData(shop)
  • redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds))
  • stringRedisTemplate.opsForValue().set(cacheKey, JSONUtil.toJsonStr(redisData))
  1. 缓存重建成功之后释放锁, unLock(), 最后返回信息 return shop

缓存工具封装


缓存工具封装
image.png

方法1 写入redis

// 写入缓存
public void set(String key, Object value, Long time, TimeUnit unit){stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}

方法2 设置逻辑过期

public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){// 设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
  1. 拿到缓存的key, 缓存的数据value, 缓存时间time, 缓存时间单位unit
  2. 分别设置数据和过期时间, 过期时间是以当前时间加上缓存时间time
  • redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)))
  1. 最后写入Redis, 缓存数据转换成String类型

方法3 解决缓存穿透

// 解决缓存穿透
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;// 从redis查询商店缓存String json = stringRedisTemplate.opsForValue().get(key);// 判断缓存是否存在if(StrUtil.isNotBlank(json)){// 缓存存在,直接返回return JSONUtil.toBean(json, type);}// 判断命中的是否是空值if(json != null){// 命中空值,直接返回return null;}// 缓存不存在,查询数据库R r = dbFallback.apply(id);// 店铺不存在, 返回错误if(r == null){// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 店铺存在, 写入redusthis.set(key, r, time, unit);// 返回return r;
}

方法4 解决缓存击穿

// 解决缓存击穿
// 独立线程重建缓存(线程池)
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
// 逻辑过期解决缓存击穿
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 r1 = dbFallback.apply(id);// 写入Redisthis.setWithLogicalExpire(key, r1, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放互斥锁unLock(lockKey);}});}// 6.4 返回过期的商铺信息return r;
}
// 获取锁
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);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/33661.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

驱动开发(六):应用层通过文件系统与内核层交互

驱动开发相关文章: 驱动开发&#xff08;一&#xff09;&#xff1a;驱动代码的基本框架 驱动开发&#xff08;二&#xff09;&#xff1a;创建字符设备驱动 驱动开发&#xff08;三&#xff09;&#xff1a;内核层控制硬件层 驱动开发&#xff08;四&#xff09;&#xff…

Redis数据结构-跳跃表 skiplist

跳跃表&#xff08;Skiplist&#xff09;是一种用于高效查找的概率型数据结构&#xff0c;它在插入、删除、搜索操作上具有较高的性能&#xff0c;接近于平衡树。Redis使用跳跃表来实现有序集合&#xff08;sorted sets&#xff09;中的范围查询。 ### 跳跃表的基本结构 跳跃…

借助TheGraph 查询ENS信息

关于ENS (以太坊域名服务) ENS 全称是 Ethereum Name Service,它是一个建立在以太坊区块链上的去中心化域名系统。 ENS 在 Web3 领域发挥着重要作用,主要有以下几个方面: 可读性更好的地址: ENS 允许用户将复杂的以太坊地址(如 0x12345…) 映射为更简单易记的域名。这极大地提…

数据分析-------面试相关

数据分析面试到底在面什么 在寻找合适的数据分析师时&#xff0c;一般招聘方的目标是确保候选人的专业技能和工作经验与岗位要求紧密对应。 关键在于两个方面:岗位的具体需求和数据分析师的职业素质。 首先&#xff0c;招聘方要确保应聘者的专业背景和技能与职位需求相匹配。…

解决微信小程序中input框被禁用后,电脑端小程序点击事件无效,手机端开发者工作和手机端正常

问题&#xff1a;input框被禁用后&#xff0c;电脑端小程序点击事件无效&#xff0c;手机端开发者工作和手机端正常。 解决方法&#xff1a;使用view包裹input标签&#xff0c;在view添加点击事件&#xff0c;input添加css:pointer-events:none; // 禁止鼠标事件 <view bin…

中国车牌检测数据集VOC+YOLO格式2001张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2001 标注数量(xml文件个数)&#xff1a;2001 标注数量(txt文件个数)&#xff1a;2001 标注…

Java 中的 Optional 类及其在避免空指针异常中的作用?如何使用它们来过滤重复元素?

在Java编程的世界里&#xff0c;NullPointerException&#xff08;空指针异常&#xff09;恐怕是每个开发者都不陌生的“老朋友”。它通常发生在尝试访问或操作一个为null的对象的成员时&#xff0c;这不仅会导致程序崩溃&#xff0c;还可能隐藏了代码中的逻辑错误。为了优雅地…

【Linux进阶】UNIX体系结构分解——操作系统,内核,shell

1.什么是操作系统&#xff1f; 从严格意义上说&#xff0c;可将操作系统定义为一种软件&#xff0c;它控制计算机硬件资源&#xff0c;提供程序运行环境。我们通常将这种软件称为内核&#xff08;kerel)&#xff0c;因为它相对较小&#xff0c;而且位于环境的核心。 从广义上…

【TensorFlow深度学习】完整项目案例:从零搭建自然语言问答系统

完整项目案例:从零搭建自然语言问答系统 完整项目案例:从零搭建自然语言问答系统1. 项目概览2. 技术栈与环境准备3. 数据准备4. 模型选择与加载5. 数据预处理6. 模型预测7. 性能优化与评估8. 部署与应用结语完整项目案例:从零搭建自然语言问答系统 随着人工智能技术的飞速发…

用python打印——九九乘法表2

for i in range(1, 10):for j in range(1, i 1):print(f"{j} * {i} {j * i}\t", end)j 1print()外层的 for 循环从 1 到 9 遍历 i。对于每个 i&#xff0c;内层的 for 循环从 1 到 i 遍历 j。在每次循环中&#xff0c;打印出 j 乘以 i 的结果&#xff0c;并以制表…

数据安全和数据加密

数据安全 数据安全是信息技术领域的一个核心议题&#xff0c;涉及到保护数字资料免受未经授权的访问和攻击。它主要包含三个关键方面&#xff1a;可用性、完整性和机密性&#xff0c;这三个要素共同构成了所谓的“信息安全三原则”。 可用性 可用性意味着授权用户能够及时地…

STM32介绍和资料地址

STM32标准外设软件库 https://www.st.com.cn/zh/embedded-software/stm32-standard-peripheral-libraries.html 支持标准外设库的产品系列&#xff1a;

XMLTomcatHttp协议

XML&Tomcat&Http协议 目录 XML&Tomcat&Http协议 1. xml解析(了解) 1.1 配置文件 1.1.1 配置文件的作用 1.1.2 常见的配置文件类型 1.2 properties文件 1.2.1 文件示例 1.2.2 语法规范 1.3 XML文件 1.3.1 文件示例 1.3.2 概念介绍 1.3.3 XML的基本语…

【Android】记录在自己的AMD处理器无法使用Android studio 虚拟机处理过程

文章目录 问题&#xff1a;无法在AMD平台打开Android studio 虚拟机&#xff0c;已解决平台&#xff1a;AMD 5700g系统&#xff1a;win10专业版1、在 amd平台上使用安卓虚拟机需要安装硬件加速器2、关闭win10上的系统服务 问题&#xff1a;无法在AMD平台打开Android studio 虚拟…

前端 CSS 经典:鼠标位置信息

前言&#xff1a;当监听鼠标事件时&#xff0c;需要了解鼠标属性所代表的位置信息 dom.addEventListener("click", (e) > {e.pageX;e.clientX;e.offsetX;e.movementX; }); 1. pageX 表示鼠标距离整个页面左边缘的距离&#xff0c;就算有滚动条&#xff0c;最左…

数据结构与算法-【算法专项】Hash算法-1(散列表+散列函数+Hash冲突解决办法)

数据结构与算法-Hash算法-1 1 前置2 散列表3 散列函数4 Hash冲突4.1 开放寻址&#xff1a;4.2 链路地址 在这里插入图片描述 1 前置 给你N&#xff08;1<N<10&#xff09;个自然数,每个数的范围为&#xff08;1~100&#xff09;。现在让你以最快的速度判断某一个数是否…

怎样选择合适的O型密封圈?

O型密封圈在机械和工业应用中起到至关重要的密封作用。选择合适的O型密封圈不仅能提高设备的运行效率&#xff0c;还能延长其使用寿命。本文将从多个角度详细介绍如何选择适合不同应用场景的O型密封圈。 1、识别操作条件 温度范围&#xff1a;不同材料的O型密封圈在不同温度下…

LabVIEW电机故障监测系统

电机作为工业生产中的关键设备&#xff0c;其故障会导致生产停滞和经济损失。因此&#xff0c;开发一个能实时监控电机状态并预测潜在故障的系统具有重要意义。通过高效的数据采集和分析技术&#xff0c;提升故障诊断的准确性和及时性。 系统组成 该系统由以下部分组成&#…

分布式系统_跨域问题

跨域&#xff1a;域名不一致就是跨域&#xff0c;主要包括&#xff1a; 域名不同&#xff1a; www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com 域名相同&#xff0c;端口不同&#xff1a;localhost:8080和localhost:8081 跨域问题&#xff1a;浏览器禁止…

Eureka区域感知路由:优化微服务架构的地理感知负载均衡

Eureka是Netflix开源的服务发现框架&#xff0c;它是Spring Cloud体系中的核心组件之一。在构建分布式系统和微服务架构时&#xff0c;服务发现是确保不同服务实例能够相互通信的关键机制。Eureka区域感知路由&#xff08;Zone-Aware Routing&#xff09;是Eureka中一个重要的特…