Redis实战—商户查询缓存

news2025/1/24 11:42:14

本博客为个人学习笔记,学习网站:黑马程序员Redis入门到实战 实战篇之商户查询缓存

目录

什么是缓存

添加Redis缓存

缓存更新策略

数据库缓存不一致解决方案

案例:给查询商铺的缓存添加超时剔除和主动更新策略

缓存穿透

案例:实现缓存空对象方案 

缓存穿透小结 

 缓存雪崩

缓存击穿

基于互斥锁解决缓存击穿逻辑

基于逻辑过期解决缓存击穿逻辑

互斥锁与逻辑过期对比总结 

基于互斥锁方式解决缓存击穿问题

基于逻辑过期方式解决缓存击穿问题


什么是缓存

1. 缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。
2. 缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,因此缓存可以大大降低用户访问并发量带来的服务器读写压力。
3. 实际开发过程中,企业的数据量少则几十万,多则几千万,这么大数据量,如果没有缓存来作为"避震器",系统是几乎撑不住的,所以企业会大量运用到缓存技术。

但是缓存也会增加代码复杂度和运营的成本:
缓存的作用:
降低后端负载、提高读写效率,降低响应时间
缓存的成本:数据一致性成本:确保缓存和数据库中的数据是一致的


添加Redis缓存

原始的根据id查询商铺代码(直接访问数据库拿到商铺信息) 

/**
 * 根据id查询商铺信息
 * @param id 商铺id
 * @return 商铺详情数据
 */
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
    return Result.ok(shopService.getById(id));
}

现在我们需要对根据id查询商铺这段代码进行改造,为其添加Redis缓存

修改controller层代码

@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
    return shopService.queryById(id);
}

修改Service层代码

//Service接口类代码
Result queryById(Long id);


//Service接口实现类代码
@Override
public Result queryById(Long id) {
    String key = "cache:shop:" + id;
    // 1.从redis查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        // 3.若存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    // 4.若不存在,根据id查询数据库
    Shop shop = getById(id);
    // 5.若数据库不存在该商铺,返回错误
    if (shop == null) 
        return Result.fail("店铺不存在!");
    
    // 6.若存在,将该商铺信息写入redis
    stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
    // 7.返回
    return Result.ok(shop);
}

缓存更新策略

缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行筛选更新,或者淘汰。


数据库缓存不一致解决方案

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

解决缓存与数据库问题的方案有如下3种:(详细内容可参考视频P38黑马程序员Redis从入门到实战)
1. Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案。
2. Read/Write Through Pattern : 缓存与数据库整合为一个服务由系统本身去维护一致性。调用者调用该服务,无需关心缓存一致性问题。
3. Write Behind Caching Pattern :调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。

综合考虑使用方案一,但是方案一调用者如何处理呢?这里有几个问题:
1. 当我们对数据库中的数据进行修改时,我们应该选择删除对应缓存还是更新对应缓存?
更新缓存:若每次更新数据库时都更新缓存,如果在一连串的更新中都没有对数据进行查询,那么只有最后一次更新是有意义的,中间的更新产生的无效写操作较多(X)
删除缓存:更新数据库时让删除缓存,等待下次查询时再把数据从数据库中更新到缓存  (✔)

2. 为确保缓存与数据库的数据一致性,应该如何保证缓存与数据库的操作同时成功或失败?
单体系统:将缓存与数据库操作放在一个事务
分布式系统:利用TCC等分布式事务方案

3. 我们应该选择先操作缓存还是先操作数据库?
先删除缓存,再操作数据库(X)
先操作数据库,再删除缓存  (✔)
解析(见下图,不要嫌麻烦,建议直接看视频理解更快 P38黑马程序员Redis从入门到实战):我们应选择先操作数据库,再删除缓存。原因在于,如果你选择第一种方案,在两个线程并发来访问时,假设线程1先来,它先把缓存删了,此时线程2过来,由于线程2查询缓存数据并不存在,此时线程2写入缓存,当线程2写入缓存后,线程1再执行更新动作时,实际上线程2写入缓存的就是旧数据,导致缓存中新数据被旧数据覆盖了。


案例:给查询商铺的缓存添加超时剔除和主动更新策略

需求1. 修改之前接口实现类中的部分代码,为其添加设置超时时间:如果在缓存中未找到所需数据,则从数据库查询,在将该数据填入缓存的同时为其设置超时时间。

stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);

需求2. 根据id修改店铺时,先修改数据库,再删除缓存

Controller层代码

    /**
     * 更新商铺信息
     *
     * @param shop 商铺数据
     * @return 无
     */
    @PutMapping
    public Result updateShop(@RequestBody Shop shop) {
        // 写入数据库
        return shopService.update(shop);
    }

Service层代码

//Service接口类方法
Result update(Shop shop);

//Service接口实现类代码
@Override
@Transactional
public Result update(Shop shop) {
    if (shop.getId() == null) {
        return Result.fail("店铺id不能为空");
    }
    // 1.更新数据库
    updateById(shop);
    // 2.删除缓存
    stringRedisTemplate.delete("cache:shop:" + shop.getId());
    // 3.返回结果
    return Result.ok();
}

缓存穿透

案例:实现缓存空对象方案 


修改和新增查询商铺Service层接口实现类中的代码
1.代码修改: 在数据库查询不到该商铺信息时,向缓存中添加空值。


2. 代码新增:新增判断代码:当拿到缓存中的值,并判断该值为空(""或null)后,再次判断该值是否为空值(""),如果为空值,则说明该空值是先前在数据库查找不到店铺信息时向缓存写入的,间接说明数据库不存在该店铺信息,因此直接返回结果信息即可。


缓存穿透小结 

缓存穿透产生的原因是什么?
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求给数据库带来巨大压力。

缓存穿透的解决方案有哪些?
1. 缓存null值
2. 布隆过滤
3. 增强id的复杂度,避免被猜测id规律,从而发起恶意请求
4. 做好数据的基础格式校验
5. 加强用户权限校验
6. 做好热点参数的限流


 缓存雪崩

缓存雪崩这部分内容视频里只做了介绍,没有给案例学习。 


缓存击穿

相比缓存雪崩是大量的key失效,缓存击穿指的是部分特殊的key失效带来的问题。缓存击穿问题也称作热点Key问题,就是一些被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大

缓存击穿的常见解决方案有:
1.互斥锁  2.逻辑过期


基于互斥锁解决缓存击穿逻辑


因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。


基于逻辑过期解决缓存击穿逻辑

我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,数据不会失效,就不会有缓存击穿的问题。但是不设置过期时间,我们怎么能知道数据是否过期呢,因此我们选择采用逻辑过期方案。

我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个单独的线程去进行以前重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。


互斥锁与逻辑过期对比总结 

互斥锁方案:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响。

逻辑过期方案:线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦。


基于互斥锁方式解决缓存击穿问题

核心思路:相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,过一会再进行尝试,直到获取到锁为止,才能进行查询

如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿


基于逻辑过期方式解决缓存击穿问题

思路分析:当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。

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

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

相关文章

【书生·浦语大模型实战营】第6节 OpenCompass 大模型评测 课后作业

OpenCompass 大模型评测 0. 课程链接1. 课后作业1.1 基础作业1.2 进阶作业 2. 结业总结 0. 课程链接 链接:https://github.com/InternLM/tutorial/blob/main/opencompass/opencompass_tutorial.md 1. 课后作业 1.1 基础作业 使用 OpenCompass 评测 InternLM2-Cha…

Map集合体系——遍历,HashMap,TreeMap,LikedHashMap

认识Map集合 Map集合体系特点 方法 代码示例 package com.zz.Map;import java.util.*;public class Test {public static void main(String args[]){Map<String, Integer> map new HashMap <>();//经典代码&#xff0c;按照键 无序 不重复 无索引map.put("…

Android岗大厂面试官常问的那些问题,2024年Android者未来的出路在哪里

前言 伟人曾经说过&#xff1a; 书是人类进步的阶梯 书中自有黄金屋&#xff0c;书中自有颜如玉 读书破万卷&#xff0c;下笔如有神 书是唯一不死的东西。 书籍是伟大的天才留给人类的遗产。 最近有很多朋友在我的公众号上提问“Android开发的经典入门教材和学习路线&#xff…

机械臂中TCP配置

红色、绿色、蓝色箭头分别代表x、y、z三个方向

Swing程序设计(11)动作事件监听器,焦点事件监听器

文章目录 前言一、事件监听器是什么&#xff1f;二、详细展开 1.动作事件监听器2.焦点事件监听器总结 前言 如果你是坚持从Swing程序第一篇看到了这里&#xff0c;恭喜你&#xff0c;Swing程序设计简单地落下了帷幕&#xff0c;关于Swing程序更深的了解&#xff0c;可以自行学习…

MySQL下实现纯SQL语句的递归查询

需求 有一个部门表&#xff0c;部门表中有一个字段用于定义它的父部门&#xff1b; 在实际业务中有一个『部门中心』的业务&#xff1b; 比如采购单&#xff0c;我们需要显示本部门及子部门的采购单显示出来。 结构 数据如下&#xff1a; 实现方式如下&#xff1a; WITH RECUR…

实验笔记之——Gaussian Splatting SLAM配置与测试

之前博客对基于3DGS的SLAM进行了调研 学习笔记之——3D Gaussian Splatting及其在SLAM与自动驾驶上的应用调研_3d gaussian splatting slam-CSDN博客文章浏览阅读3.2k次&#xff0c;点赞40次&#xff0c;收藏58次。论文主页3D Gaussian Splatting是最近NeRF方面的突破性工作&a…

OBS插件开发(二)推流实时曲线

不发视频了&#xff0c;截个图算了&#xff0c;嫌麻烦 1&#xff0c;自定义QWidget图表绘制 &#xff0c;动态更新 2&#xff0c;OBS直播帧率&#xff0c;码率监控 3&#xff0c;主要用于前端推流状况可视化&#xff0c;异常报警&#xff0c;及时性&#xff0c;无人值守直播

springcloud:3.6测试信号量隔离

服务提供者【test-provider8001】 Openfeign远程调用服务提供者搭建 文章地址http://t.csdnimg.cn/06iz8 相关接口 测试远程调用&#xff1a;http://localhost:8001/payment/index 服务消费者【test-consumer-resilience4j8004】 Openfeign远程调用消费者搭建 文章地址http://t…

如何制作一个简单html网页

要制作一个简单的HTML网页&#xff0c;可以按照以下步骤进行&#xff1a; 创建一个新的文本文件并将其保存为.html文件&#xff08;例如&#xff0c;index.html&#xff09;。 打开文本文件&#xff0c;并使用以下基本的HTML结构开始编写代码&#xff1a; <!DOCTYPE html…

HplusAdmin ASP.NET基本权限管理系统

HplusAdmin 介绍 一套ASP.NET WebForm(不用控件) hplusasp.netsqlserver 基本权限管理系统 http://hplus.baocaige.top 暂不开源&#xff0c;需要的滴滴或者留下邮箱&#xff01;&#xff01;&#xff01; 账号 普通账号 账号&#xff1a;user 密码&#xff1a;Aa123456普…

土地利用数据分类过程教学/土地利用分类/遥感解译/土地利用获取来源介绍/地理数据获取

本篇主要介绍如何对影像数据进行分类解译&#xff0c;及过程教学&#xff0c;示例数据下载链接&#xff1a;数据下载链接 一、背景介绍 土地是人类赖以生存与发展的重要资源和物质保障&#xff0c;在“人口&#xff0d;资源&#xff0d;环境&#xff0d;发展&#x…

Linux 之三:CentOS7 目录结构 和 日期及时区设置

Linux 目录 以下是对这些目录的解释&#xff1a; /bin&#xff1a;bin是Binary的缩写, 这个目录存放着最经常使用的命令。/boot&#xff1a; 这里存放的是启动Linux时使用的一些核心文件&#xff0c;包括一些连接文件以及镜像文件。/dev &#xff1a; dev是Device(设备)的缩写…

Git快速上手二

对Git命令的深入理解快速上手Git&#xff08;包含提交至GitHub和Gitee&#xff09;-CSDN博客 1.5 分支操作 1.5.1 分支原理 系统上线后,又要修改bug,又要开发新的功能。 由于新功能没有开发完,所以需要建立分支,一边修改bug,一边开发新功能,最终合并. 1.5.2 分支实操 创建…

React 的入门介绍

React 是什么 React是一个用于构建用户界面的JavaScript库。它由Facebook开发&#xff0c;并于2013年首次发布。React将用户界面拆分为小的可重用组件&#xff0c;每个组件都有自己的状态&#xff0c;并根据状态的变化来更新界面。 React使用了虚拟DOM&#xff08;Virtual DO…

[ISP]DCT离散余弦变换及C++代码demo

1.基本定义 离散余弦变换(DCT for Discrete Cosine Transform) DCT&#xff08;Discrete Cosine Transform&#xff0c;离散余弦变换&#xff09;是一种常用的信号处理技术&#xff0c;广泛应用于图像处理、音频处理、视频压缩等领域。DCT将一个信号或数据序列从时域&#xf…

最新JVM面试题(持续更新)

JVM 1 为什么需要JVM&#xff0c;不要JVM可以吗&#xff1f; JVM可以帮助我们屏蔽底层的操作系统 一次编译&#xff0c;到处运行 JVM可以运行Class文件 2 JDK&#xff0c;JRE以及JVM的关系 3 我们的编译器到底干了什么事&#xff1f; 仅仅是将我们的 .java 文件转换成了 .c…

数据库系列之:什么是 SAP HANA?

数据库系列之&#xff1a;什么是 SAP HANA&#xff1f; 一、什么是 SAP HANA&#xff1f;二、什么是内存数据库&#xff1f;三、SAP HANA 有多快&#xff1f;四、SAP HANA 的十大优势五、SAP HANA 架构六、数据库设计七、数据库管理八、应用开发九、高级分析十、数据虚拟化 一、…

原生IP是什么?如何获取海外原生IP?

一、什么是原生IP 原生IP地址是互联网服务提供商&#xff08;ISP&#xff09;直接分配给用户的真实IP地址&#xff0c;无需代理或转发。这类IP的注册国家与IP所在服务器的注册地相符。这种IP地址直接与用户的设备或网络关联&#xff0c;不会被任何中间服务器或代理转发或隐藏。…

后台组件-语言包

<groupId>org.qlm</groupId><artifactId>qlm-language</artifactId><version>1.0-SNAPSHOT</version> 平台提供多语言支持&#xff0c;以上为语言包&#xff0c;提供后台多语言支持。首批实现&#xff1a; public class LanguageConstan…