当前位置: 首页 > news >正文

解决Redis缓存穿透(缓存空对象、布隆过滤器)

文章目录

  • 背景
  • 代码实现
    • 前置
      • 实体类
      • 常量类
      • 工具类
      • 结果返回类
      • 控制层
    • 缓存空对象
    • 布隆过滤器
    • 结合两种方法

背景

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

常见的解决方案有两种,分别是缓存空对象布隆过滤器

1.缓存空对象

image-20241025163728328

优点:实现简单,维护方便

缺点:额外的内存消耗、可能造成短期的不一致

2.布隆过滤器

image-20241025163737389

优点:内存占用较少,没有多余key

缺点:实现复杂、存在误判可能

代码实现

前置

这里以根据 id 查询商品店铺为案例

实体类

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_shop")
public class Shop implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 商铺名称*/private String name;/*** 商铺类型的id*/private Long typeId;/*** 商铺图片,多个图片以','隔开*/private String images;/*** 商圈,例如陆家嘴*/private String area;/*** 地址*/private String address;/*** 经度*/private Double x;/*** 维度*/private Double y;/*** 均价,取整数*/private Long avgPrice;/*** 销量*/private Integer sold;/*** 评论数量*/private Integer comments;/*** 评分,1~5分,乘10保存,避免小数*/private Integer score;/*** 营业时间,例如 10:00-22:00*/private String openHours;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;@TableField(exist = false)private Double distance;
}

常量类

public class RedisConstants {public static final Long CACHE_NULL_TTL = 2L;public static final Long CACHE_SHOP_TTL = 30L;public static final String CACHE_SHOP_KEY = "cache:shop:";
}

工具类

public class ObjectMapUtils {// 将对象转为 Mappublic static Map<String, String> obj2Map(Object obj) throws IllegalAccessException {Map<String, String> result = new HashMap<>();Class<?> clazz = obj.getClass();Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {// 如果为 static 且 final 则跳过if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {continue;}field.setAccessible(true); // 设置为可访问私有字段Object fieldValue = field.get(obj);if (fieldValue != null) {result.put(field.getName(), field.get(obj).toString());}}return result;}// 将 Map 转为对象public static Object map2Obj(Map<Object, Object> map, Class<?> clazz) throws Exception {Object obj = clazz.getDeclaredConstructor().newInstance();for (Map.Entry<Object, Object> entry : map.entrySet()) {Object fieldName = entry.getKey();Object fieldValue = entry.getValue();Field field = clazz.getDeclaredField(fieldName.toString());field.setAccessible(true); // 设置为可访问私有字段String fieldValueStr = fieldValue.toString();// 根据字段类型进行转换if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {field.set(obj, Integer.parseInt(fieldValueStr));} else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {field.set(obj, Boolean.parseBoolean(fieldValueStr));} else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {field.set(obj, Double.parseDouble(fieldValueStr));} else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {field.set(obj, Long.parseLong(fieldValueStr));} else if (field.getType().equals(String.class)) {field.set(obj, fieldValueStr);} else if(field.getType().equals(LocalDateTime.class)) {field.set(obj, LocalDateTime.parse(fieldValueStr));}}return obj;}}

结果返回类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {private Boolean success;private String errorMsg;private Object data;private Long total;public static Result ok(){return new Result(true, null, null, null);}public static Result ok(Object data){return new Result(true, null, data, null);}public static Result ok(List<?> data, Long total){return new Result(true, null, data, total);}public static Result fail(String errorMsg){return new Result(false, errorMsg, null, null);}
}

控制层

@RestController
@RequestMapping("/shop")
public class ShopController {@Resourcepublic IShopService shopService;/*** 根据id查询商铺信息* @param id 商铺id* @return 商铺详情数据*/@GetMapping("/{id}")public Result queryShopById(@PathVariable("id") Long id) {return shopService.queryShopById(id);}/*** 新增商铺信息* @param shop 商铺数据* @return 商铺id*/@PostMappingpublic Result saveShop(@RequestBody Shop shop) {return shopService.saveShop(shop);}/*** 更新商铺信息* @param shop 商铺数据* @return 无*/@PutMappingpublic Result updateShop(@RequestBody Shop shop) {return shopService.updateShop(shop);}
}

缓存空对象

流程图为:

image-20241025165838030

服务层代码:

public Result queryShopById(Long id) {// 从 redis 查询String shopKey = RedisConstants.CACHE_SHOP_KEY + id;Map<Object, Object> entries = redisTemplate.opsForHash().entries(shopKey);// 缓存命中if(!entries.isEmpty()) {try {// 如果是空对象,表示一定不存在数据库中,直接返回(解决缓存穿透)if(entries.containsKey("")) {return Result.fail("店铺不存在");}// 刷新有效期redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class);return Result.ok(shop);} catch (Exception e) {throw new RuntimeException(e);}}// 查询数据库Shop shop = this.getById(id);if(shop == null) {// 存入空值redisTemplate.opsForHash().put(shopKey, "", "");redisTemplate.expire(shopKey, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);// 不存在,直接返回return Result.fail("店铺不存在");}// 存在,写入 redistry {redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop));redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (IllegalAccessException e) {throw new RuntimeException(e);}return Result.ok(shop);
}

布隆过滤器

这里选择使用布隆过滤器存储存在于数据库中的 id,原因在于,如果存储了不存在于数据库中的 id,首先由于 id 的取值范围很大,那么不存在的 id 有很多,因此更占用空间;其次,由于布隆过滤器有一定的误判率,那么可能导致少数原本存在于数据库中的 id 被判为了不存在,然后直接返回了,此时就会出现根本性的正确性错误。相反,如果存储的是数据库中存在的 id,那么即使少数不存在的 id 被判为了存在,由于数据库中确实没有对应的 id,那么也会返回空,最终结果还是正确的

这里使用 guava 依赖的布隆过滤器

依赖为:

<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30.1.1-jre</version>
</dependency>

封装了布隆过滤器的类(注意初始化时要把数据库中已有的 id 加入布隆过滤器):

public class ShopBloomFilter {private BloomFilter<Long> bloomFilter;public ShopBloomFilter(ShopMapper shopMapper) {// 初始化布隆过滤器,设计预计元素数量为100_0000L,误差率为1%bloomFilter = BloomFilter.create(Funnels.longFunnel(), 100_0000, 0.01);// 将数据库中已有的店铺 id 加入布隆过滤器List<Shop> shops = shopMapper.selectList(null);for (Shop shop : shops) {bloomFilter.put(shop.getId());}}public void add(long id) {bloomFilter.put(id);}public boolean mightContain(long id){return bloomFilter.mightContain(id);}}

对应的配置类(将其设置为 bean)

@Configuration
public class BloomConfig {@Beanpublic ShopBloomFilter shopBloomFilter(ShopMapper shopMapper) {return new ShopBloomFilter(shopMapper);}}

首先要修改查询方法,在根据 id 查询时,如果对应 id 不在布隆过滤器中,则直接返回。然后还要修改保存方法,在保存的时候还需要将对应的 id 加入布隆过滤器中

@Override
public Result queryShopById(Long id) {// 如果不在布隆过滤器中,直接返回if(!shopBloomFilter.mightContain(id)) {return Result.fail("店铺不存在");}// 从 redis 查询String shopKey = RedisConstants.CACHE_SHOP_KEY + id;Map<Object, Object> entries = redisTemplate.opsForHash().entries(shopKey);// 缓存命中if(!entries.isEmpty()) {try {// 刷新有效期redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class);return Result.ok(shop);} catch (Exception e) {throw new RuntimeException(e);}}// 查询数据库Shop shop = this.getById(id);if(shop == null) {// 不存在,直接返回return Result.fail("店铺不存在");}// 存在,写入 redistry {redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop));redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (IllegalAccessException e) {throw new RuntimeException(e);}return Result.ok(shop);
}@Override
public Result saveShop(Shop shop) {// 写入数据库this.save(shop);// 将 id 写入布隆过滤器shopBloomFilter.add(shop.getId());// 返回店铺 idreturn Result.ok(shop.getId());
}

结合两种方法

由于布隆过滤器有一定的误判率,所以这里可以进一步优化,如果出现误判情况,即原本不存在于数据库中的 id 被判为了存在,就用缓存空对象的方式将其缓存到 redis 中

@Override
public Result queryShopById(Long id) {// 如果不在布隆过滤器中,直接返回if(!shopBloomFilter.mightContain(id)) {return Result.fail("店铺不存在");}// 从 redis 查询String shopKey = RedisConstants.CACHE_SHOP_KEY + id;Map<Object, Object> entries = redisTemplate.opsForHash().entries(shopKey);// 缓存命中if(!entries.isEmpty()) {try {// 如果是空对象,表示一定不存在数据库中,直接返回(解决缓存穿透)if(entries.containsKey("")) {return Result.fail("店铺不存在");}// 刷新有效期redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class);return Result.ok(shop);} catch (Exception e) {throw new RuntimeException(e);}}// 查询数据库Shop shop = this.getById(id);if(shop == null) {// 存入空值redisTemplate.opsForHash().put(shopKey, "", "");redisTemplate.expire(shopKey, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);// 不存在,直接返回return Result.fail("店铺不存在");}// 存在,写入 redistry {redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop));redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (IllegalAccessException e) {throw new RuntimeException(e);}return Result.ok(shop);
}

相关文章:

解决Redis缓存穿透(缓存空对象、布隆过滤器)

文章目录 背景代码实现前置实体类常量类工具类结果返回类控制层 缓存空对象布隆过滤器结合两种方法 背景 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在&#xff0c;这样缓存永远不会生效&#xff0c;这些请求都会打到数据库 常见的解决方案有两种&#xff0c;分别…...

初探Flink的序列化

Flink中的序列化应用场景 程序通常使用(至少)两种不同的数据表示形式[2]&#xff1a; 1. 在内存中&#xff0c;数据保存在对象、结构体、列表、数组、哈希表和树等结构中。 2. 将数据写入文件或通过网络发送时&#xff0c;必须将其序列化为字节序列。 从内存中的表示到字节序列…...

QT 机器视觉 (3. 虚拟相机SDK、测试工具)

本专栏从实际需求场景出发详细还原、分别介绍大型工业化场景、专业实验室场景、自动化生产线场景、各种视觉检测物体场景介绍本专栏应用场景 更适合涉及到视觉相关工作者、包括但不限于一线操作人员、现场实施人员、项目相关维护人员&#xff0c;希望了解2D、3D相机视觉相关操作…...

1分钟解决Excel打开CSV文件出现乱码问题

一、编码问题 1、不同编码格式 CSV 文件有多种编码格式&#xff0c;如 UTF - 8、UTF - 16、ANSI 等。如果 CSV 文件是 UTF - 8 编码&#xff0c;而 Excel 默认使用的是 ANSI 编码打开&#xff0c;就可能出现乱码。例如&#xff0c;许多从网络应用程序或非 Windows 系统生成的 …...

基于SpringBoot+Vue的仓库管理系统【前后端分离】

基于SpringBootVue的仓库管理系统设计与实现 摘要 仓库管理系统在现代企业物流中具有重要作用&#xff0c;能够有效提高库存管理效率&#xff0c;优化资源配置。本系统采用Spring Boot作为后端框架&#xff0c;Vue作为前端框架&#xff0c;通过前后端分离的开发模式构建一个现代…...

vue和django接口联调

vue访问服务端接口 配置跨域 前端跨域 打开vite.config.js&#xff0c;在和resolve同级的地方添加配置。 proxy代表代理的意思 "/api"是以/api开头的路径走这个配置 target代表目标 changeOrigin: true,是开启跨域请求 rewrite是编辑路径。 (path) > pa…...

2-141 怎么实现ROI-CS压缩感知核磁成像

怎么实现ROI-CS压缩感知核磁成像&#xff0c;这个案例告诉你。基于matlab的ROI-CS压缩感知核磁成像。ROI指在图像中预先定义的特定区域或区域集合&#xff0c;选择感兴趣的区域&#xff0c;通过减少信号重建所需的数据来缩短信号采样时间&#xff0c;减少计算量&#xff0c;并在…...

开源库 FloatingActionButton

开源库FloatingActionButton Github:https://github.com/Clans/FloatingActionButton 这个库是在前面这个库android-floating-action-button的基础上修改的&#xff0c;增加了一些更强大和实用的特性。 特性&#xff1a; Android 5.0 以上点击会有水波纹效果 可以选择自定义…...

技术选型不当对项目的影响与补救措施

在项目管理中&#xff0c;初期技术选型与项目需求不匹配的情况并不罕见&#xff0c;这可能导致项目延误、成本增加和最终成果的不理想。补救的关键措施包括&#xff1a;重新评估技术选型、加强团队沟通、实施有效的需求管理以及建立持续的反馈机制。其中&#xff0c;重新评估技…...

Spring的核心类: BeanFactory, ApplicationContext 笔记241103

Spring的核心类: BeanFactory, ApplicationContext, ConfigurableApplicationContext, WebApplicationContext, WebServerApplicationContext, ClassPathXmlApplicationContext, FileSystemXmlApplicationContext, XmlWebApplicationContext, AnnotationConfigServletWebServer…...

UE5移动端主要对象生命周期及监听

1、GameInstance 1、首先加载GameInstance,全局唯一,切换Map也是唯一的,用于做一些全局操作,比如监听Map加载,监听App进入前台、退出后台 // Fill out your copyright notice in the Description page of Project Settings.#include "Core/Base/MyGameInstance.h&q…...

LLM | 论文精读 | CVPR | SelTDA:将大型视觉语言模型应用于数据匮乏的视觉问答任务

论文标题&#xff1a;How to Specialize Large Vision-Language Models to Data-Scarce VQA Tasks? Self-Train on Unlabeled Images! 作者&#xff1a;Zaid Khan, Vijay Kumar BG, Samuel Schulter, Xiang Yu, Yun Fu, Manmohan Chandraker 期刊&#xff1a;CVPR 2023 DOI…...

kafka里的consumer 是推还是拉?

大家好&#xff0c;我是锋哥。今天分享关于【kafka里的consumer 是推还是拉&#xff1f;】面试题&#xff1f;希望对大家有帮助&#xff1b; kafka里的consumer 是推还是拉&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在Kafka中&#xff0c;消费者&…...

针对物联网边缘设备基于EIT的手部手势识别的1D CNN效率增强的组合模型压缩方法

论文标题&#xff1a;Combinative Model Compression Approach for Enhancing 1D CNN Efficiency for EIT-based Hand Gesture Recognition on IoT Edge Devices 中文标题&#xff1a;针对物联网边缘设备基于EIT的手部手势识别的1D CNN效率增强的组合模型压缩方法 作者信息&a…...

商品满减、限时活动、折扣活动的计算最划算 golang

可以对商品的不同活动&#xff08;如满减、限时价和折扣&#xff09;进行分组&#xff0c;并在购物车中显示各个活动标签下的最优价格组合。以下代码将商品按活动类别进行分组计算&#xff0c;并输出在购物车中的显示信息。 package mainimport ("fmt""math&qu…...

vue3 + ts + element-plus 二次封装 el-table

一、实现效果&#xff1a; &#xff08;1&#xff09;数据为空时&#xff1a; &#xff08;2&#xff09;有数据时&#xff1a;存在数据合并&#xff1b;可自定义表头和列的内容 &#xff08;3&#xff09;新增行&#xff1a; &#xff08;4&#xff09;删除行&#xff1a; &a…...

python传递json参数给php

python传递json参数给php 在Python中&#xff0c;你可以使用requests库来发送JSON数据给一个PHP脚本。以下是一个简单的例子&#xff1a; 首先&#xff0c;安装requests库&#xff08;如果你还没有安装的话&#xff09;&#xff1a; pip install requests 然后&#xff0c;…...

2.若依vue表格数据根据不同状态显示不同颜色style

例如国标显示蓝色&#xff0c;超标是红色 使用是蓝色&#xff0c;未使用是绿色 <el-table-column label"外卖配送是否完成评价" align"center" prop"isOverFlag"> <template slot-scope"scope"> …...

JZ2440开发板——LCD

以下内容源于韦东山嵌入式课程的学习与整理&#xff0c;如有侵权请告知删除。 之前在博文中学习过LCD&#xff08;SoC是S5PV210&#xff09;&#xff0c;作为对比&#xff0c;本文学习S3C2440这款SoC的LCD方面的内容。主要涉及以下三个内容&#xff1a; 一、LCD的硬件原理 1.…...

YOLOv6-4.0部分代码阅读笔记-yolo_lite.py

yolo_lite.py yolov6\models\yolo_lite.py 所需的库和模块 #!/usr/bin/env python3 # -*- coding:utf-8 -*- import math import torch import torch.nn as nn import torch.nn.functional as F from yolov6.layers.common import * from yolov6.utils.torch_utils import i…...

手把手教你用Gen6D制作个人数据集:从视频采集到6D姿态估计全流程

从零构建Gen6D个人数据集&#xff1a;6D姿态估计实战指南 引言&#xff1a;为什么需要自定义6D姿态数据集&#xff1f; 在计算机视觉领域&#xff0c;6D姿态估计(6D Pose Estimation)正成为增强现实、机器人抓取和自动驾驶等应用的核心技术。与传统的2D检测不同&#xff0c;6…...

字节Agent开发岗面试血泪史:ReAct框架、IterResearch架构、训练流程全解析,小白必看!收藏!

字节Agent开发岗面试血泪史&#xff1a;ReAct框架、IterResearch架构、训练流程全解析&#xff0c;小白必看&#xff01;收藏&#xff01; 文章通过字节Agent开发岗校招面试实例&#xff0c;揭示候选人因缺乏底层逻辑和工程实现细节而失败。详细解析ReAct框架、IterResearch架构…...

【FastAPI 2.0流式AI响应实战指南】:3步接入、5大避坑点、性能提升300%的工业级落地方案

第一章&#xff1a;FastAPI 2.0流式AI响应的核心演进与工业价值FastAPI 2.0 将原生流式响应能力从实验性支持升级为一级公民特性&#xff0c;彻底重构了高吞吐 AI 服务的构建范式。其核心在于深度整合 ASGI 3.0 的异步流语义与 Starlette 的 StreamingResponse 基础设施&#x…...

告别VS手动配置!用Premake5+Lua脚本一键生成C++项目(附多平台实战案例)

告别VS手动配置&#xff01;用Premake5Lua脚本一键生成C项目&#xff08;附多平台实战案例&#xff09; 每次新建C项目时&#xff0c;你是否也厌倦了在Visual Studio中重复点击那些繁琐的配置选项&#xff1f;从平台工具集到运行时库&#xff0c;从包含目录到预处理器定义&…...

CVE_2020_26259 任意文件删除

为什么要用c语言搓个shellcode出来&#xff0c;为什么不用msfvenom&#xff1f;因为这玩意生成的shellcode是基于winsocket的&#xff0c;注进去还要启动个监听&#xff0c;我仅仅想要验证一下可行性而已&#xff0c;不如自己搓个弹出messagebox版本的shellcode 环境 windows 1…...

Armbian 国内源一键配置:清华镜像加速实战

1. 为什么需要给Armbian换国内源&#xff1f; 如果你在国内使用Armbian系统&#xff0c;可能会遇到软件包下载速度慢、更新失败等问题。这主要是因为默认的软件源服务器通常位于国外&#xff0c;物理距离远导致网络延迟高。我最初用树莓派搭建家庭服务器时就深有体会&#xff0…...

OFA视觉问答模型实战教程:与OCR模块串联实现图文联合问答流程

OFA视觉问答模型实战教程&#xff1a;与OCR模块串联实现图文联合问答流程 1. 教程概述 今天我们来探索一个非常实用的技术方案&#xff1a;如何将OFA视觉问答模型与OCR模块串联&#xff0c;实现真正的图文联合问答流程。这个方案能让你的AI应用不仅看懂图片内容&#xff0c;还…...

树莓派Pico RP2040 I2C实战:用AT24C02 EEPROM做个数据掉电保存的小项目

树莓派Pico RP2040 I2C实战&#xff1a;用AT24C02 EEPROM实现数据持久化存储 在嵌入式开发中&#xff0c;数据持久化存储是一个常见需求。当我们需要保存设备配置、运行日志或用户设置时&#xff0c;EEPROM&#xff08;电可擦可编程只读存储器&#xff09;因其非易失性特性成为…...

为什么92%的Python低代码平台不敢暴露内核?:深度解析GIL绕过策略、上下文感知缓存与热重载原子切换机制

第一章&#xff1a;Python低代码平台内核不透明的产业困局在当前企业数字化加速落地的背景下&#xff0c;Python生态衍生出大量低代码平台&#xff08;如Streamlit Cloud、Gradio Spaces、Dash Enterprise&#xff09;&#xff0c;它们以“拖拉拽少量Python脚本”为卖点&#x…...

探索照片转3D模型:用Meshroom实现7步从2D到3D的蜕变

探索照片转3D模型&#xff1a;用Meshroom实现7步从2D到3D的蜕变 【免费下载链接】Meshroom 3D Reconstruction Software 项目地址: https://gitcode.com/gh_mirrors/me/Meshroom 定位3D重建价值&#xff1a;打破技术壁垒的开源方案 在数字创作领域&#xff0c;3D模型一…...