Redis - 缓存场景

学习资料

学习的黑马程序员哔站项目黑马点评,用作记录和探究原理。

Redis缓存

缓存 :就是数据交换的缓冲区,是存储数据的临时地方,读写性能较高
缓存常见的场景:

  • 数据库查询加速:通过将频繁查询的数据缓存起来,减少对数据库的直接访问,提高查询效率。
  • 网页内容缓存:将动态生成的网页内容缓存起来,减少服务器生成页面的次数,提高网页响应速度。
  • 会话数据缓存:将用户会话数据缓存起来,支持分布式系统中的会话共享。
  • 队列系统:将任务队列缓存起来,支持高效的任务调度和执行。
    在这里插入图片描述
使用Redis缓存的流程

实际上,读取数据库是很消耗资源的。假设现在有一大批用户短时间内请求数据库,数据库此时将面临巨大的压力。为了缓解这种压力,可以使用Redis来缓存频繁访问的数据,减少对数据库的直接访问。
在这里插入图片描述
具体流程如下:
检查缓存:首先检查Redis缓存中是否存在所需数据。

如果缓存中存在数据,则直接返回数据给用户,结束流程。
如果缓存中不存在数据,则继续下一步。

查询数据库:从数据库中查询所需数据。

查询到数据后,将数据返回给用户,同时将数据写入Redis缓存,以便下次快速访问。
如果未查询到数据,则返回空或相应的提示信息。

更新缓存:当数据库中的数据发生变化时,需要同步更新Redis缓存中的数据,确保数据的一致性。通常可以使用以下两种策略:

主动更新:在数据库更新的同时,主动更新缓存中的数据。
被动更新:当检测到缓存中的数据已失效时,再次从数据库中查询数据并更新缓存。

更新策略问题

为了保证缓存一致问题,我们在选择更新策略的时候一般选择主动更新的策略,

被动更新不一致问题

因为被动更新会出现不一致情况,比如说我们现在将一个店铺信息存入数据库,策略选择的被动策略,但是查询店铺信息我们选择的是使用Redis缓存,此时永远不回去更新这个店铺,因为我页面呈递不出来这个店铺信息,怎么能去请求他然后查询数据库更新呢?这就出现不一致的情况了。
对于这种情况再进行一个说明,我这边说的场景就是说我们把这个商品的列表信息存入Redis,当添加一个新的商品之后,缓存中没有,那么前端中就指定不会有,此时就很尴尬的一个场景。但是说一般不会这样玩,一般只是把商品详情信息存入Redis,商品列表还是查询数据库,毕竟不是很多,并且这样的话避免出现不同步问题。

主动更新 缓存更新时机选择

主动更新的时候时机选择也是比较重要的,当我更新数据库一条消息的时候,此时我是选择先将数据存入数据库还是说先将Redis中的数据删除? 或者我向数据库添加一个消息,我是先存入数据库,还是先存入Redis中呢?
我们来看一下这张图

先删除缓存再更新数据库

在这里插入图片描述
场景先删除缓存,然后操作更新数据库,这个时候我们面临一个问题,由于就是说第一次请求肯定是先请求缓存中的,当缓存没有的时候此时请求数据库,我们假设第一个用户A此时更新了数据库中一件商品的信息,选择先把缓存中旧的信息删除,然后将数据库更新,那么此时用户A更新数据库的时候,用户B来访问这个信息,先看缓存,缓存中没有,那么此时肯定得查询数据库,数据库中还没更新成功,那么这个数据查询依旧是旧的值,这样不就出现问题了吗,这样的话就出现一个场景,由于就是更新数据库耗时久,那么这个时候出现大量请求这个内容,那么持续请求数据库,会造成压力,二就是此时数据也不一致。

先更新数据库,再删除缓存。

在这里插入图片描述
在处理缓存和数据库更新时,我们选择先更新数据库,然后删除缓存。这样做有几个优势:
降低数据库查询压力:
当用户访问该信息时,缓存中仍然是旧的信息。虽然这意味着短时间内用户可能获取到的是旧数据,但由于缓存命中,减少了对数据库的直接查询,降低了数据库的查询压力。
确保数据一致性:
一旦数据库更新完成,再删除缓存。此时,缓存被清除,下一次用户访问该信息时,会从数据库读取最新的数据并重新加载到缓存中。这种方式保证了数据的一致性,确保缓存中不会出现过时的数据。

针对缓存不一致问题我了解的解决方案有以下几种:
  1. 延迟双删策略(Double Deletion with Delay)
    在更新数据库后,通过延迟再次删除缓存以确保数据的一致性。
// 更新数据库
updateDatabase();
// 删除缓存
deleteCache();
// 延迟一段时间后再删除缓存
Thread.sleep(500);  // 延迟时间根据具体场景调整
deleteCache();
  1. 互斥锁机制(Mutex Locking Mechanism)
    使用分布式锁来避免多个请求同时更新数据库或缓存。确保只有一个请求可以访问数据库并更新缓存。
String value = getCache(key);
if (value == null) {
    if (lock(key)) {
        try {
            value = queryDatabase();
            setCache(key, value);
        } finally {
            unlock(key);
        }
    } else {
        // 等待锁释放后重试
        Thread.sleep(50);  // 重试时间根据具体场景调整
        value = getCache(key);
    }
}
return value;

  1. 缓存预热(Cache Warming)
    在应用启动时或缓存失效时,预先加载常用的数据到缓存中,减少缓存穿透的概率。
// 在应用启动时加载常用数据到缓存
loadCommonDataToCache();

  1. 读写分离(Read-Write Separation)
    将读操作与写操作分离,写操作使用主数据库,读操作使用从数据库,减轻数据库压力。
// 写操作使用主数据库
updateMainDatabase();

// 读操作使用从数据库
value = queryReadReplicaDatabase();

缓存场景会出现的问题

缓存穿透

是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。常见的解决方案有两种。

  • 缓存空对象
    也就是说我们此时查询数据库发现这个内容是空的,我们避免下一次有人把继续访问这个空的内容,然后造成服务器压力,那么此时我们把这个空的内容缓存到Redis中,此时当再次请求这个内容的时候,请求的是Redis中的内容,对服务器压力会降低很多。
步骤:

客户端请求数据。
检查缓存,发现没有命中。
查询数据库,发现数据不存在。
将空对象(如null或者特殊标识)缓存到Redis中,并设置一个合理的过期时间。
当再次请求这个内容时,直接从Redis中获取空对象,减少数据库压力。
在这里插入图片描述

  • 布隆过滤
    布隆过滤器是一种概率型数据结构,可以高效地判断某个元素是否在一个集合中。通过将所有可能存在的缓存键值存储在布隆过滤器中,可以快速判断某个请求是否是无效的(即数据库中也不存在),从而减少对数据库的查询。
步骤:

初始化布隆过滤器,将所有可能存在的键值添加到过滤器中。
客户端请求数据时,先检查布隆过滤器。
如果布隆过滤器判断数据不存在,直接返回空结果,避免查询数据库。
如果布隆过滤器判断数据可能存在,再查询缓存和数据库。
示例代码:

// 初始化布隆过滤器,并添加所有可能存在的键值
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1000000);
bloomFilter.putAll(getAllPossibleKeys());

String key = getRequestKey();
if (!bloomFilter.mightContain(key)) {
    // 布隆过滤器判断数据不存在,直接返回空结果
    return null;
}

String value = getCache(key);
if (value == null) {
    value = queryDatabase(key);
    if (value == null) {
        // 数据不存在,缓存空对象
        setCache(key, "NULL", 300);  // 300秒过期时间,可根据具体情况调整
    } else {
        // 数据存在,缓存实际数据
        setCache(key, value);
    }
}
if ("NULL".equals(value)) {
    // 返回数据不存在的响应
    return null;
}
return value;
缓存穿透的解决方案
  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验

缓存雪崩

缓存雪崩主要是由于一段时间内,缓存的key值全部失效,也就是缓存期结束,导致大量请求到达数据库,带来巨大的压力。
那么这个场景的解决方案也很简单。
在这里插入图片描述

缓存击穿

缓存击穿被称为热点key 问题,就是一种被高并发访问并且缓存重建业务较复杂的key,失效了,大量请求访问的瞬间给数据库带来巨大压力
假设一家店突然推出一家新的菜品,但是说此时缓存过程中数据库更新了,将旧的Redis缓存删除之后,此时大量用户访问这个内容的话会出现一个场景,形成一个闭环。
在这里插入图片描述
这样不就出现大量请求数据库的场景了吗?我们要怎么避免这个内容呢
这个解决方案有两种:

方案一:使用互斥锁

相信大家在学习javase阶段的多线程肯定面临一个问题,也就是说多线程抢票问题,当不加锁的情况下会出现超卖问题,加一个锁一次只能抢一个,这样会更好。这个位置也就是说更新缓存的时候加一个锁,让其他业务线程不进行更新操作。
但是说这个锁应该选什么呢?现在既然在使用Redis,那么这个时候我们就直接选择使用Redis中字符串类型的SETNX 作为互斥锁。

方案二:使用逻辑过期

将Redis缓存中的内容设置一个逻辑过期字段,保证在读取缓存时,可以判断数据是否过期。这样可以减少缓存穿透和击穿问题。
具体步骤:
客户端请求数据。
检查缓存,获取缓存数据和逻辑过期时间。
如果数据未过期,直接返回缓存数据。
如果数据已过期或缓存未命中:
启动一个异步线程更新缓存。
返回旧数据或提示正在更新中。

缓存击穿 - Java代码解决

互斥锁解决

互斥锁这边使用的锁对象是Redis 中String类型的SETNX,由于其一个键只能赋值一次,这样的话符合预期场景。
在这里插入图片描述
这样写的话,会保证不会出现连续查询数据库,保证当一个值改变之后只更新一次数据库操作。
其实也就是说,当我线程一更新的时候,我先判断指定的锁的key是否存在,存在将他设置,此时相当于线程一获取了锁,那么县城二进入的时候便不可以获取道这个锁对象,那么线程二就需要等待,等待之后重试。直到缓存命中才结束。

场景:书写接口查询店铺的详细信息。其中id是店铺的标识
    public Shop queryWithPassThrough(Long id) {
        //         店铺在Redis中的key规范 同一前缀 + 店铺id
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        //         查询redis缓存中 是否会有这个内容
        String s = stringRedisTemplate.opsForValue().get(key);
        //         存在 直接返回
        if (StrUtil.isNotBlank(s)) {
            return JSONUtil.toBean(s, Shop.class);
        }
        /*
         *   既然不是null  那一定是  ""
         *   这样的话我们需要进行 返回错误了 防止继续去查询数据库
         *   这个是缓存穿透的一个防护手段 上面讲解过了
         * */
        if (s != null) {
            return null;
        }
        // 实现缓存重建
        // 获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;

        Shop byId = null;
        try {
//            获取锁 直接使用String中的setnx即可 判断是否获取成功
            boolean b = tryLocal(lockKey);
//            失败
            if (!b) {
                // 休眠
                Thread.sleep(50);
                // 重试 重新调用递归
                queryWithPassThrough(id);
            }
            //         不存在 查询数据库
            byId = getById(id);
            //        不存在 返回错误
            if (byId == null) {
                /*
                 *   写入空值内容 防止 持续 缓存穿透 造成服务器资源浪费
                 *   时间设置成 2 min
                 * */
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //        数据库存在
            //         将数据放入redis中
            /*
             *   这个位置书写一个超时设置 避免资源浪费
             *   主要是为了解决
             *   缓存不一致问题
             * */
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(byId), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
            //         返回数据
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
//            释放锁
            delLocal(lockKey);
        }
//        返回店铺详情
        return byId;
    }
    private boolean tryLocal(String key) {
        Boolean judge = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(judge);
    }

    private void delLocal(String key) {
        stringRedisTemplate.delete(key);
    }

逻辑删除

逻辑删除 ,也就是向储存的数据中加一个字段,这个字段就是过期时间,这边设置的实体是按照这种格式设计的内容。

package com.hmdp.utils;

import lombok.Data;

import java.time.LocalDateTime;

/*
*   逻辑过期时间实体
* */
@Data
public class RedisData {
    private LocalDateTime expireTime;
    // 存入的数据
    private Object data;
}

我们缓存重建的时候,需要将逻辑过期时间重置,所以这个时候我们需要封装一个方法来重置逻辑过期时间。

    public void saveShopToRedis(Long id, Long seconds) {
//        1.查询店铺数据
        Shop byId = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(byId);
//        2.封装逻辑过期时间
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(seconds));
//        3.写入redis
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

逻辑过期代码 - 其中缓存重建新开一个线程使用。

public Shop queryWithLogicalExpire(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
//         查询redis缓存中 是否会有这个内容
        String s = stringRedisTemplate.opsForValue().get(key);
//         存在 直接返回
        if (StrUtil.isNotBlank(s)) {
            return JSONUtil.toBean(s, Shop.class);
        }
        /*
         *   既然不是null  那一定是  ""
         *   这样的话我们需要进行 返回错误了 防止继续去查询数据库
         *   这个是缓存穿透的一个防护手段
         * */
        if (s != null) {
            return null;
        }
        RedisData bean = JSONUtil.toBean(s, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) bean.getData(), Shop.class);
        LocalDateTime localDateTime = bean.getExpireTime();

        if (localDateTime.isAfter(LocalDateTime.now())) {
            return shop;
        }

        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean b = tryLocal(lockKey);
        if (b) {
            try {
                CACHE_REBUILD_EXCUTOR.submit(() -> {
                    this.saveShopToRedis(id, RedisConstants.CACHE_SHOP_TTL);
                });
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
//            释放锁
                delLocal(lockKey);
            }
        }
        return shop;
    }

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C从零开始实现贪吃蛇大作战

个人主页&#xff1a;星纭-CSDN博客 系列文章专栏 : C语言 踏上取经路&#xff0c;比抵达灵山更重要&#xff01;一起努力一起进步&#xff01; 有关Win32API的知识点在上一篇文章&#xff1a; 目录 一.地图 1.控制台基本介绍 2.宽字符 1.本地化 2.类项 3.setlocale函…

推荐10款优秀的组件库(一)

1.Ant Desgin UI 网址&#xff1a; https://ant-design-mobile.antgroup.com/zh Ant Design - 一套企业级 UI 设计语言和 React 组件库 "Ant Design Mobile"是一个在线的移动端Web体验平台&#xff0c;让你探索移动端Web的体验极限。 添加图片注释&#xff0c;不…

5月26(信息差)

&#x1f30d; 珠峰登顶“堵车”后冰架断裂 5人坠崖 2人没爬上来&#xff01; 珠峰登顶“堵车”后冰架断裂 5人坠崖 2人没爬上来&#xff01; &#x1f384; Windows 11 Beta 22635.3646 预览版发布&#xff1a;中国大陆地区新增“微软电脑管家”应用 ✨ 成都限购解除即将满…

5.23.12 计算机视觉的 Inception 架构

1. 介绍 分类性能的提升往往会转化为各种应用领域中显着的质量提升&#xff0c;深度卷积架构的架构改进可用于提高大多数其他计算机视觉任务的性能&#xff0c;这些任务越来越依赖于高质量的学习视觉特征。在 AlexNet 功能无法与手工设计、制作的解决方案竞争的情况下&#xf…

能找伴侣的相亲婚恋平台有哪些?6款值得信赖的恋爱交友软件体验测评

在这个超快节奏的社会里&#xff0c;好多人都忙着搞事业和搞钱&#xff0c;却把终身大事给忽略了。但是随着年龄越来越大&#xff0c;来自长辈和社会的压力也越来越大&#xff0c;因此网络上的相亲交友软件&#xff0c;就成了大多数单身贵族的脱单首选了。下面就来给大家讲讲我…

Day06:Flex 布局

目标&#xff1a;熟练使用 Flex 完成结构化布局 一、标准流 标准流也叫文档流&#xff0c;指的是标签在页面中默认的排布规则&#xff0c;例如&#xff1a;块元素独占一行&#xff0c;行内元素可以一行显示多个。 二、浮动 1、基本使用 作用&#xff1a;让块元素水平排列。 …

【C++题解】1698. 请输出带有特殊尾数的数

问题&#xff1a;1698. 请输出带有特殊尾数的数 类型&#xff1a; 题目描述&#xff1a; 请输出1∼n 中所有个位为 1、3、5、7中任意一个数的整数&#xff0c;每行 1 个。( n<1000 ) 比如&#xff0c;假设从键盘读入 20&#xff0c;输出结果如下&#xff1a; 1 3 5 7 11 1…

树莓派4B 有电但无法启动

试过多个SD卡&#xff0c;反复烧系统镜像都无法启动。接HDMI显示器没有信号输出&#xff0c;上电后PWR红灯长亮&#xff0c;ACT绿灯闪一下就不亮了&#xff0c;GPIO几个电源脚有电&#xff0c;芯片会发热&#xff0c;测量多个TP点电压好像都正常。 ……

N进制计数器【01】

N进制计数器 前面介绍过二进制计数器和十进制计数器&#xff0c;但是在很多时候需要到其他进制的计数器&#xff0c;我们把这些任意进制的计数器简称为 N 进制计数器 设计 N 进制计数器的方法有两种&#xff1a; 用时钟触发器和门电路设计&#xff08;前面常用的方法&#xf…

【Telemac】Telemac相关报错记录

文章目录 1.下载BlueKenue后缀为man解决办法2.运行Telemac项目提示Fortran报错解决办法1.下载BlueKenue后缀为man BlueKenue官方下载链接: 可以看到下载器请求时出现了问题,下载BlueKenue后缀为man. 解决办法 修改下载后的文件后缀为msi即可 2.运行Telemac项目提示Fortr…

Git时光机、Git标签、Git分支、GitHub协作

Git时光机&#xff08;切换版本&#xff09; 1.查看提交历史 HEAD指针指向这次分支的最后一次提交 版本信息一行显示【git log --prettyoneline】 2.引用日志【git reflog】 &#xff08;只在自己的工作区中存在&#xff09; 非常重要&#xff1a;当HEAD指针进行切换之后&…

el-switch自动触发更新事件

比如有这样一个列表&#xff0c;允许修改单条数据的状态。希望在更改el-switch状态时能够有个弹框做二次确认&#xff0c;没问题&#xff0c;el-switch已经帮我们想到了&#xff0c;所以它提供了beforeChange&#xff0c;根据beforeChange的结果来决定是否修改状态。一般确认修…

qt-C++笔记之使用QtConcurrent异步地执行槽函数中的内容,使其不阻塞主界面

qt-C笔记之使用QtConcurrent异步地执行槽函数中的内容&#xff0c;使其不阻塞主界面 code review! 文章目录 qt-C笔记之使用QtConcurrent异步地执行槽函数中的内容&#xff0c;使其不阻塞主界面1.QtConcurrent::run基本用法基本用法启动一个全局函数或静态成员函数使用 Lambda…

C++进阶之路:何为拷贝构造函数,深入理解浅拷贝与深拷贝(类与对象_中篇)

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

脚注:书籍的小秘密,躲藏在脚注间

脚注&#xff1a;书籍的小秘密&#xff0c;躲藏在脚注间 脚注是一种在文本中提供补充信息、引用出处或注解的方式&#xff0c;有助于读者更全面地理解文中内容&#xff0c;并为进一步研究提供参考和跳转点。 在一书本中&#xff0c;脚注是额外提供给读者的文字信息&#xff0…

SpringCloud系列(31)--使用Hystrix进行服务降级

前言&#xff1a;在上一章节中我们创建了服务消费者模块&#xff0c;而本节内容则是使用Hystrix对服务进行服务降级处理。 1、首先我们先对服务提供者的服务进行服务降级处理 (1)修改cloud-provider-hystrix-payment8001子模块的PaymentServiceImpl类 注&#xff1a;HystrixP…

Stream流的使用

目录 一&#xff0c;Stream流 1.1 概述 1.2 Stream代码示例 二&#xff0c;Stream流的使用 2.1 数据准备 2.2 创建流对象 2.3 中间操作 filter map distinct sorted limit skip flatMap 2.4 终结操作 foreach count max&min collect 2.5 查找与匹配 a…

秒级达百万高并发框架Disruptor

1、起源 Disruptor最初由lmax.com开发&#xff0c;2010年在Qcon公开发表&#xff0c;并于2011年开源&#xff0c;企业应用软件专家Martin Fowler专门撰写长文介绍&#xff0c;同年它还获得了Oracle官方的Duke大奖。其官网定义为&#xff1a;“High Performance Inter-Thread M…

2022年CSP-J入门级第一轮初赛真题

一、单项选择题&#xff08;共15题&#xff0c;每题2分&#xff0c;共计30分&#xff1b;每题有且仅有一个正确选项&#xff09; 第 1 题 在内存储器中每个存储单元都被赋予一个唯一的序号&#xff0c;称为&#xff08;&#xff09;。 A. 地址B. 序号C. 下标D. 编号 第 2 题 编…

Spring MVC+mybatis 项目入门:旅游网(三)用户注册——控制反转以及Hibernate Validator数据验证

个人博客&#xff1a;Spring MVCmybatis 项目入门:旅游网&#xff08;三&#xff09;用户注册 | iwtss blog 先看这个&#xff01; 这是18年的文章&#xff0c;回收站里恢复的&#xff0c;现阶段看基本是没有参考意义的&#xff0c;技术老旧脱离时代&#xff08;2024年辣铁铁&…