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规范 同一前缀 + 店铺idString 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.写入redisstringRedisTemplate.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;}

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

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

相关文章

【挖金子game】

如果您想要编写一个简单的“挖金子”游戏代码&#xff0c;可以使用Python这样的编程语言来实现。以下是一个简单的Python代码示例&#xff0c;用于创建一个基本的“挖金子”游戏&#xff1a; import random # 游戏设置 max_gold 10 # 最大金子数量 max_digs 5 # 最大挖掘…

数据驱动(Data-Driven)和以数据为中心(Data-Centric)的区别

一、什么是数据驱动&#xff1f; 数据驱动&#xff08;Data-Driven&#xff09;是在管理科学领域经常提到的名词。数据驱动决策&#xff08;Data-Driven Decision Making&#xff0c;简称DDD&#xff09;是一种方法论&#xff0c;即在决策过程中主要依赖于数据分析和解释&…

Java基础学习:java中的基础注解

在Java中&#xff0c;有一些内置的&#xff08;或称为“基础”&#xff09;注解&#xff08;annotation&#xff09;&#xff0c;这些注解在Java标准库中定义&#xff0c;并且具有特定的用途。以下是一些主要的Java内置注解&#xff1a; Override&#xff1a; 用于表示一个方法…

Keras深度学习框架第二十七讲:KerasTuner超参数优化基础

1、超参数优化概念 1.1 什么是超参数优化 超参数调优&#xff0c;也称为超参数优化或参数调优&#xff0c;是寻找学习算法或模型最佳超参数组合的过程。超参数是在训练过程开始之前设置的参数&#xff0c;模型无法直接从数据中学习这些参数。它们控制着学习算法的行为&#x…

NDIS小端口驱动开发(二)

初始化微型端口适配器 当网络设备可用时&#xff0c;系统会加载所需的 NDIS 微型端口驱动程序。 随后&#xff0c;即插即用 (PnP) 管理器向 NDIS 发送即插即用 IRP 来启动设备。 NDIS 调用微型端口驱动程序的 MiniportInitializeEx 函数来初始化用于网络 I/O 操作的适配器。 初…

嵩山为什么称为三水之源

三水指黄河、淮河、济河&#xff0c;这三条河流环绕在嵩山周边。 黄河横亘在嵩山北部&#xff0c;其支流伊洛河从西南方环绕嵩山&#xff0c;然后汇入黄河。济河&#xff0c;古称济水&#xff0c;源自济源王屋山&#xff0c;自身河道在东晋时代被黄河夺占&#xff0c;从此消失。…

毕设 大数据校园卡数据分析

文章目录 0 前言1 课题介绍2 数据预处理2.1 数据清洗2.2 数据规约 3 模型建立和分析3.1 不同专业、性别的学生与消费能力的关系3.2 消费时间的特征分析 4 Web系统效果展示5 最后 0 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设…

职场不是挣钱

职场怎么不是挣钱&#xff1f; 曾经我也一直这么想&#xff0c;只要做好老板安排的事情&#xff0c;自然就可以挣到钱了。 目的应该是没错的&#xff0c;是挣钱。 只是做好活就能挣钱&#xff0c;好像想得有些简单了。 毕竟每个人都在干活&#xff0c;为什么就该自己挣钱呢&a…

【vue2配置】Vue Router

Vue Router官网 1、npm install vue-router4 2、创建模块&#xff0c;在src目录小创/views/map/MapIndex.vue模块和创router/index.js文件 3、在router/index.js配置路由 import Vue from "vue"; import Router from "vue-router"; // 引入模块 const Ma…

C语言——在头⽂件中#if、_STDC_等字⾏起什么作⽤?

一、问题 通常&#xff0c;⼀些程序员都不会去研究头⽂件中的内容是什么含义&#xff0c;总觉得乱乱的&#xff0c;有很多 #if、_STDC_、#line 等字符&#xff0c;那么这些字符都各代表什么呢&#xff0c;在头⽂件中又起到什么作⽤呢&#xff1f; 二、解答 在头⽂件中存在类似…

智慧校园建设的进阶之路

智慧校园的建设现已到达了老练的阶段&#xff0c;许多学校设备充满着数字化信息&#xff0c;进出宿舍楼&#xff0c;校园一卡通体系会记载下学生信息&#xff0c;外来人员闯入会报警&#xff0c;翻开电脑就能查到学生是否在宿舍等……学生的学习和日子都充满了数字化的痕迹。但…

C# WPF入门学习(三)

目录 核心架构 核心组件和概念 1. XAML&#xff08;eXtensible Application Markup Language&#xff09; 2. 依赖属性&#xff08;Dependency Properties&#xff09; 3. 路由事件&#xff08;Routed Events&#xff09; 4. 数据绑定 5. 命令&#xff08;Commands&…

itertools内置模块的过滤妙用

itertools内置模块的妙用 过滤源迭代器中的元素 Python内置itertools模块里有一些函数可以过滤源迭代器中的元素。 islice islice可以在不拷贝数据的前提下&#xff0c;按照下标切割源迭代器。可以只给出切割的终点&#xff0c;也可以同时给出起点和终点&#xff0c;还可以…

MongoDB 覆盖索引查询:提升性能的完整指南

MongoDB 覆盖索引查询是一种优化数据库查询性能的技术&#xff0c;它通过创建适当的索引&#xff0c;使查询可以直接从索引中获取所需的数据&#xff0c;而无需访问实际的文档数据。这种方式可以减少磁盘 I/O 和内存消耗&#xff0c;提高查询性能。 基本语法 在 MongoDB 中&a…

SQL练习题:2.4

建表 # 学生表 create table t_student (stu_id varchar(10),stu_name varchar(10),stu_age datetime,stu_sex varchar(10) );# 课程表 create table t_t_course (c_id varchar(10),c_name varchar(10),c_teaid varchar(10) );# 教师表 create table t_t_teacher (tea…

光速入门python的OpenCV

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文整理python的OpenCV模块的关键知识点 争取用最短的时间入门OpenCV 并且做到笔记功能直接复制使用 OpenCV简介 不浪费时间的介绍: 就是类似于ps操作图片。 至于为什么不直接用ps&#xff0c;因为只有程序能…

【找出满足差值条件的下标 I】python

目录 暴力题解 优化&#xff1a;滑动窗口维护大小值 暴力题解 class Solution:def findIndices(self, nums: List[int], indexDifference: int, valueDifference: int) -> List[int]:nlen(nums)for i in range(n):for j in range(n-1,-1,-1):if abs(i-j)>indexDiffere…

海康威视NVR通过ehome协议接入视频监控平台,视频浏览显示3011超时错误的问题解决,即:The request timeout! 【3011】

目录 一、问题描述 二、问题分析 2.1 初步分析 2.2 查看日志 2.3 问题验证 1、查看防火墙 2、查看安全组 3、问题原因 三、问题解决 3.1 防火墙开放相关端口 3.2 安全组增加规则 3.3 测试 1、TCP端口能够联通的情况 2、TCP端口不能够联通的情况 四、验证 五、云…

「51媒体」如何与媒体建立良好关系?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 与媒体建立良好关系对于企业或个人来说都是一项重要的公关活动。 了解媒体&#xff1a;研究媒体和记者的兴趣&#xff0c;提供相关且有价值的信息。 建立联系&#xff1a;通过专业的方式…

牛客NC324 下一个更大的数(三)【中等 双指针 Java/Go/PHP/C++】参考lintcode 52 · 下一个排列

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/475da0d4e37a481bacf9a09b5a059199 思路 第一步&#xff1a;获取数字上每一个数&#xff0c;组成数组arr 第二步&#xff1a;利用“下一个排列” 问题解题方法来继续作答&#xff0c;步骤&#xff1a;利用lintc…