【Redis学习 | 第5篇】Redis缓存 —— 缓存的概念 + 缓存穿透 + 缓存雪崩 + 缓存击穿

在这里插入图片描述

文章目录

  • 完成任务
    • 1. 什么是缓存
    • 2. 添加商户缓存
    • 3. 缓存更新策略
      • 3.1 主动更新
    • 4. 缓存穿透
    • 5. 缓存雪崩
    • 6. 缓存击穿
      • 6.1 使用互斥锁查询商铺信息
      • 6.2 使用逻辑过期查询商铺信息
    • 7. 封装 Redis 工具类

完成任务

1. 什么是缓存

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

比如说,CPU读取数据是内存从磁盘中读取,再到CPU,磁盘中读取数据速度非常慢,于是在 CPU 中设置一个缓冲区,将 CPU 常用的数据存储在该缓冲区中,需要使用这些数据时,直接从缓冲区中读取要比从磁盘中读取快得多!

  • 缓存的作用: 降低后端负载;提高读写效率,降低响应时间
  • 缓存的成本:数据一致性成本;代码维护成本;运维成本

2. 添加商户缓存

使用Redis,用户访问商铺信息的过程:
在这里插入图片描述
关于 Redis 的操作有:

  1. 先从 Redis 中查询数据,如果 Redis 中存在响应数据,则返回给客户端。
  2. 如果 Redis 中不存在响应数据,将从数据库中查询到的数据存储到 Redis,以便于下次访问时,直接从 Redis 中获取,效率会提高很多。

实现代码:

	/*** 根据id查询店铺信息*/@Overridepublic Result queryById(Long id) {// 1. 查询 Redis 缓存String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 2. 判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3. 如果存在,从 Redis 缓存中获取返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}// 4. 不存在,根据 id 查询数据库Shop shop = getById(id);// 5. 如果 shop 不存在,返回错误if (shop == null) {return Result.fail("店铺不存在!");}// 6. 存在,将 shop 存入 Redis 缓存stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop));// 7. 返回return Result.ok(shop);}

对比一下,从 Redis 中获取数据和从数据库中获取数所需时间:
在这里插入图片描述
第一次访问,Redis 中没有存储对应数据,需要从数据库中获取,花费较多的时间。第二次访问,直接从 Redis 中获取数据,可以发现,需要的时间比较少。

查询商铺类型使用 Redis,因为商铺类型基本都是静态的,不会很大地变动,所以建议使用 Redis:

	/*** 查询所有商铺类型* @return*/@Overridepublic Result queryTypeList() {// 1. 从 Redis 中获取缓存String shopTypesJson = stringRedisTemplate.opsForValue().get(CACHE_SHOPTYPE_KEY);// 2. 判断缓存是否存在if (shopTypesJson != null) {// 3. 缓存存在,直接返回List<ShopType> shopTypes = JSONUtil.toList(shopTypesJson, ShopType.class);return Result.ok(shopTypes);}// 4. 缓存不存在,查询数据库List<ShopType> shopTypes = this.query().orderByAsc("sort").list();// 5. 将查询结果缓存到 RedisstringRedisTemplate.opsForValue().set(CACHE_SHOPTYPE_KEY, JSONUtil.toJsonStr(shopTypes));return Result.ok(shopTypes);}

3. 缓存更新策略

(1)内存淘汰:当 Redis 内存不足时,自动淘汰部分数据,下次查询时更新缓存。一致性差,无维护成本。
(2)超时剔除:给缓存数据添加 TTL 时间,到期后自动删除缓存。下次查询时更新缓存。一致性一般,维护成本低。
(3)主动更新:编写业务逻辑,再修改数据库的同时,更新缓存。一致性好,维护成本高。

  • 低一致性需求(不经常修改数据):使用内存淘汰机制。
  • 高一致性需求(需要常常对数据进行修改):使用主动更新,并以超时剔除作为兜底。比如说:优惠券。

3.1 主动更新

在更新数据库的同时更新缓存。

  • 删除缓存还是更新缓存?
    不采取更新缓存:每次更新数据库都要更新缓存,增加无效写操作。比如,当更新了100次数据库,那就要更新100次缓存,而这期间并未对缓存的内容进行访问,此时就是有100次无效写的操作。
    采取删除缓存:更新数据库时让缓存失效,查询时再更新缓存。
  • 如何保证缓存和数据库操作的同时成功或失败?
    单体系统:将缓存与数据库操作放在一个事务中
    分布式系统:利用 TCC 等分布式事务方案
  • 先操作数据库,再删除缓存。这样发生线程安全的可能性更低。

更新店铺信息,同时更新数据库和缓存中的数据信息:

	/*** 更新店铺信息*/@Overridepublic Result update(Shop shop) {Long id = shop.getId();if (null == id) {return Result.fail("店铺id不能为空!");}// 1. 更新数据库updateById(shop);// 2. 删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY + id);return Result.ok();}

4. 缓存穿透

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

如果客户端请求一个根本不存在的 id,数据库只能返回 null 给客户端,客户端收到 null,再次请求… …
当一个人恶意地使用多个线程并发请求数据库中根本不存在的 id,这些请求都会到达数据库,很可能使数据库崩溃。

那应该怎么解决缓存穿透问题呢?
(1)缓存空对象:实现简单,维护方便;但会有额外的内存消耗。
在这里插入图片描述

(2)布隆过滤:内存占用较少,没有多余的 key;但实现复杂,存在误判的可能。
在这里插入图片描述

那布隆过滤器怎么知道数据库是否存在当前访问的数据?

  • 可理解布隆过滤器中有一个byte 数组,里面存储二进制位,当要判断数据库中是否存在当前访问对象时,把数据库中的是数据基于某种哈希算法计算出哈希值,再将哈希值转换为二进制位保存在布隆过滤器中,以0和1的形式进行保存,判断数据是否存在时,就是判断对应的位置是0还是1,以此判断数据是否存在。
  • 判断存在是有误判的可能的。也就是说,布隆过滤器判断不存在,那就一定是不存在;但如果判断是存在的,也有可能数据库中并不存在。

5. 缓存雪崩

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

解决方案:
(1)给不同的 key 的 TTL 添加随机值 ——> 大量缓存同时失效
(2)利用 Redis 集群提高服务的可用性 ——> Redis 服务宕机
(3)给缓存业务添加降级限流策略
(4)给业务添加多级缓存

6. 缓存击穿

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

解决方案:
(1)互斥锁
在这里插入图片描述
(2)逻辑过期
在这里插入图片描述

优点缺点
互斥锁没有额外的内存消耗;保证一致性;实现简单线程需要等待,性能受影响;可能有死锁的风险
逻辑过期线程无需等待,性能较好不保证一致性;有额外的内存消耗;实现复杂

6.1 使用互斥锁查询商铺信息

public Shop queryWithMutex(Long id) {// 1. 查询 Redis 缓存String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 2. 判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3. 如果存在,从 Redis 缓存中获取返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}// 判断是否为空字符串if (shopJson.equals("")) {return null;}// 4. 实现缓存重建// 4.1 尝试获取锁Shop shop = null;try {boolean lock = tryLock(LOCK_SHOP_KEY + id);// 4.2 判断是否获取到锁if (!lock) {// 4.3 获取锁失败,休眠并重试Thread.sleep(50);return queryWithMutex(id);}// 4.4 获取锁成功,根据 id 查询数据库shop = getById(id);// 5. 如果 shop 不存在,返回错误if (shop == null) {// 将空字符串写入 Redis 缓存stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6. 存在,将 shop 存入 Redis 缓存stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 7. 释放锁unLock(LOCK_SHOP_KEY + id);}// 8. 返回return shop;}/*** 尝试获取锁*/private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}/*** 释放锁*/private void unLock(String key) {stringRedisTemplate.delete(key);}

6.2 使用逻辑过期查询商铺信息

// 逻辑过期解决缓存击穿public Shop queryWithLogicalExpire(Long id) {// 1. 查询 Redis 缓存String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);// 2. 判断是否存在if (StrUtil.isBlank(shopJson)) {// 3. 如果不存在,从 Redis 缓存中获取返回return null;}// 4. 存在,Json 反序列化为对象RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);// 5. 判断是否过期if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {// 5.1 未过期,直接返回return shop;}// 5.2 过期,缓存重建// 6. 缓存重建// 6.1 获取互斥锁boolean flag = tryLock(LOCK_SHOP_KEY + id);// 6.2 判断是否获取到锁if (flag) {// 6.3 如果获取到锁,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 缓存重建saveShop2Redis(id, 30L);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放锁unLock(LOCK_SHOP_KEY + id);}});}// 6.4 未获取到锁,返回过期的缓存数据return shop;}/*** 保存店铺信息到 Redis*/public void saveShop2Redis(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));}

7. 封装 Redis 工具类

Redis 的缓存穿透和缓存击穿的解决方法还是比较复杂的,如果每次都重写这些方法,会浪费较多的时间,所以需要将对 Redis 的缓存穿透和缓存击穿的解决方法封装到一个工具类中

这段代码有比较高的复用性,我粘贴在这里,以便于以后使用:

@Slf4j
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}/*** 将任意 Java 对象序列化为 json 并存储在 String 类型的 key 中,并可以设置TTL过期时间*/public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}/*** 将任意 Java 对象序列化为 json 并存储在 String 类型的 key 中,并可以设置逻辑过期时间,用于处理缓存击穿问题*/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));}/*** 根据指定的 key 查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题*/public <R, ID> R queryWithPassThrough(String predixKey, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = predixKey + id;// 1. 查询 Redis 缓存String json = stringRedisTemplate.opsForValue().get(key);// 2. 判断是否存在if (StrUtil.isNotBlank(json)) {// 3. 如果存在,从 Redis 缓存中获取返回return JSONUtil.toBean(json, type);}// 判断是否为空值if (json != null) {// 返回错误信息return null;}// 4. 不存在,根据 id 查询数据库R r = dbFallback.apply(id);// 5. 如果 shop 不存在,返回错误if (r == null) {// 将空字符串写入 Redis 缓存stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6. 存在,将 shop 存入 Redis 缓存set(key, r, time, unit);// 7. 返回return r;}private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);/*** 根据指定的 key 查询缓存,并反序列化为指定类型,利用逻辑过期的方式解决缓存击穿问题*/public <R, ID> R queryWithLogicalExpire(String prefixKey, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = prefixKey + 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);// 5. 判断是否过期if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {// 5.1 未过期,直接返回return r;}// 5.2 过期,缓存重建// 6. 缓存重建// 6.1 获取互斥锁String lockKey = LOGIN_CODE_KEY + id;boolean flag = tryLock(lockKey);// 6.2 判断是否获取到锁if (flag) {// 6.3 如果获取到锁,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查询数据库R r1 = dbFallback.apply(id);// 存储到 RedissetWithLogicalExpire(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", LOCK_SHOP_TTL, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}/*** 释放锁*/private void unLock(String key) {stringRedisTemplate.delete(key);}}

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

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

相关文章

单元测试概述入门

引入 什么是测试&#xff1f;测试的阶段划分&#xff1f; 测试方法有哪些&#xff1f; 1.什么是单元测试&#xff1f; 单元测试&#xff1a;就是针对最小的功能单元&#xff08;方法&#xff09;&#xff0c;编写测试代码对其正确性进行测试。 2.为什么要引入单元测试&#x…

微服务的配置共享

1.什么是微服务的配置共享 微服务架构中&#xff0c;配置共享是一个重要环节&#xff0c;它有助于提升服务间的协同效率和数据一致性。以下是对微服务配置共享的详细阐述&#xff1a; 1.1.配置共享的概念 配置共享是指在微服务架构中&#xff0c;将某些通用或全局的配置信息…

基于改进粒子群优化的无人机最优能耗路径规划

目录 1. Introduction2. Preliminaries2.1. Particle Swarm Optimization Algorithm2.2. Deep Deterministic Policy Gradient2.3. Calculation of the Total Output Power of the Quadcopter Battery 3.OptimalEnergyConsumptionPathPlanningBasedonPSO-DDPG3.1.ProblemModell…

HQChart使用教程30-K线图如何对接第3方数据44-DRAWPIE数据结构

HQChart使用教程30-K线图如何对接第3方数据44-DRAWPIE数据结构 效果图DRAWPIEHQChart代码地址后台数据对接说明示例数据数据结构说明效果图 DRAWPIE DRAWPIE是hqchart插件独有的绘制饼图函数,可以通过麦语法脚本来绘制一个简单的饼图数据。 饼图显示的位置固定在右上角。 下…

Wi-Fi Direct (P2P)原理及功能介绍

目录 Wi-Fi Direct &#xff08;P2P&#xff09;介绍Wi-Fi Direct P2P 概述P2P-GO&#xff08;P2P Group Owner&#xff09;工作流程 wifi-Direct使用windows11 wifi-directOpenwrtwifi的concurrent mode Linux环境下的配置工具必联wifi芯片P2P支持REF Wi-Fi Direct &#xff…

树莓派-5-GPIO的应用实验之GPIO的编码方式和SDK介绍

文章目录 1 GPIO编码方式1.1 管脚信息1.2 使用场合1.3 I2C总线1.4 SPI总线2 RPI.GPIO2.1 PWM脉冲宽度调制2.2 静态函数2.2.1 函数setmode()2.2.2 函数setup()2.2.3 函数output()2.2.4 函数input()2.2.5 捕捉引脚的电平改变2.2.5.1 函数wait_for_edge()2.2.5.2 函数event_detect…

【数据库】四、数据库管理与维护

文章目录 四、数据库管理与维护1 安全性管理2 事务概述3 并发控制4 备份与恢复管理 四、数据库管理与维护 1 安全性管理 安全性管理是指保护数据库&#xff0c;以避免非法用户进行窃取数据、篡改数据、删除数据和破坏数据库结构等操作 三个级别认证&#xff1a; 服务器级别…

Observability:将 OpenTelemetry 添加到你的 Flask 应用程序

作者&#xff1a;来自 Elastic jessgarson 待办事项列表可以帮助管理与假期计划相关的所有购物和任务。使用 Flask&#xff0c;你可以轻松创建待办事项列表应用程序&#xff0c;并使用 Elastic 作为遥测后端&#xff0c;通过 OpenTelemetry 对其进行监控。 Flask 是一个轻量级…

Python双指针

双指针 双指针&#xff1a;在区间操作时&#xff0c;利用两个下标同时遍历&#xff0c;进行高效操作 双指针利用区间性质可以把 O ( n 2 ) O(n^2) O(n2) 时间降低到 O ( n ) O(n) O(n) 反向扫描 反向扫描&#xff1a; l e f t left left 起点&#xff0c;不断往右走&…

python无需验证码免登录12306抢票 --selenium(2)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 [TOC](python无需验证码免登录12306抢票 --selenium(2)) 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 就在刚刚我抢的票&#xff1a;2025年1月8日…

软件系统安全逆向分析-混淆对抗

1. 概述 在一般的软件中&#xff0c;我们逆向分析时候通常都不能直接看到软件的明文源代码&#xff0c;或多或少存在着混淆对抗的操作。下面&#xff0c;我会实践操作一个例子从无从下手到攻破目标。 花指令对抗虚函数表RC4 2. 实战-donntyousee 题目载体为具有漏洞的小型软…

解决nginx多层代理后应用部署后访问发现css、js、图片等样式加载失败

一般是采用前后端分离部署方式&#xff0c;被上一层ng代理后&#xff0c;通过域名访问报错&#xff0c;例如&#xff1a;sqx.com.cn/应用代理路径。 修改nginx配置&#xff0c;配置前端页面的路径&#xff1a; location / {proxy_pass http://前端页面所在服务器的IP:PORT;pro…

小程序textarea组件键盘弹起会遮挡住输入框

<textarea value"{{remark}}" input"handleInputRemark" ></textarea> 如下会有遮挡&#xff1a; 一行代码搞定 cursor-spacing160 修改后代码 <textarea value"{{remark}}" input"handleInputRemark" cursor-spacin…

1.CSS的复合选择器

1.1 什么是复合选择器 在CSS中&#xff0c;可以根据选择器的类型把选择器分为基础选择器和复合选择器&#xff0c;复合选择器是建立在基础选择器之上&#xff0c;对基础选择器进行组合形成的。 复合选择器可以更精准、更高效的选择目标元素&#xff08;标签&#xff09; 复…

【微服务】面试 2、服务雪崩

服务雪崩概念 主要内容&#xff1a;在微服务项目中&#xff0c;微服务间存在远程调用。若某一服务&#xff08;如服务 d&#xff09;出现故障&#xff0c;调用它的服务&#xff08;如服务 a&#xff09;会失败。若调用方持续向故障服务发起请求&#xff0c;由于服务连接数有限且…

计算机视觉算法实战——车道线检测

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​​​​​​ ​​​​​​​​​​​​ ​​​​​ 车道线检测是计算机视觉领域的一个重要研究方向&#xff0c;尤其在自动驾驶和高级驾驶辅助…

腾讯云AI代码助手编程挑战赛 - 腾讯云AI代码助手小试

作品简介 本项目基于腾讯云AI代码助手&#xff0c;对平台提供的AI问答应用进行了功能拓展。页面显示采用Vue框架&#xff0c;对话部分使用TDesign组件&#xff0c;模型为hunyuan。主要新增了TDesign页面主题模式改变、页面主题颜色随机切换、文件上传模拟等功能&#xff0c;虽未…

【Web安全】SQL 注入攻击技巧详解:UNION 注入(UNION SQL Injection)

【Web安全】SQL 注入攻击技巧详解&#xff1a;UNION 注入&#xff08;UNION SQL Injection&#xff09; 引言 UNION注入是一种利用SQL的UNION操作符进行注入攻击的技术。攻击者通过合并两个或多个SELECT语句的结果集&#xff0c;可以获取数据库中未授权的数据。这种注入技术要…

机器学习05-最小二乘法VS梯度求解

机器学习05-最小二乘法VS梯度求解 文章目录 机器学习05-最小二乘法VS梯度求解0-核心知识点梳理1-最小二乘法和梯度求解算法什么关系最小二乘法梯度求解算法两者的关系 2-最小二乘法可以求解非线性回归吗3-最小二乘法不使用梯度求解算法&#xff0c;给出一个简单的示例&#xff…

网络授时笔记

SNTP的全称是Simple Network Time Protocol&#xff0c;意思是简单网络时间协议&#xff0c;用来从网络中获取当前的时间&#xff0c;也可以称为网络授时。项目中会使用LwIP SNTP模块从服务器(pool.ntp.org)获取时间 我们使用sntp例程&#xff0c;sntp例程路径为D:\Espressif\…