Redis:原理+项目实战——Redis实战3(Redis缓存最佳实践(问题解析+高级实现))

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理+项目实战——Redis实战2(Redis实现短信登录(原理剖析+代码优化))
📚订阅专栏:Redis速成
希望文章对你们有所帮助

Redis实现商铺查询缓存

  • 什么是缓存
  • 给商铺查询功能添加Redis缓存
    • 基础业务逻辑
    • 缓存作用模型与缓存流程
  • 给商品类型添加缓存
  • 缓存更新策略
    • 主动更新策略
      • 更新缓存 or 删除缓存
      • 保证缓存与数据库操作同时成功或失败
      • 线程安全问题(重要问题)
    • 最佳实践方案
  • 实现商铺缓存与数据库的双写一致性
    • 双写一致性的验证

什么是缓存

缓存:数据交换的缓冲区(Cache),是存储数据的临时地方,读写性能高。
缓存的原理学过计算机组成原理、微机、计算机系统等课程的人都会很熟悉。
我们的浏览器有浏览器缓存,在浏览器未命中数据,就会在tomcat的应用层缓层中取数据,再没有命中的话就去数据库进行查询检索。
缓存的作用:
1、降低后端负载
2、提高读写效率,降低响应时间
缓存的成本:
1、数据的一致性成本
2、代码维护成本(解决一致性问题的时候带来的代码复杂)
3、运维的成本

给商铺查询功能添加Redis缓存

基础业务逻辑

在这里插入图片描述
这里的网址就是根据ID查询商户信息的接口,可以看到信息还是很多的:
在这里插入图片描述
所以我们要做的事情就是给这个接口添加缓存,从而提高查询的性能。
我们找到查询商户的业务逻辑代码:
在这里插入图片描述
在这里插入图片描述
这里直接调用了mybatis-plus提供的接口,直接就能实现数据库的根据id查询的操作,我们就在这里进行Redis缓存的添加。

缓存作用模型与缓存流程

学过计算机系统结构,下面的内容还是很容易看懂的,最简单的缓存作用模型如图所示(当然这个模型实际上是有问题的,学过计算机系统结构的会明白,后续开发会优化这个模型):
在这里插入图片描述
知道原理,我们也很容易知道流程该如何进行:
在这里插入图片描述
按照这个流程去实现代码,流程比较多我们就不在controller中去写了,直接让controller层中的方法去调用service层中的方法:
在这里插入图片描述
然后在service中实现相应的业务:

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryById(Long id) {String key = CACHE_SHOP_KEY + id;//从Redis中查询商铺缓存,存储对象可以用String或者Hash,这里用StringString shopJson = stringRedisTemplate.opsForValue().get(key);//判断是否存在if (StrUtil.isNotBlank(shopJson)) {//存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}//不存在,根据id查询数据库Shop shop = getById(id);//不存在,返回错误if (shop == null){return Result.fail("店铺不存在");}//存在,写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));//返回return Result.ok(shop);}
}

刷新页面,用了700多ms访问到该页面:
在这里插入图片描述
查询Redis数据库,发现成功存储:
在这里插入图片描述
再次刷新页面,可以发现拥有缓存后速度快了很多:
在这里插入图片描述
至此,我们已经成功给商户查询增添了Redis缓存。

给商品类型添加缓存

商品类型也就是Shop-List,在很多地方都会用到,比如首页:
在这里插入图片描述
这里的controller层代码逻辑:
在这里插入图片描述
这个代码也是直接使用的mybatis-plus的查询功能,同时对“sort”这个优先级进行排序。

在这里添加Redis缓存,可以使用String也可以使用Hash,因为最终的返回值类型是List的,当然也可以使用List的数据结构。
这里最推荐使用的应该就是Hash了,因为List里面的内容都是对象,Hash是天然适配的。

但在这里我打算使用List来实现,用List实现的话,由于存储到Redis里面的List面向的是String类型的对象,所以我们从Redis取出后、在存入Redis的时候,都需要进行类型的转换。
这里的转换我用了比较高级一点的方式,先把List转换成Stream的形式,然后在map方法中进行转换。

controller层:

	@GetMapping("list")public Result queryTypeList() {//List<ShopType> typeList = typeService//        .query().orderByAsc("sort").list();//return Result.ok(typeList);if (typeService.queryTypeList() == null){return Result.fail("查询出错");}return Result.ok(typeService.queryTypeList());}

service层:

@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {@ResourceStringRedisTemplate stringRedisTemplate;@Overridepublic List<ShopType> queryTypeList() {//查询Redis是否有缓存Long size = stringRedisTemplate.opsForList().size(CACHE_SHOP_TYPE_KEY);//存储到Redis的——存储商品类型的ListList<String> typeList_String;//返还给前端的——存储商品类型的ListList<ShopType> typeList;//Redis里有数据,就转换成对象类型后直接返回数据if (size != 0){//CACHE_SHOP_TYPE_KEY="cache:shopType"typeList_String = stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, size);//将每个String都转换回ShopType类typeList = typeList_String.stream().map(s->{ShopType shopType = JSONUtil.toBean(s, ShopType.class);return shopType;}).collect(Collectors.toList());return typeList;}//Redis里面没有数据,就查询数据库typeList = this.query().orderByAsc("sort").list();if (typeList == null || typeList.isEmpty()){//数据库中不存在,返回nullreturn null;}//数据库里面有,就存入Redis缓存,并返回这个ListtypeList_String = typeList.stream().map(shopType -> {return JSONUtil.toJsonStr(shopType);}).collect(Collectors.toList());//存入Redis,注意插入的方式stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_KEY, typeList_String);//返回return typeList;}
}

刷新页面,响应时间接近600ms:
在这里插入图片描述
查看Redis,已经成功存储,且是按照sort来排序的:
在这里插入图片描述
再次刷新,响应只花了20多ms:
在这里插入图片描述

缓存更新策略

学过计算机系统结构的同学应该知道,上面的作用模型可能会造成数据一致性问题,当我们对数据库进行修改的时候,缓存并没有同步进行修改,然后我们的页面在缓存中获取数据的时候,其实并不是最新的数据。这肯定是不允许的。
下面是缓存更新策略:

内存淘汰超时剔除主动更新
说明不用自己维护,利用Redis的内存淘汰机制,内存不足时自动淘汰部分数据,下次查询时更新缓存给缓存数据添加TTL时间,到期后自动删除缓存。下次查询即可实现缓存的更新自己编写业务逻辑,在修改数据库的同时,更新缓存
一致性一般
维护成本

上述的策略选择要根据具体的业务场景:
1、低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存。
2、高一致性需求:主动更新,以超时剔除作兜底方案。例如店铺详情查询的缓存。

主动更新策略

1、Cache Aside Pattern(最常用)
由缓存的调用者在更新数据库同时更新缓存
2、Read/Write Through Pattern
缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务无需关注一致性问题。但这种服务的成本肯定是很高的。
3、Write Behind Caching Pattern
调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。
比如我们一直对缓存进行更新,更新10次以后轮到这个线程工作,就维护一下数据库的数据为更新10次后的数据,中途的其他9次更新操作根本不重要,这样的性能显然是很高的。
这种方式当然也有很大问题,比如长期的数据不一致、缓存宕机造成的严重后果等。

之后的更新策略我们将会用第一种方式,操作缓存和数据库时有三个问题考虑:

更新缓存 or 删除缓存

更新缓存:每次更新数据库都更新缓存,中途无效写操作太多了
删除缓存:更新数据库时让缓存失效,查询时更新缓存

因此,我们会选择删除缓存

保证缓存与数据库操作同时成功或失败

单体系统:将缓存和数据库操作放到一个事务
分布式系统:利用TCC等分布式事务方案
总之我们要确保数据库与缓存操作的原子性

线程安全问题(重要问题)

我们到底要先删除缓存再操作数据库,还是先操作数据库再删除缓存呢?其实这两种方案都是会遇到问题的,我们可以先分析一下。
1、先删除缓存再操作数据库:
(1)正常情况
在这里插入图片描述
(2)异常情况
线程1工作的时候,线程2可能也要进行操作,就可能会造成数据不一致:
在这里插入图片描述
2、先操作数据库再删除缓存
(1)正常情况
在这里插入图片描述
(2)异常情况
查询缓存未命中,在查询数据库的过程中,数据库更新了,这时候也会造成数据不一致:
在这里插入图片描述
显然,第二种的异常条件是在查询数据库的过程中发生的不一致,相对是更难发生的,因此我们会选择先操作数据库,再删除缓存

最佳实践方案

1、低一致性:使用Redis自带的内存淘汰机制
2、高一致性:主动更新,并以超时剔除作为兜底方案
(1)读操作:
①缓存命中直接返回
②缓存未命中则查询数据库,并写入缓存,设定超时时间(这是一个兜底,万一不一致,超时就剔除)
(2)写操作:
①先写数据库,再删除缓存
②确保数据库与缓存操作的原子性

实现商铺缓存与数据库的双写一致性

通过上面的讨论,现在我们要给查询商铺的缓存添加主动更新超时剔除策略。
修改ShopController的业务逻辑满足:
(1)根据id查询店铺,没命中就查数据库,然后写入缓存,并设置超时时间
(2)根据id修改店铺,先操作数据库,再删除缓存

controller层:

	@PutMappingpublic Result updateShop(@RequestBody Shop shop) {// 写入数据库//shopService.updateById(shop);return shopService.update(shop);}

service层:
1、在queryById函数的写入Redis环节加上时间限定:
在这里插入图片描述
2、根据id更新商铺逻辑:

	@Override@Transactional  //如果操作中有异常,我们用该注解实现事务回滚public Result update(Shop shop) {//我们要首先对id进行判断Long id = shop.getId();if (id == null) {return Result.fail("店铺id不能为空");}//更新数据库updateById(shop);//删除缓存 这里用了单事务,将删除缓存与更新数据库放在了一个事务里面stringRedisTemplate.delete(CACHE_SHOP_KEY + id);return Result.ok();}

我们现在可以进行验证,首先进行查询的验证,当我们打开商铺页面以后,后端会进行数据库查询操作,接着我们打开Redis可以发现:
在这里插入图片描述
说明查询完毕了之后又写到了Redis,并且可以看到时限。

双写一致性的验证

对于修改店铺的验证,这种修改并不是使用前端的用户能进行的,因此我们需要Postman工具。
网页目前是103餐厅:
在这里插入图片描述
2、我们在postman中修改餐厅的名称,并点击send:
在这里插入图片描述
3、查询数据库是否成功修改:
在这里插入图片描述
4、刷新一下Redis,可以发现已经没有了,说明成功进行了缓存删除:
在这里插入图片描述
5、这时候去刷新浏览器,可以发现查询无误,说明确实是在缓存查询不到之后查询了数据库:
在这里插入图片描述
6、再次打开Redis,可以看见,查询完数据库以后,数据写回了Redis:
在这里插入图片描述
自此,流程完全调通!

上述是一些比较基础的内容和最佳实践,但是企业在使用Redis的时候,还会遇到其它难点和热点如缓存穿透、缓存雪崩、缓存击穿,在后续会慢慢解决这些问题。

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

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

相关文章

【计算机毕业设计】SSM游戏点评网站

项目介绍 本项目分为前后台&#xff0c;前台为普通用户登录&#xff0c;后台为管理员登录&#xff1b; 管理员角色包含以下功能&#xff1a; 管理员登录,管理员管理,网站用户管理,游戏资讯管理,游戏类型管理,城市信息管理,竞技场管理,游戏信息管理,游戏评价信息管理等功能。…

使用UDF扩展Spark SQL

Apache Spark是一个强大的分布式计算框架&#xff0c;Spark SQL是其一个核心模块&#xff0c;用于处理结构化数据。虽然Spark SQL内置了许多强大的函数和操作&#xff0c;但有时可能需要自定义函数来处理特定的数据需求。在Spark SQL中&#xff0c;可以使用UDF&#xff08;User…

视频剪辑技巧:制作视频画中画效果,背景图片的选取与运用

在视频剪辑中&#xff0c;画中画效果是一种常用的技巧&#xff0c;它可以在同一屏幕上展示两个或多个视频片段。这种效果在制作教程、比较两个物品或展示对话等场景中非常实用。现在一起来看看云炫AI智剪如何制作视频画中画效果&#xff0c;选取和运用背景图片的方法。 一起来看…

【Apache-2.0】springboot-openai-chatgpt超级AI大脑产品架构图

springboot-openai-chatgpt: 一个基于SpringCloud的Chatgpt机器人&#xff0c;已对接GPT-3.5、GPT-4.0、百度文心一言、stable diffusion AI绘图、Midjourney绘图。用户可以在界面上与聊天机器人进行对话&#xff0c;聊天机器人会根据用户的输入自动生成回复。同时也支持画图&a…

RT-Thread内核移植

目录 前言一、实验平台简介1.1 W601简介1.2 RT-Thread简介1.3 ENV简介 二、开发环境搭建2.1 MDK安装2.2 Git安装2.3 RT-Thread相关下载2.4 其他素材 三、移植RT-Thread四、ENV使用五、W601开发板下载验证5.1 使用串口下载软件5.2 ST-Link下载 前言 本文以正点原子W601开发板为例…

阿里云服务器端口PPTP 1723放行教程

阿里云服务器安装PPTP VPN需要先开通1723端口&#xff0c;阿里云服务器端口是在安全组中操作的&#xff0c;阿里云服务器网aliyunfuwuqi.com来详细说明阿里云服务器安全组开放PPTP VPN专用1723端口教程&#xff1a; 阿里云服务器放行1723端口教程 PPTP是点对点隧道协议&#…

STM32 学习(二)GPIO

目录 一、GPIO 简介 1.1 GPIO 基本结构 1.2 GPIO 位结构 1.3 GPIO 工作模式 二、GPIO 输出 三、GPIO 输入 1.1 传感器模块 1.2 开关 一、GPIO 简介 GPIO&#xff08;General Purpose Input Output&#xff09;即通用输入输出口。 1.1 GPIO 基本结构 如下图&#xff0…

C#使用 OpenHardwareMonitor获取CPU或显卡温度、使用率、时钟频率相关方式

C# 去获取电脑相关的基础信息&#xff0c;还是需要借助 外部的库&#xff0c;我这边尝试了自己去实现它 网上有一些信息&#xff0c;但不太完整&#xff0c;都比较零碎&#xff0c;这边尽量将代码完整的去展示出来 OpenHardwareMonitor获取CPU的温度和频率需要管理员权限 在没…

梯度下降法

前言&#xff1a;在均方差损失函数推导中&#xff0c;我使用到了梯度下降法来优化模型&#xff0c;即迭代优化线性模型中的和。现在进一步了解梯度下降法的含义以及具体用法。 一、梯度下降法(入门级理解&#xff09; 定义&#xff1a;梯度下降是一种用于最小化损失函数的优化…

Python爬取今日头条热门文章

前言 今日头条文章收益是没有任何门槛&#xff0c;只要是你发布文章&#xff0c;每篇文章的阅读量超过1000就能有收益&#xff0c;阅读量越多收益越高。于是乎我就有了个大胆的想法。何不利用Python爬虫&#xff0c;爬取热门文章&#xff0c;然后完成自动化发布文章呢&#xf…

专访 | STIF2023第四届国际科创节访第七在线CEO赵嘉程

12月15日&#xff0c;在STIF2023第四届国际科创节暨数服会上&#xff0c;第七在线获得年度数智化创新典范奖&#xff0c;第七在线CEO赵嘉程在颁奖典礼现场接受了媒体专访。 主持人&#xff1a;赵总&#xff0c;您好&#xff0c;欢迎您接受我们的专访&#xff0c;首先我们特别想…

杰发科技AC7840——Eclipse环境DMA注意事项

0.序 用 户 使 用 DMA 时 &#xff0c; 所 有 DMA 搬 运 的 SRAM 数 据 都 必 须 存 放 在 SRAM_U 区 (0x20000000~0x2000EFFF) 。 1. 修改办法 第一步&#xff1a; RAM定义 /* Specify the memory areas */ MEMORY {FLASH (rx) : ORIGIN 0x00000000, LENGT…

dyld: Library not loaded: /usr/lib/swift/libswiftCoreGraphics.dylib

更新Xcode14后低版本iPhone调试报错 dyld: Library not loaded: /usr/lib/swift/libswiftCoreGraphics.dylib Referenced from: /var/containers/Bundle/Application/…/….app/… Reason: image not found 这是缺少libswiftCoreGraphics库 直接导入libswiftCoreGraphics库即…

【新手小白的xsslab靶场学习】

第1关 最开始页面源代码 直接输入<script>alert(1)</script> 第2关 页面源代码 先尝试<script>alert(1)</script>看页面源代码 <h2>里面尖括号被编码&#xff0c;<input>里面没有编码,直接双引号闭合&#xff0c; 修改payload&…

STM32F407-14.3.10-表73具有有断路功能的互补通道OCx和OCxN的输出控制位-01x00

如上表所示&#xff0c;MOE0&#xff0c;OSSI1&#xff0c;CCxE0&#xff0c;CCxNE0时&#xff0c;OCx与OCxN的输出状态取决于GPIO端口上下拉状态。 ---------------------------------------------------------------------------------------------------------------------…

基于RetinaFace+Jetson Nano的智能门锁系统——第二篇(配置环境)

文章目录 设备一、安装远程登录终端Xshell1.1下载Xshell1.2新建回话1.3查询ip地址1.4启动连接 二、安装远程文件管理WinScp2.1下载WinScp2.2连接Jetson Nano2.3连接成功 三、安装远程桌面VNC Viewer3.1下载VNC Viewer3.2在Jetson Nano安装VNC Viewer3.3设置VINO登录选项3.4将网…

单片机原理及应用:计数按键控制数码管显示

承接上文&#xff0c;我们来介绍一下按键和数码管的配合工作&#xff0c;由于数码管显示的字符和位数多种多样&#xff0c;无法做到一个字符对应一个按键&#xff0c;所以程序主要记录按键的使用次数来切换数码管的显示。 #include <reg52.h> //包含reg52.h头…

opencv003图像裁剪(应用NumPy矩阵的切片)

这一部分相对于马上要学习的二值化是要更更更简单一些的&#xff0c;只需三行&#xff0c;便能在opencv上裁剪图像啦&#xff08;顺便云吸猫&#xff0c;太可爱了&#xff01;&#xff09; 出处见水印&#xff01; 1、复习图像的显示 前几天期末考试&#xff0c;太久没有看…

第28关 k8s监控实战之Prometheus(一)

------> 课程视频同步分享在今日头条和B站 大家好&#xff0c;我是博哥爱运维。对于运维开发人员来说&#xff0c;不管是哪个平台服务&#xff0c;监控都是非常关键重要的。 在传统服务里面&#xff0c;我们通常会到zabbix、open-falcon、netdata来做服务的监控&#xff0…

error C2666: “Date::operator ==”: 重载函数具有类似的转换

error C2666: “Date::operator ”: 重载函数具有类似的转换 1.错误描述2.解决方案 1.错误描述 class Date { public:Date(int year 2024, int month 1, int day 1){_year year;_month month;_day day;}bool operator(const Date& d){return _year d._year&&…