优惠券平台(十一):布隆过滤器、缓存空值、分布式组合的双重判定锁解决缓存穿透问题

业务背景

在上一节中,我们讨论了正常用户在访问优惠券时可能遇到的缓存击穿问题,并介绍了缓存预热、缓存永不过期、分布式锁、双重判定锁、分片分布式锁等技术来应对这些问题。然而,还有一个问题需要解决:如果用户频繁访问数据库中不存在的数据,就无法有效使用缓存,每次都需要访问数据库,这将导致数据库承受较大的压力。这也就是缓存穿透问题。

 什么是缓存穿透?

缓存穿透是指由于请求没有办法命中缓存,因此就会直接打到数据库,当请求量较大时,大量的请求就可能会直接把数据库打挂。

通常情况下,缓存是为了提高数据访问速度,避免频繁查询数据库。但如果攻击者故意请求缓存中不存在的数据,就会导致缓存不命中,请求直接访问数据库。

没有经过缓存穿透处理的业务伪代码如下:

public String selectUser(String userId) {String cacheData = cache.get(userId);if (StrUtil.isBlank(cacheData)) {String dbData = userMapper.selectId(userId);if (StrUtil.isNotBlank(dbData)) {cahce.set(userId, dbData);cacheData = dbData;} else {throw new RuntimeException();}}return cacheData;
}

缓存穿透常见解决方案

1. 空对象缓存

当查询结果为空时,也将结果进行缓存,但是设置一个较短的过期时间。这样在接下来的一段时间内,如果再次请求相同的数据,就可以直接从缓存中获取,而不是再次访问数据库,可以一定程度上解决缓存穿透问题。

缓存空值逻辑伪代码实现如下:

public String selectUser(String userId) {String cacheData = cache.get(userId);if (StrUtil.isBlank(cacheData)) {// 判断 Key 是否包含空值缓存,存在直接返回,不存在继续流程Boolean cacheIsNull = cache.hasKey("is-null_" + userId);if (cacheIsNull) {throw new RuntimeException();}String dbData = userMapper.selectId(userId);if (StrUtil.isNotBlank(dbData)) {cahce.set(userId, dbData);cacheData = dbData;} else {// 查询数据库中不存在数据,添加空值缓存并返回cache.set("is-null_" + userId, 较短过期时间);throw new RuntimeException();}}return cacheData;
}

这种方式是比较简单的一种实现方案,会存在一些弊端。那就是当短时间内存在大量恶意请求,缓存系统会存在大量的内存占用

2. 布隆过滤器

2.1 什么是布隆过滤器

布隆过滤器是一种数据结构,用于快速判断一个元素是否存在于一个集合中。它以牺牲一定的准确性为代价,换取了存储空间的极大节省和查询速度的显著提升。

具体来说,布隆过滤器包含一个位数组和一组哈希函数。位数组的初始值全部置为 0。在插入一个元素时,将该元素经过多个哈希函数映射到位数组上的多个位置,并将这些位置的值置为 1。

 在查询一个元素是否存在时,会将该元素经过多个哈希函数映射到位数组上的多个位置,如果所有位置的值都为 1,则认为元素存在;如果存在任一位置的值为 0,则认为元素不存在。

2.2 布隆过滤器优缺点

在查询一个元素是否存在时,会将该元素经过多个哈希函数映射到位数组上的多个位置,如果所有位置的值都为 1,则认为元素存在;如果存在任一位置的值为 0,则认为元素不存在。那么就可能会出现误判情况,比如lemon本身不存在,但经过哈希函数映射后的桶位都是1则误判为存在。 

2.3 布隆过滤器解决缓存穿透

可以将所有存量数据全部放入布隆过滤器,然后如果缓存中不存在数据,紧接着判断布隆过滤器是否存在,如果存在访问数据库请求数据,如果不存在直接返回错误响应即可。

伪代码如下:

public String selectUser(String userId) {String cacheData = cache.get(userId);if (StrUtil.isBlank(cacheData)) {if (!bloomFilter.contains(fullShortUrl)) {throw new RuntimeException();}String dbData = userMapper.selectId(userId);if (StrUtil.isNotBlank(dbData)) {cahce.set(userId, dbData);cacheData = dbData;}}return cacheData;
}

因为布隆过滤器的误判是误判不存在的数据存在,不可能误判存在的数据不存在,所以只要布隆过滤器不存在则一定不存在。 

但是这种问题还是会有一些小概率问题,那就是如果使用一种小概率误判的缓存进行攻击,依然会对数据库造成比较大的压力。这个怎么理解呢?

  • 比如说一个优惠券 ID 是 1827975299049058306,我通过优惠券 ID 规则,模拟一个不存在的但很相近的,比如 1827975299049058307,去碰撞那个误判的概率;
  • 怎么判断这个数据是不是存在?就是看接口的响应时间,直接查询缓存和布隆过滤器是绝对的毫秒级,比如 5 毫秒,而且性能基本上比较恒定。那我们就可以根据相应时间是否大于 5 毫秒,因为误判了还会查一次数据库;
  • 如果查询第一次大于 5 毫秒且数据返回为空,那就证明这是个碰撞漏网之鱼,直接拿高并发访问即可,还是会请求到数据库。

    布隆过滤器+空值缓存+分布式锁

    如果说缓存不存在,那么就通过布隆过滤器进行初步筛选,然后判断是否存在缓存空值,如果存在直接返回失败。如果不存在缓存空值,使用锁机制避免多个相同请求同时访问数据库。最后,如果请求数据库为空,那么将为空的 Key 进行空对象值缓存

    在获取到锁后,不止对正常缓存双重判定,同时也要对空值缓存对象做双重判定

    多重方案伪代码如下所示:

    public String selectUser(String userId) {String cacheData = cache.get(userId);if (StrUtil.isBlank(cacheData)) {// 判断 Key 是否存在布隆过滤器,存在则继续流程,否则直接返回if (!bloomFilter.contains(fullShortUrl)) {throw new RuntimeException();}
    ​// 判断 Key 是否包含空值缓存,存在直接返回,不存在继续流程Boolean cacheIsNull = cache.hasKey("is-null_" + userId);if (cacheIsNull) {throw new RuntimeException();}
    ​// 获取分布式锁Lock lock = getLock(userId);lock.lock();
    ​try {// 拿到锁之后进行双重判定,如果缓存已经存在则直接返回即可cacheData = cache.get(userId);if (StrUtil.isNotBlank(cacheData)) {return cacheData;}
    ​// 拿到锁之后进行双重判定,如果空值缓存已经存在则直接终止流程即可cacheIsNull = cache.hasKey("is-null_" + userId);if (!cacheIsNull) {throw new RuntimeException();}
    ​// 根据用户标识查询数据库记录String dbData = userMapper.selectId(userId);if (StrUtil.isNotBlank(dbData)) {cahce.set(userId, dbData);cacheData = dbData;} else {// 查询数据库中不存在数据,添加空值缓存并返回cache.set("is-null_" + userId, 较短过期时间);throw new RuntimeException();}} finally {lock.unlock();}}return cacheData;
    }

    这样多重方案解决缓存穿透问题感觉已经很全面了,只要不出现极端场景,大概率能涵盖大部分工作当中的业务场景。

    本章总结

    最终编写的业务逻辑代码过长,简要流程图如下:

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

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

    相关文章

    VUE 集成企微机器人通知

    message-robot 便于线上异常问题及时发现处理,项目中集成企微机器人通知,及时接收问题并处理 企微机器人通知工具类 export class MessageRobotUtil {constructor() {}/*** 发送 markdown 消息* param robotKey 机器人 ID* param title 消息标题* param…

    阿里云cdn怎样设置图片压缩

    阿里云 CDN 提供了图像加速服务,其中包括图像压缩功能。通过设置图片压缩,可以显著减小图片文件的体积,提升网站加载速度,同时减少带宽消耗。九河云来告诉你如何进行图片压缩吧。 如何设置阿里云 CDN 图片压缩? 1. 登…

    GB/T28181 开源日记[8]:国标开发速知速会

    服务端源代码 github.com/gowvp/gb28181 前端源代码 github.com/gowvp/gb28181_web 介绍 go wvp 是 Go 语言实现的开源 GB28181 解决方案,基于GB28181-2022标准实现的网络视频平台,支持 rtmp/rtsp,客户端支持网页版本和安卓 App。支持rts…

    初窥强大,AI识别技术实现图像转文字(OCR技术)

    ⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ 🐴作者:秋无之地 🐴简介:CSDN爬虫、后端、大数据、人工智能领域创作者。目前从事python全栈、爬虫和人工智能等相关工作,主要擅长领域有:python…

    如何在Docker中运行MySQL容器?

    随着容器化技术的普及,Docker已成为开发和部署应用的首选工具之一。MySQL作为最流行的开源关系型数据库,也非常适合在Docker容器中运行。本文将介绍如何在Docker中运行MySQL容器,帮助你快速搭建一个可用的数据库环境。 1. 安装Docker 首先&a…

    [ESP32:Vscode+PlatformIO]添加第三方库 开源库 与Arduino导入第三方库的区别

    前言 PlatformIO与Arduino在添加第三方库方面的原理存在显著差异 在PlatformIO中,第三方库的使用是基于项目(工程)的。具体而言,只有当你为一个特定的项目添加了某个第三方库后,该项目才能使用该库。这些第三方库的文…

    高端入门:Ollama 本地高效部署DeepSeek模型深度搜索解决方案

    目录 一、Ollama 介绍 二、Ollama下载 2.1 官网下载 2.2 GitHub下载 三、模型库 四、Ollmal 使用 4.1 模型运行(下载) 4.2 模型提问 五、Ollama 常用命令 相关推荐 一、Ollama 介绍 Ollama是一个专为在本地机器上便捷部署和运行大型语言模型&…

    【DeepSeek论文精读】2. DeepSeek LLM:以长期主义扩展开源语言模型

    欢迎关注[【youcans的AGI学习笔记】](https://blog.csdn.net/youcans/category_12244543.html)原创作品 【DeepSeek论文精读】1. 从 DeepSeek LLM 到 DeepSeek R1 【DeepSeek论文精读】2. DeepSeek LLM:以长期主义扩展开源语言模型 【DeepSeek论文精读】…

    力扣.623. 在二叉树中增加一行(链式结构的插入操作)

    Problem: 623. 在二叉树中增加一行 文章目录 题目描述思路复杂度Code 题目描述 思路 1.首先要说明,对于数据结构无非两大类结构:顺序结构、链式结构,而二叉树实质上就可以等效看作为一个二叉链表,而对于链表插入一个节点的操作是应…

    深度学习01 神经网络

    深度学习是机器学习领域中的一个新的研究方向。所以在学习深度学习之前我们需要了解一下神经网络。 神经网络 神经网络:是由大量的节点(或称“神经元”)和之间相互的联接构成。 每个节点代表一种特定的输出函数,称为激励函数、激活函数&…

    基于JUnit4和JUnit5配合例子讲解JUnit的两种运行方式

    1 引言 最近读的书有老有新,在读的过程中都完全完成了相应例子的构建和运行。在读《Spring in Action》1第4版时,其第37页的例子(以下称例子1)基于JUnit 4,并需要spring-test.jar;而在读《JUnit in Action…

    【提示词工程】探索大语言模型的参数设置:优化提示词交互的技巧

    在与大语言模型(Large Language Model, LLM)进行交互时,提示词的设计和参数设置直接影响生成内容的质量和效果。无论是通过 API 调用还是直接使用模型,掌握模型的参数配置方法都至关重要。本文将为您详细解析常见的参数设置及其应用场景,帮助您更高效地利用大语言模型。 …

    使用Python创建、读取和修改Word文档

    自动化文档处理是提升工作效率的关键路径之一,而Python凭借其简洁语法和丰富的生态工具链,是实现文档自动化处理的理想工具。通过编程手段批量生成结构规范的合同模板、动态注入数据分析结果生成可视化报告,或是快速提取海量文档中的关键信息…

    Android Studio 2024.2.2.13版本安装配置详细教程

    Android Studio 是由 Google 官方开发和维护的集成开发环境(IDE),专为 Android 应用开发设计。它是基于 JetBrains 的 IntelliJ IDEA 平台构建的,集成了丰富的工具和功能,帮助开发者高效构建、调试、测试和发布 Androi…

    Qt实现简易音乐播放器

    使用Qt6实现简易音乐播放器,效果如下: github: Gabriel-gxb/MusicPlayer: qt6实现简易音乐播放器 一、整体架构 基于Qt框架构建 整个音乐播放器程序以Qt框架为基础进行开发。Qt提供了丰富的类库和工具,方便开发者构建图形用户界…

    GPT-4使用次数有上限吗?一文了解使用规则

    GPT-4的推出,让越来越多的用户开始体验其卓越的功能。无论是用于日常需求还是专业内容制作,GPT-4的应用范围广泛,获得了用户的广泛赞誉。但是,在具体使用过程中,不少用户发现自己似乎触碰到了GPT-4的使用上限&#xff…

    水波效果

    水波效果指在计算机图形学中模拟水面波纹的视觉效果,通常用于游戏、动画或者其他虚拟场景中。主要用于体现水体的动态感,比如水的波动、反射、折射、透明等,可以让人感觉像真实的水一样流动闪耀。 核心特点就是: 动态波纹光学特…

    Redis | 十大数据类型

    文章目录 十大数据类型概述key操作命令数据类型命令及落地运用redis字符串(String)redis列表(List)redis哈希表(Hash)redis集合(Set)redis有序集合(ZSet / SortedSet&…

    Linux之安装docker

    一、检查版本和内核是否合格 Docker支持64位版本的CentOS 7和CentOS 8及更高版本,它要求Linux内核版本不低于3.10。 检查版本 cat /etc/redhat-release检查内核 uname -r二、Docker的安装 1、自动安装 Docker官方和国内daocloud都提供了一键安装的脚本&#x…

    【WebLogic】Oracle发布WebLogic 14c最新版本-14.1.2.0

    根据Oracle官方产品经理的博客,Oracle于2024年12月20日正式对外发布了WebLogic 14c的第二个正式版本,版本号为 14.1.2.0.0 ,目前官方已开放客户端下载。该版本除继续支持 Jakarta EE 8 版本外,还增加了对 Java SE 17(J…