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

news2024/11/5 1:12:10

文章目录

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

背景

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

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

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 {

    // 将对象转为 Map
    public 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 {

    @Resource
    public IShopService shopService;

    /**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id) {
        return shopService.queryShopById(id);
    }
    
    /**
     * 新增商铺信息
     * @param shop 商铺数据
     * @return 商铺id
     */
    @PostMapping
    public Result saveShop(@RequestBody Shop shop) {
        return shopService.saveShop(shop);
    }

    /**
     * 更新商铺信息
     * @param shop 商铺数据
     * @return 无
     */
    @PutMapping
    public 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("店铺不存在");
    }
    // 存在,写入 redis
    try {
        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 {

    @Bean
    public 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("店铺不存在");
    }
    // 存在,写入 redis
    try {
        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());
    // 返回店铺 id
    return 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("店铺不存在");
    }
    // 存在,写入 redis
    try {
        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.coloradmin.cn/o/2230824.html

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

相关文章

基于微信小程序的校园失物招领系统的研究与实现(V4.0)

博主介绍&#xff1a;✌stormjun、8年大厂程序员经历。全网粉丝15w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&…

vscode 创建 vue 项目时,配置文件为什么收缩到一起展示了?

一、前言 今天用 vue 官方脚手架创建工程&#xff0c;然后通过 vscode 打开项目发现&#xff0c;配置文件都被收缩在一起了。就像下面这样 这有点反直觉&#xff0c;他们应该是在同一层级下的&#xff0c;怎么会这样&#xff0c;有点好奇&#xff0c;但是打开资源管理查看&…

LInux系统编程(二)操作系统和进程

目录 一、前言&#xff1a;冯诺依曼体系结构 1、图中各个单元的介绍 2、值得注意的几点 二、操作系统 1、操作系统分层图 2、小总结 三、 进程&#xff08;重点&#xff09; 1、进程的基本概念 2、存放进程信息的数据结构——PCB&#xff08;Linux 下称作 task_struct…

HNU-小学期-专业综合设计

写在前面 选题&#xff1a;大数据技术-智慧交通预测系统 项目github地址&#xff08;如果有用麻烦点个star与follow&#xff09;&#xff1a;https://github.com/wolfvoid/HNU-ITPS &#xff08;全部代码以及如何部署参见README&#xff09; 项目报告&#xff1a;如下&…

Linux特种文件系统--tmpfs文件系统

tmpfs类似于RamDisk&#xff08;只能使用物理内存&#xff09;&#xff0c;使用虚拟内存&#xff08;简称VM&#xff09;子系统的页面存储文件。tmpfs完全依赖VM&#xff0c;遵循子系统的整体调度策略。说白了tmpfs跟普通进程差不多&#xff0c;使用的都是某种形式的虚拟内存&a…

PLC会被卡脖子吗?冗余技术才是中型和大型PLC的门槛

美方称北京天圣华参与高超音速武器的研发和空对空导弹的生产&#xff0c;因此把北京天圣华列入实体制裁清单。据说因为天圣华向和中国军方相关研究机构出售了西门子的建模软件&#xff0c;并为军工项目的也就做出了积极贡献&#xff0c;因此美方对西门子施压。 西门子是全球最大…

【测试平台】打包 jenkins配置和jenkinsfile文件

背景&#xff1a; 当打包机环境配置完成后&#xff0c;需要挂到master的jenkins中&#xff0c;完成调度。 jenkins启动 命令行直接启动即可。 nohup java -jar /usr/local/opt/jenkins/libexec/jenkins.war --httpListenAddress0.0.0.0 --httpPort80 appending output to n…

【刷题13】链表专题

目录 一、两数相加二、两两交换链表的节点三、重排链表四、合并k个升序链表五、k个一组翻转链表 一、两数相加 题目&#xff1a; 思路&#xff1a; 注意整数是逆序存储的&#xff0c;结果要按照题目的要求用链表连接起来遍历l1的cur1&#xff0c;遍历l2的cur2&#xff0c;和…

人工智能与数据安全:Facebook如何应对隐私挑战

在数字时代&#xff0c;数据隐私和安全成为了用户和企业关注的核心问题。作为全球最大的社交媒体平台之一&#xff0c;Facebook面临着日益严峻的隐私挑战。近年来&#xff0c;频繁发生的数据泄露事件和对用户隐私的质疑&#xff0c;使得Facebook在保护用户数据方面倍感压力。为…

凸极式发电机的相量图分析和计算,内功率因数角和外功率因数角和功角的定义。

图1&#xff1a;同步发电机稳态相量图 若发电机为凸极式&#xff0c;由于凸极机正、交轴同步电抗不等&#xff0c;即xd≠xq&#xff0c;因此必须先借助虚构电动势 E ˙ Q E ˙ q − ( x d − x q ) I ˙ d \dot{E}_Q\dot{E}_q-(x_d-x_q)\dot{I}_d E˙Q​E˙q​−(xd​−xq​)…

C语言模拟实现堆排序

堆排序是一种效率比较高的排序方法&#xff0c;时间复杂度。 堆分为大堆和小堆&#xff0c;如果想要拍升序我们需要建立大堆&#xff0c;而如果想要拍降序则需要建立小堆&#xff0c;在使用堆排序前需要先建立一个堆&#xff0c;如果不会建立可以看我前面写的C语言模拟实现堆的…

HCIA笔记整合

第一部分&#xff1a; OSI七层模型 应用层&#xff1a;人机交互 抽象语言--------编码 表示层&#xff1a;编码------二进制 会话层&#xff1a;提供会话号 传输层&#xff1a;TCP/UDP 分段&#xff08;收到MTU值的限制&#xff09; MTU&#xff1a;最大传输单元&#xff…

html简易流程图

效果图 使用htmlcssjs&#xff0c;无图片&#xff0c;没用Canvas demo: <!DOCTYPE html> <html> <head><link href"draw.css" rel"stylesheet" /><script src"draw.js" type"text/javascript"></…

新手BUG:在声明了返回值的函数中不写返回值

本文对两个分别以int和string为返回值类型的函数进行分析&#xff0c;说明了在有返回值的函数中不写返回值会产生的问题。然后给出在编译阶段检查出这样的问题的办法。 一、背景 在软件测试环节发现&#xff0c;函数会在返回之前coredump。经过排查发现&#xff0c;在这个会…

单个相机矫正畸变

1、通过标定助手获取到内参外参&#xff0c;外参在此无效&#xff0c;只用到了内参 2、然后通过halcon算子进行矫正 参考&#xff1a;超人视觉

【骑士放置——最大独立集】

题目 思路 最大独立集n-最小点覆盖n-最大边匹配 代码 #include <bits/stdc.h> using namespace std; #define x first #define y second typedef pair<int, int> PII; const int N 110; int dx[8] {-2, -1, 1, 2, 2, 1, -1, -2}; int dy[8] {1, 2, 2, 1, -1, …

【每日题解】3226. 使两个整数相等的位更改次数

给你两个正整数 n 和 k。 你可以选择 n 的 二进制表示 中任意一个值为 1 的位&#xff0c;并将其改为 0。 返回使得 n 等于 k 所需要的更改次数。如果无法实现&#xff0c;返回 -1。 示例 1&#xff1a; 输入&#xff1a; n 13, k 4 输出&#xff1a; 2 解释&#xff1a…

使用Docker Swarm进行集群管理

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用Docker Swarm进行集群管理 引言 Docker Swarm 简介 安装 Docker Ubuntu CentOS 初始化 Swarm 集群 加入 Worker 节点 验证集…

SPA和SSR

单页面应用程序(SPA) 单页面应用(SPA)全称是:Single-page application, SPA应用是在客户端呈现的(术语称:CRS)。 SPA应用默认只返回一个空HTML页面&#xff0c;如:body只有<div id"app"></div>而整个应用程序的内容都是通过JavaScript动态加载&#xf…

初始JavaEE篇——多线程(4):wait、notify,饿汉模式,懒汉模式,指令重排序

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a;JavaEE 目录 wait、notify 方法 多线程练习 单例模式 饿汉模式 懒汉模式 指令重排序 wait、notify 方法 wait 和 我们前面学习的sleep…