Redis项目:缓存

黑马点评缓存部分:

缓存的标准操作方式就是查询数据库之前先查询缓存,如果缓存数据存在,则直接从缓存中返回,如果缓存数据不存在,再查询数据库,然后将数据存入redis

最初缓存版本

//最初源代码public Shop queryWithPassThrough(Long id) {//从redis中查商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(SHOP_ID + id);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {Shop shop = JSONUtil.toBean(shopJson, Shop.class);//存在 直接返回return null;}//判断是不是空if (shopJson != null) {//返回错误信息return null;}//不存在  查数据库 MybatisPlusShop shop = getById(id);//数据库不存在 返回错误if (shop == null) {//将空值写入redisstringRedisTemplate.opsForValue().set(SHOP_ID + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//数据库中存在  写入redisstringRedisTemplate.opsForValue().set(SHOP_ID + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);//返回return null;}

解决数据不一致的办法:双写

由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在, 几种方案

Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案

Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理

Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致 

如果采用第一个方案,那么假设我们每次操作数据库后,都操作缓存,但是中间如果没有人查询,那么这个更新动作实际上只有最后一次生效,中间的更新动作意义并不大,我们可以把缓存删除,等待再次查询时,将缓存中的数据加载出来

我们应当是先操作数据库,再删除缓存,原因在于,如果你选择先删除缓存再操作数据库,在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。

缓存穿透问题:缓存空对象

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

缓存穿透的解决方案有哪些?

  • 缓存null值

  • 布隆过滤

  • 增强id的复杂度,避免被猜测id规律

  • 做好数据的基础格式校验

  • 加强用户权限校验

  • 做好热点参数的限流 

这里使用缓存null值来完成缓存穿透

public <R,ID> R queryWithPassThrough(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.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}// 判断命中的是否是空值if (json != null) {// 返回一个错误信息return null;}// 4.不存在,根据id查询数据库R r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);return r;}

缓存雪崩

热点Key问题

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

解决方案:

  • 给不同的Key的TTL添加随机值

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

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

  • 给业务添加多级缓存

缓存雪崩的解决方案:互斥锁

代码:

//互斥锁public Shop queryWithMutex(Long id) {//1.从redis中查商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(SHOP_ID + id);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在 直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断是不是空if (shopJson != null) {//返回错误信息return null;}//实现缓存重建// 获取互斥锁String lockKey = SHOP_LOCK_KEY + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);// 判断是否成功if (!isLock) {//失败->休眠并重试Thread.sleep(50);queryWithMutex(id);}//成功->查询数据库信息shop = getById(id);//模拟延迟//Thread.sleep(200);//数据库不存在 返回错误if (shop == null) {//将空值写入redisstringRedisTemplate.opsForValue().set(SHOP_ID + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//数据库中存在  写入redisstringRedisTemplate.opsForValue().set(SHOP_ID + id, 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", LOCK_SHOP_TTL, TimeUnit.SECONDS);//返回的啥时候拆箱 可能出现空指针return BooleanUtil.isTrue(flag);}//释放锁private void unLock(String key) {stringRedisTemplate.delete(key);}

互斥锁的大致逻辑:

缓存查询+缓存重建

先从 Redis 中查询缓存数据。

如果缓存命中(数据存在),直接返回缓存数据。

如果缓存未命中(数据不存在),尝试获取分布式锁,防止多个线程同时重建缓存。

如果没有得到锁,休眠 重试

如果得到了锁 查询数据库信息 

数据库信息不存在 将空值写入redis 

数据库中存在 写入redis

最后无论如何释放锁 返回数据

 DoubleCheck: 为了避免多个线程同时重建缓存,从而减少不必要的数据库查询和缓存写入操作

在这里需要加入这个doubleCheck

在获取锁之前,先检查一次缓存(第一次检查)。如果缓存已经命中,直接返回数据,无需获取锁。这样可以避免大量线程同时竞争锁,减少锁的开销。

在获取锁之后、查询数据库之前,再次检查缓存(第二次检查)。如果缓存已经命中,直接返回数据,无需查询数据库。这样可以确保在等待锁的过程中,其他线程没有已经重建缓存,从而避免重复操作。(上述代码没写,逻辑过期的写了)

第二次检查防止数据更新,即使锁了,其他线程或进程仍然可以 通过其他途径更新缓存,而不受当前锁的限制

缓存雪崩的解决方案:逻辑过期

 private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//热点key 逻辑public Shop queryWithLogicalExpire(Long id) {String key = CACHE_SHOP_KEY + id;String json = stringRedisTemplate.opsForValue().get(key);//从redis中查商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(SHOP_ID + id);//判断是否存在if (StrUtil.isBlank(shopJson)) {//存在 直接返回return null;}//命中 需要把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);//redisData.getData()是一个Object对象 需要,但实际上是一个JSONObjectShop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();//判断过期if (expireTime.isAfter(LocalDateTime.now())) {//没过期return shop;}//过期了需要缓存重建String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);//判断是否获取锁成功if (isLock) {//!!!做DoubleCheckString jsonAgain = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(jsonAgain)) {RedisData redisDataAgain = JSONUtil.toBean(jsonAgain, RedisData.class);LocalDateTime expireTimeAgain = redisDataAgain.getExpireTime();if (expireTimeAgain.isAfter(LocalDateTime.now())) {return JSONUtil.toBean((JSONObject) redisDataAgain.getData(), Shop.class);}}// 缓存仍需重建,异步执行CACHE_REBUILD_EXECUTOR.submit(() -> {try {this.saveShop2Redis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);} finally {unLock(lockKey);}});}return shop;}
//缓存重建
public void saveShop2Redis(Long id, Long expireSeconds) {//查询店铺信息Shop shop = getById(id);//封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));//写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

实现逻辑过期,需要创建一个逻辑过期数据类 记录过期时间的一个类

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

逻辑:

和前面一样,先从redis中查缓存 存在就返回数据

在返回数据的时候 先将json反序列化成RedisData对象 再将RedisData 中的data转换成shop,真正需要的对象

然后先判断数据是否过期 没过期返回

过期了需要进行缓存重建

重建还是先拿到锁,再做DoubleCheck 如果不需要继续重建,返回对象

如果确实需要重建 这里采用的是异步执行 

        1、使用线程池 CACHE_REBUILD_EXECUTOR 提交一个异步任务。

        2、在异步任务中,调用 saveShop2Redis 方法重建缓存。

        3、无论缓存重建是否成功,最终都会释放锁。

最后返回数据

值得注意的是,在执行之前需要对缓存数据进行预热 将数据加载到缓存中。

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

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

相关文章

OpenCV中距离公式

一、各类距离公式总结 常见距离公式 欧氏距离&#xff1a; 曼哈顿距离&#xff08;L1&#xff09;‌&#xff1a; 切比雪夫距离&#xff08;Chessboard&#xff09;‌&#xff1a; 1、点与点距离(欧氏距离) ‌二维空间‌ 设两点坐标为 P1(x1,y1)、P2(x2,y2)&#xff0c;其距离…

六十天前端强化训练之第二十四天之Vue 模板语法与 v-for 指令大师级详解

欢迎来到编程星辰海的博客讲解 看完可以给一个免费的三连吗&#xff0c;谢谢大佬&#xff01; 目录 一、模板语法与指令知识精讲 1.1 模板语法三大核心 1.2 常见指令全家福 1.3 v-for 深度解析 二、商品列表示例完整实现 2.1 完整可运行代码 2.2 代码解析 2.3 运行效果…

XSS跨站脚本攻击漏洞(Cross Site Scripting)

前提概要 本文章主要用于分享XSS跨站脚本攻击漏洞基础学习&#xff0c;以下是对XSS跨站脚本攻击漏洞的一些个人解析&#xff0c;请大家结合参考其他文章中的相关信息进行归纳和补充。 XSS跨站脚本攻击漏洞描述 跨站脚本攻击&#xff08;XSS&#xff09;漏洞是一种常见且危害较…

用ArcGIS做一张符合环评要求的植被类型图

植被类型图是环境影响评价&#xff08;环评&#xff09;中的重要图件&#xff0c;需满足数据准确性、制图规范性和信息完整性等要求。本教程将基于ArcMap平台&#xff0c;从数据准备到成果输出&#xff0c;详细讲解如何制作符合环评技术规范的植被类型图。 ArcGIS遥感解译土地…

详解string类+迭代器

迭代器 概念&#xff1a;在 C 中&#xff0c;迭代器是访问容器&#xff08;如数组、列表、向量、字符串等&#xff09;元素的一种方式。迭代器提供了一种统一的接口&#xff0c;使得你可以使用相同的代码来遍历不同类型的容器。迭代器本质上是一个指针或者指针的封装&#xff0…

Sqoop安装部署

Apache Sqoop 简介 Sqoop&#xff08;SQL-to-Hadoop&#xff09;是 Apache 开源项目&#xff0c;主要用于&#xff1a; 将关系型数据库中的数据导入 Hadoop 分布式文件系统&#xff08;HDFS&#xff09;或相关组件&#xff08;如 Hive、HBase&#xff09;。 将 Hadoop 处理后…

软件工程之软件验证计划Software Verification Plan

个人主页&#xff1a;云纳星辰怀自在 座右铭&#xff1a;“所谓坚持&#xff0c;就是觉得还有希望&#xff01;” 本文为基于ISO26262软件验证计划模板&#xff0c;仅供参考。 软件验证计划&#xff0c;包括&#xff1a; 1. 软件需求验证计划 2. 软件架构设计验证计划 3. 软件单…

Windows系统本地部署OpenManus对接Ollama调用本地AI大模型

文章目录 前言1. 环境准备1.1 安装Python1.2. 安装conda 2. 本地部署OpenManus2.1 创建一个新conda环境2.2 克隆存储库2.3 安装依赖环境 3. 安装Ollama4. 安装QwQ 32B模型5. 修改OpenManus配置文件6. 运行OpenManus7.通过网页使用OpenManus8. 安装内网穿透8.1 配置随机公网地址…

计算机网络总结

一、IP地址及子网掩码、MAC 二、DNS、ARP 三、DHCP、UDP、TCP 四、NAT、NAPT、端口、网关 五、路由器与交换机 六、OSI模型 一、IP地址及子网掩码、MAC 1.1 IP地址的作用 用来全局网络通信&#xff08;门牌号&#xff09;用来区分相同网络之间的主机 1.2 子网掩码的作用 …

MySQL0基础学习记录-下载与安装

下载 下载地址&#xff1a; &#xff08;Windows&#xff09;https://dev.mysql.com/downloads/file/?id536787 安装 直接点next&#xff0c;出现&#xff1a; 点execute 然后一直next到这页&#xff1a; next 然后需要给root设置一个密码&#xff1a; 在next。。很多页…

React基础语法速览

一、项目创建 npm create vite 这里选择react即可&#xff0c;如图&#xff1a; 二、基本文件说明 react函数式编程时&#xff0c;用的是JSX语法进行开发的&#xff0c;这里注意&#xff0c;return时只能有一个根标签&#xff1b; 三、React核心语法 1.插值功能 插值可以使用…

IT工具 | node.js 进程管理工具 PM2 大升级!支持 Bun.js

P(rocess)M(anager)2 是一个 node.js 下的进程管理器&#xff0c;内置负载均衡&#xff0c;支持应用自动重启&#xff0c;常用于生产环境运行 node.js 应用&#xff0c;非常好用&#x1f44d; &#x1f33c;概述 2025-03-15日&#xff0c;PM2发布最新版本v6.0.5&#xff0c;这…

teaming技术

一.介绍 在CentOS 6与RHEL 6系统中&#xff0c;双网卡绑定采用的是bonding技术。到了CentOS 7&#xff0c;不仅能继续沿用bonding&#xff0c;还新增了teaming技术。在此推荐使用teaming&#xff0c;因其在查看与监控方面更为便捷 。 二.原理 这里介绍两种最常见的双网卡绑定…

SpringSecurity配置(自定义认证过滤器)

文末有本篇文章的项目源码文件可供下载学习 在这个案例中,我们已经实现了自定义登录URI的操作,登录成功之后,我们再次访问后端中的API的时候要在请求头中携带token,此时的token是jwt字符串,我们需要将该jwt字符串进行解析,查看解析后的User对象是否处于登录状态.登录状态下,将…

【机器学习-模型评估】

“评估”已建立的模型 在进行回归和分类时&#xff0c;为了进行预测&#xff0c;定义了预测函数fθ(x) 然后根据训练数据求出了预测函数的参数θ(即对目标函数进行微分&#xff0c;然后求出参数更新表达式的操作) 之前求出参数更新表达式之后就结束了。但是&#xff0c;其实我…

区块链开发技术公司:引领数字经济的创新力量

在数字化浪潮席卷全球的今天&#xff0c;区块链技术作为新兴技术的代表&#xff0c;正以其独特的去中心化、不可篡改和透明性等特点&#xff0c;深刻改变着各行各业的发展格局。区块链开发技术公司&#xff0c;作为这一领域的先锋和推动者&#xff0c;正不断研发创新&#xff0…

油候插件、idea、VsCode插件推荐(自用)

开发软件&#xff1a; 之前的文章&#xff1a; 开发必装最实用工具软件与网站 推荐一下我使用的开发工具 目前在用的 油候插件 AC-baidu-重定向优化百度搜狗谷歌必应搜索_favicon_双列 让查询变成多列&#xff0c;而且可以流式翻页 Github 增强 - 高速下载 github下载 TimerHo…

Linux中find 命令的高级用法 组合条件 与、或、非(-a、-o、!) 以及通过 -regex 和 -iregex 选项使用正则表达式

find 命令详解 find 是 Unix 和类 Unix 操作系统&#xff08;如 Linux 和 macOS&#xff09;中一个非常强大的命令行工具&#xff0c;用于在文件系统中搜索文件和目录。find 命令可以根据多种条件&#xff08;如文件名、类型、大小、修改时间等&#xff09;进行搜索&#xff0c…

基于Python的垃圾短信分类

垃圾短信分类 1 垃圾短信分类问题介绍 1.1 垃圾短信 随着移动互联科技的高速发展&#xff0c;信息技术在不断改变着我们的生活&#xff0c;让我们的生活更方便&#xff0c;其中移动通信技术己经在我们生活起到至关重要的作用&#xff0c;与我们每个人人息息相关。短信作为移…

go语言中空结构体

空结构体(struct{}) 普通理解 在结构体中&#xff0c;可以包裹一系列与对象相关的属性&#xff0c;但若该对象没有属性呢&#xff1f;那它就是一个空结构体。 空结构体&#xff0c;和正常的结构体一样&#xff0c;可以接收方法函数。 type Lamp struct{}func (l Lamp) On()…