解决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, "", "");// 不存在,直接返回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, "", "");// 不存在,直接返回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);
}

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

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

相关文章

2024年10月24日第一部分AOP编程和自信

测试 Spring通知&#xff08;前置通知&#xff0c;后置通知&#xff0c;返回通知&#xff0c;异常通知&#xff0c;环绕通知&#xff09;_前置通知后置通知环绕通知-CSDN博客 一、前置通知 --前置通知 : 在方法执行之前执行的通知 --前置通知使用 Before 注解 , 并将切入点表…

【2024CANN训练营第二季】使用华为云体验AscendC_Sample仓算子运行

环境介绍 NPU&#xff1a;Ascend910B2 环境准备 创建Notebook 华为云选择&#xff1a;【控制台】-【ModelArts】 ModelArts主页选择【开发生产】-【开发空间】-【Notebook】 页面右上角选择【创建Notebook】 选择资源 主要参数 规格&#xff1a;Ascend: 1*ascend-snt…

VS code部署Vue项目Demo

在之前已经在IDEA中部署过vue项目demo。本次在上次基础上进行。 IDEA中Vue的安装和使用【window10】_idea安装vue-CSDN博客 步骤一、安装VSCode 双击安装即可 步骤二&#xff1a;检查npm是否安装 步骤三&#xff1a;检查vue是否安装 &#xff08;vue create 项目名 只要在v…

【AscendC算子开发】笔记1 算子开发哲学

重看这门课&#xff0c;有很多内容的认识更深了&#xff0c;做一些记录。 为什么不能将网络节点融合 这个问题关联到另一个问题&#xff1a;为什么我们需要激活函数&#xff1f; 使用线性的神经元堆叠得到的方程最后也是线性方程&#xff0c;无法表征非线性的信息&#xff0c…

微信网页授权回调地址放多个参数的方法

https://open.weixin.qq.com/connect/oauth2/authorize?appidAPPID&redirect_uriREDIRECT_URI&response_typecode&scopeSCOPE&stateSTATE#wechat_redirect 跳转后地址 redirect_uri/?codeCODE&stateSTATE。 redirect_uri如果不进行urlencode编码, 跳转后…

C++20中头文件syncstream的使用

<syncstream>是C20中新增加的头文件&#xff0c;提供了对同步输出流的支持&#xff0c;即在多个线程中可安全地进行输出操作&#xff0c;此头文件是Input/Output库的一部分。包括&#xff1a; 1.std::basic_syncbuf&#xff1a;是std::basic_streambuf的包装器(wrapper)&…

《在1688的数字海洋中,如何用API网罗一家店铺的所有商品?》

想象一下&#xff0c;你是一位船长&#xff0c;航行在1688这个电商的数字海洋上。你的任务是探索一家神秘的店铺岛屿&#xff0c;并且用你的API魔法网&#xff0c;网罗岛上所有的商品宝藏。不用担心&#xff0c;即使你不是海贼王&#xff0c;有了代码的力量&#xff0c;你也能成…

【数据结构初阶】二叉树---堆

二叉树-堆的实现 一、树的概念&#xff08;什么是树&#xff09;二、二叉树的概念及结构2.1 二叉树的概念2.2 二叉树的性质2.3 二叉树存储结构 三、二叉树的顺序结构3.1 堆的概念及结构3.2 堆的向下调整算法3.3堆的创建 四、堆的代码实现4.1 堆的初始化4.2 堆的销毁4.3 堆的插入…

ipguard与Ping32如何加密数据防止泄露?让企业信息更安全

在信息化时代&#xff0c;数据安全已成为企业运营的重中之重。数据泄露不仅会导致经济损失&#xff0c;还可能损害企业声誉。因此&#xff0c;选择合适的数据加密工具是保护企业敏感信息的关键。本文将对IPGuard与Ping32这两款加密软件进行探讨&#xff0c;了解它们如何有效加密…

SAP_SD模块-销售订单创建价格扩大10倍问题分析及后续订单价格批量更新问题处理

一、业务背景 我们公司的销售订单&#xff0c;是通过第三方销售管理平台创建好订单后&#xff0c;把表头和行项目数据&#xff0c;定时推送到SAP&#xff1b;SAP通过自定义表ZZT_ORDER_HEAD存放订单表头数据&#xff0c;通过ZZT_ORDER_DETAIL存放行项目数据&#xff1b;然后再用…

git安装-Tortoise git 安装汉化教程

1. 安装git 2. 安装git图形化工具Tortoise git 3. 汉化 Tortoise git 汉化安装包

证件照电子版怎么弄?不花钱制作方法快来学

想要制作免费照证件照&#xff1f;证件照在我们的日常生活中扮演着重要角色&#xff0c;无论是求职、求学还是办理各类证件&#xff0c;都少不了它的身影。 但是&#xff0c;去照相馆拍照不仅耗时&#xff0c;费用也不菲。那么&#xff0c;有没有可能不花一分钱就搞定证件照呢…

互联网系统的微观与宏观架构

互联网系统的架构设计&#xff0c;通常会根据项目的体量、业务场景以及技术需求被划分为微观架构&#xff08;Micro-Architecture&#xff09;和宏观架构&#xff08;Macro-Architecture&#xff09;。这两者的概念与职责既独立又相互关联。本文将通过一些系统案例&#xff0c;…

淘宝API的实战应用:数据驱动增长,实时监控商品信息是关键

数据驱动增长&#xff0c;实时监控商品信息是关键 —— 淘宝API的实战应用 在数字化时代&#xff0c;数据已经成为商业决策的核心。对于电商行业而言&#xff0c;获取准确、实时的数据是保持竞争力的关键。淘宝API接口作为连接淘宝电商平台与外部应用的桥梁&#xff0c;为电商商…

【论文+源码】基于spring boot的垃圾分类网站

创建一个基于Spring Boot的垃圾分类网站涉及多个步骤&#xff0c;包括环境搭建、项目创建、数据库设计、后端服务开发、前端页面设计等。下面我将引导您完成这个过程。 第一步&#xff1a;准备环境 确保您的开发环境中安装了以下工具&#xff1a; Java JDK 8 或更高版本Mav…

uv: 一个统一的Python包管理工具

uv是由Astral公司开发的一个极其快速的Python包管理器,完全用Rust编写。它最初在2月份发布,作为pip工作流的替代品。现在,uv已经扩展成为一个端到端的解决方案,可以管理Python项目、命令行工具、单文件脚本,甚至Python本身。可以说,uv就像是Python界的Cargo:一个快速、可靠、易…

Rust小练习,编写井字棋

画叉画圈的游戏通常指的是 井字棋&#xff08;Tic-Tac-Toe&#xff09;&#xff0c;是一个简单的两人游戏&#xff0c;规则如下&#xff1a; 游戏规则 棋盘&#xff1a;游戏在一个3x3的方格上进行。玩家&#xff1a;有两个玩家&#xff0c;一个用“X”表示&#xff0c;另一个…

Vivado自定义IP修改顶层后Port and Interface不更新解决方案

问题描述 在整个项目工程中&#xff0c;对自定义IP进行一个比较大的改动&#xff0c;新增了不少端口(这里具体的就是bram的读写端口)&#xff0c;修改是在block design中右击IP编辑在IP编辑工程中进行的。 在修改完所有代码后&#xff08;顶层新增了需要新加的输入输出端口&…

算法的学习笔记—平衡二叉树(牛客JZ79)

&#x1f600;前言 在数据结构中&#xff0c;二叉树是一种重要的树形结构。平衡二叉树是一种特殊的二叉树&#xff0c;其特性是任何节点的左右子树高度差的绝对值不超过1。本文将介绍如何判断一棵给定的二叉树是否为平衡二叉树&#xff0c;重点关注算法的时间复杂度和空间复杂度…

未来汽车驾驶还会有趣吗?车辆动力学系统简史

未来汽车驾驶还会有趣吗&#xff1f;车辆动力学系统简史 本篇文章来源&#xff1a;Schmidt, F., Knig, L. (2020). Will driving still be fun in the future? Vehicle dynamics systems through the ages. In: Pfeffer, P. (eds) 10th International Munich Chassis Symposiu…