业务设计——注册用户如何防止缓存穿透

news2024/9/25 19:18:31

什么是缓存穿透

        缓存穿透是指在使用缓存系统时,恶意或频繁地请求一个不存在于缓存中的数据,导致每次请求都需要查询数据库或其他数据存储系统,从而绕过了缓存的效果,严重影响系统性能。

这种情况通常发生在恶意攻击、大量请求缓存中不存在的数据或缓存数据过期后的高并发访问。

缓存穿透会导致以下问题:

  1. 频繁的查询数据库或其他数据存储系统,增加了数据库负载,降低了系统的吞吐量。
  2. 大量的缓存不存在的数据请求可能会导致缓存服务器的内存被耗尽,影响其他正常的缓存操作。
  3. 用户体验下降,因为请求的数据无法从缓存中获取,导致响应时间延长。

用户注册穿透场景

在高并发的会员注册场景下,可能会出现缓存穿透问题。主要原因可能是:

  • 用户注册时,需要验证用户名是否已存在,这通常需要查询数据库。
  • 如果缓存中没有该用户名,就会去数据库查询,如果数据库中也没有,就可以判断该用户名可用。
  • 在高并发的情况下,可能有大量的新用户同时注册,输入的用户名极有可能都不存在于数据库中。这将导致大量的缓存不存在,都去查询数据库,造成数据库压力剧增。
  • 且这些查询数据库的 Key 都不会被缓存,因为数据库中没有,不会写入缓存。那么这些 Key 对应的 Null 值也不会被缓存,造成每次请求都查不到缓存,直接查询数据库。
  • 这样就形成了缓存穿透情况。

而且极端情况下,注册的流程可能时恶意请求访问。注册请求缓存穿透流程图如下:

常见解决方案

所以,在用户注册场景下,需要注意防止缓存穿透,常见的处理方式有下述这些:

  1. 对不存在的 Key 进行缓存,值设为 Null,并设置短暂过期时间,如 60 秒。
  2. 使用布隆过滤器,将所有已注册的用户名存入布隆过滤器,判断时先判断该用户名是否在布隆过滤器中,不在的一定不存在,避免直接查询数据库。
  3. 使用确定的数据结构如 Redis 的 Set 集合来存储已注册用户名,判断时检查是否在集合内。
  4. 针对高并发注册场景,可以先查询缓存,如果不命中则使用分布式锁来保证只有一个线程访问数据库,避免重复查询。

但是,从真实业务场景来看,上面这些解决方案都存在弊端,不能适用于真实场景。

接下来我带大家一一解析,这些解决思路到底为什么不能用。

1. 不存在的 Key 进行缓存值设为 Null

对不存在的 Key 进行缓存,值设为 Null,并设置短暂过期时间,如 60 秒。

  • 假设用户 A 注册 username 为 magestack,查询 DB 不存在,返回请求成功,并放入 Redis 缓存。但是用户 A 并没有使用该值作为 username。
  • 如果有另一个用户 B 注册 username 为 magestack,将返回失败。也就是说每尝试一次不存在的用户名,该值 60 秒内都不可被注册。

结论:对用户使用体验不友好。此外,如果有大量并发请求查询不存在的用户名,可能会导致数据库短时间内被打挂。

2. 布隆过滤器

        使用布隆过滤器,将所有已注册的用户名存入布隆过滤器,判断时先判断该用户名是否在布隆过滤器中,不在的一定不存在,避免直接查询数据库。

  • 这种解决方案算是网上八股说的比较多的一个版本。但是依然不能解决实际场景问题。
  • 如果用户注销了账号,该用户名就可以再次被使用。然而,布隆过滤器由于无法删除元素,因此无法处理这种情况。

结论:布隆过滤器不能删除元素的限制,导致该方案无法正式使用生产。

3. Redis Set 存储已注册用户名

使用确定的数据结构如 Redis 的 Set 集合来存储已注册用户名,判断时检查是否在集合内。

  • 永久存储十几亿的用户信息到 Redis 缓存中显然不太现实,因为这会占用大量的内存资源。
  • 即使是临时存储,如果在缓存中查询不到数据,仍然无法避免查询数据库的场景。
  • 此外,对于这么多的用户信息,是否应该将其存储在一个 Key 中呢?显然是不可行的。即使进行分片,也会增加系统的复杂度。

结论:由于该方案占用内存较多且复杂度较高,因此不适合实际应用。

4. 分布式锁

        针对高并发注册场景,可以先查询缓存,如果不命中则使用分布式锁来保证只有一个线程访问数据库,避免重复查询。

  • 相对于上述解决方案,该方案在一定程度上可以解决会员注册缓存穿透的问题。
  • 但是,如果在用户注册高峰期,只有一个线程访问数据库,这可能会导致大量用户的注册请求缓慢或超时。

结论:这对用户的使用体验来说并不友好,因此我们不建议使用该方案。


项目中如何解决注册穿透

        如果没有用户名注销后可重复使用的需求,布隆过滤器无疑是最好的解决方案。但是考虑到企业的需求多样化,我们在设计时需要做好全方面的准备。

        设计时需要考虑一个问题,布隆过滤器删除不了。如果已经加进去的用户名,无疑是无法再次复用的。

        为此,我们可以通过再加一层缓存来解决这个问题。

1. 什么是布隆过滤器

        布隆过滤器(Bloom Filter)是一种高效的数据结构,用于判断一个元素是否存在于一个集合中。它利用位数组和多个哈希函数来实现快速的成员查询。

        布隆过滤器的核心思想是用一个位数组(通常用二进制位表示)来表示一个集合,初始时所有位都置为0。

        然后,对于每个要加入集合的元素,通过多个哈希函数计算出多个哈希值,然后将位数组中对应的位置置为1。

        判断一个元素是否存在于集合时,同样使用多个哈希函数计算出对应的哈希值,然后检查位数组中对应的位置是否都为1,如果有任意一个位置为0,则说明该元素不存在于集合中;如果所有位置都为1,则该元素可能存在于集合中(因为有可能发生哈希碰撞),需要进一步查询底层数据结构来确认。

关于布隆过滤器删除

        布隆过滤器在删除元素方面存在一定的限制,因为多个元素可能哈希到布隆过滤器的同一个位置。直接删除该位置的元素可能会影响其他元素的判断,因此布隆过滤器并不直接支持元素的删除操作。

关于布隆过滤器查询误判

        由于布隆过滤器使用多个哈希函数来映射元素到不同的位置,并且使用二进制位来表示元素的存在状态,这就导致了误判的可能性。具体来说,以下几个因素会导致误判:

  1. 哈希碰撞:不同的元素经过哈希函数计算可能映射到相同的二进制位,导致不同元素在布隆过滤器中产生了冲突,从而可能被误判为存在于集合中。
  2. 容量限制:布隆过滤器使用有限的二进制位数组来表示集合,当集合中元素数量较多时,可能会导致位的重复使用,从而增加误判的概率。

虽然查询元素是否存在有这误判率,但是如果查询元素是否不存在,则没有误判率。

2. 最终解决方案

上面说了,通过再加一层缓存(Redis Set )来解决布隆过滤器无法删除的问题。具体流程如下图所示:

假设我们有一条用户名为 "mading" 的数据,注册后是如何不被重复注册,以及注销后又是如何能被再次使用的。

  1. 用户名 "mading" 成功注册后,将其添加至布隆过滤器。
  2. 当其他用户查询"mading"是否已被使用时,首先检查布隆过滤器是否包含该用户名。
  3. 如果布隆过滤器中不存在该用户名,根据布隆过滤器的特点,可以确认该用户名一定没有被使用过,因此返回成功,证明该用户名可用。
  4. 如果布隆过滤器中存在该用户名,进一步检查Redis Set结构中是否包含该用户名。如果存在,表示该用户名已被注销,同样可被再次使用。
  5. 如果布隆过滤器中存在该用户名,但 Redis Set 结构中不存在,说明该用户名已被使用且尚未被注销,因此不可用。

Redis Set 结构是什么?

        当用户注销后,系统会将其用户名放入缓存结构中。如果其他用户想要使用该用户名,会检查缓存是否存在该用户名。如果缓存中存在该用户名,就表示该用户名已被注销,可以被再次使用。

使用这种方式后,会面临以下问题:

  1. 查询性能消耗增加:由于采用了额外的 Redis Set 结构,查询过程需要进行两次查询,一次是查询布隆过滤器,另一次是查询Redis Set结构,这导致查询性能相比之前有所增加。
  2. 存储损耗增加:相较于之前仅使用布隆过滤器的存储,现在需要额外存储 Redis Set 结构,这导致存储开销增加。

上面这种方式真的是没有问题的么?

        有一个问题可能会出现:如果用户频繁申请账号再注销,可能导致用户注销可复用的 Username Redis Set 结构变得庞大,增加了存储和查询的负担。

为了防止这种情况,我采取了以下解决方案:

  1. 异常行为限制:每次用户注销时,记录用户的证件号,并限制证件号仅可用于注销五次。超过这个限制的次数,将禁止该证件号再次用于注册账号。
  2. 缓存分片处理:对 Username Redis Set 结构进行分片。即使我们对异常行为进行了限制,如果有大量用户注销账户,存储这些数据在一个 Redis Set 结构中可能成为一个灾难,可能出现 Redis 大 Key 问题。因此,我将 Set 结构进行分片,根据用户名的 HashCode 进行取模操作,将数据分散存储在 1024 个 Set 结构中,从而有效地解决了这个问题。

拓展:用户注册布隆过滤器容量设置以及碰撞率问题

查询碰撞误判问题

碰撞误判问题用一句话说:指的是当布隆过滤器判断一个元素不存在于集合中时,调用判断是否存在方法它可能会返回给你存在。这种情况主要由于哈希碰撞和过滤器容量不足等原因造成的。

以下是导致布隆过滤器重复误判的一些主要原因:

  1. 哈希碰撞:布隆过滤器使用多个哈希函数将一个元素映射到多个位置。在极少数情况下,不同的元素可能会映射到相同的位置,导致误判。
  2. 过滤器容量不足:如果布隆过滤器的容量设置得不够大,会增加误判的可能性。过小的容量可能会导致哈希冲突增多,从而提高误判率。
  3. 删除操作:Redis 的布隆过滤器不支持删除操作。一旦一个元素被添加,就无法从布隆过滤器中删除。如果需要删除,可能会导致一些误判。
  4. 误判率设置不合理:布隆过滤器的误判率是可以通过合适的参数设置进行调节的。如果误判率设置得过高,可能会导致误判问题。
  5. 业务场景要求高准确性:如果业务场景对准确性要求极高,布隆过滤器可能不是最合适的选择,应该考虑其他更准确的数据结构或算法。

用户注册场景布隆过滤器实战

1. 容量如何评估

不管任何业务或者任何技术的容量评估都不会是一拍脑门决定的。

        如果淘宝商城第一年做业务时,他们可能很难预估订单量。因为他们不清楚运营会带给他们多少流量以及订单,也没有往年的相关数据参考。这种是比较难评估的。但是咱们 12306 的用户注册场景,明显不在这个范围内。

        当面试官问布隆过滤器的大小时,我们可以先说一个容量评估思路。比如:用户注册场景下布隆过滤器的主要评估来源是使用 12306 的用户,从系统刚开始运行就开始估算,大概会有多少用户会注册该平台。

        2013年12月8日推出平台,同年国内人口约等于14亿,算上前几年大部分人不会使用系统,再加上国内每年的新增人口数量,估算出一个大概值即可。该题重点在一个解题思路,并不一定需要准备的数值。如果非要说的话,让不使用 12306 的人和未来十年的增长起一个对冲,设置 14 亿即可。意味着 10 年内 14 亿这个数据量不会出问题。

2. 碰撞率如何评估

        如果需要选择一个较低的碰撞率目标。通常情况下,布隆过滤器的碰撞率可以设置在非常低的范围内,例如 0.1%或更低。

        这从根本上来说是一个空间和重复碰撞的博弈。希望空间占用小,那就尽可能让碰撞率调高。如果希望碰撞率低,那就把空间调大。

布隆过滤器的内存需求可以通过以下公式来计算:

m = -(n * ln(p)) / (ln(2)^2)

其中:

  • m 是所需要的位数。
  • n 是过滤器中元素的数量。
  • p 是期望的误报率。

在给定的条件下,其中 n 是10^9(10亿),p 是0.001(0.1%),我们可以将这些值带入公式中:

m = -(10^9 * ln(0.001)) / (ln(2)^2)

运算后,我们得到的结果 m 大约为 2.88*10^10 位。为了将位转换为字节(1字节 = 8位),我们需要除以8:

m_in_bytes = m / 8

        这将得到大约 3.6*10^9 字节,或者说约 3.6 GB 的内存需求。

        但是请注意,这是一个理想的估计值。实际上,在实际设备和实现中,布隆过滤器可能需要稍微多一些的内存。例如,Redis 的布隆过滤器插件(如 RedisBloom)可能需要一些额外的内存来维护元数据和内部结构。

3. 初始容量评估不够用怎么办

如果随着国内人口的越来越多,之前评估的布隆过滤器容量不够了怎么办?

        我们可以有个定时任务,每天统计已注册人数有多少,和布隆过滤器的预期值差值还有多少。假设布隆过滤器容量设置 14 亿,当已注册人数达到这个数量 80%时,我们通过后台任务重建布隆过滤器,在 14 亿的基础上再增加一定的数量即可。


文末总结

       缓存穿透是指在使用缓存系统时,恶意或频繁地请求一个不存在于缓存中的数据,导致每次请求都需要查询数据库或其他数据存储系统,严重影响系统性能。在高并发的用户注册场景下,可能会出现缓存穿透问题。

为了解决这个问题,我们可以采取多种解决方案,但每种方案都存在一些弊端。

  • 对不存在的 Key 进行缓存值设为 Null:虽然可以避免重复查询数据库,但对用户体验不友好,且可能会导致数据库压力增加。
  • 布隆过滤器:虽然可以快速判断是否存在于集合中,但无法处理删除元素的情况,且存在哈希碰撞导致误判的可能性。
  • Redis Set 存储已注册用户名:占用内存较多且复杂度较高,不适合实际应用。
  • 分布式锁:虽然可以解决并发查询数据库的问题,但影响用户注册的响应时间,不友好的用户体验。

        最终,我们可以采用布隆过滤器结合缓存的方式来解决缓存穿透问题。通过布隆过滤器判断用户名是否可能存在,再通过缓存来确认是否真的存在,避免了对数据库的频繁查询。为了解决布隆过滤器无法删除的问题,我们再加一层缓存来存储已注销用户名,实现了用户名的可复用性。此外,为了防止缓存结构过大带来的问题,我们对缓存进行了分片处理,有效减轻了存储和查询的负担。

        综上所述,综合考虑业务需求和系统性能,采用布隆过滤器结合缓存的解决方案是一个比较理想的解决方案,可以有效防止缓存穿透,并提升系统的性能和用户体验。

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

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

相关文章

高可用集群 keepalived

一、高可用集群: 1、简介: 高可用集群(High Availability Cluster,简称 HA Cluster),是一组相互连接的计算机或服务器,它通过保护用户的业务程序对外不间断提供的服务,把因软件、硬…

PHP如何批量修改二维数组中值

每个name值加pex,age加5, 原数据: $data[["name">a,age>12],["name">b,age>22],["name">c,age>33],["name">d,age>44], ];实现效果 方案一、foreach引用方式 $data[["…

7 款好用的 PDF 密码删除工具

这是最佳 PDF 密码删除工具列表。该列表包括免费和付费软件。将要在线完成的所有事情都需要密码才能感到安全。数据传输也是如此。大多数数据是通过文件传输的,PDF有自己的标志。PDF是一种灵活的文件格式,因此也可以专业使用。 有多种PDF 编辑器软件&am…

echart 设置 legend.type=“scroll“ 翻页时,中文上面被截取,显示不完整,解决办法

问题如图: 修复后的样子: 设置 legend 的 padding height 和 grid 都没有效果,最终找到办法: legend: {itemHeight: 10, // 截取原因是这里设置的太小textStyle: {fontSize: 20,// 行高设置会改变文本区域的高度,不…

SpringMVC Day 04 : 数据绑定

前言 SpringMVC是一个非常流行的Java Web框架,它提供了很多方便的功能和工具来帮助我们构建高效、灵活的Web应用程序。其中,数据绑定就是SpringMVC中非常重要的一部分,它可以帮助我们方便地将请求参数绑定到Java对象上,从而简化了…

北美干旱监测 (NADM)数据集

北美干旱监测 (NADM) 栅格数据集由国家环境信息中心 (NCEI) 和国家海洋和大气管理局 (NOAA) 国家综合干旱信息系统 (NIDIS) 生成。该数据集是加拿大、墨西哥和美国作者制作的北美干旱监测 (NADM) 的网格版本,其中对于每个 2.5 公里网格单元,该值由该地区…

hdlbits系列verilog解答(32位加法器)-25

文章目录 一、问题描述二、verilog源码三、仿真结果 一、问题描述 您将获得一个执行 16 位加法的模块 add16 。实例化其中两个以创建一个 32 位加法器。一个 add16 模块在接收到第一个加法器的进位结果后,计算加法结果的低 16 位,而第二个 add16 模块计…

大厂面试题-什么是JVM

JVM全称是Java虚拟机,在聊什么是JVM之前,我们不妨看⼀下这张图。 从这张图中可以看出JVM所处的位置,同时也能看出它两个作用: 1、运⾏并管理Java源码⽂件所⽣成的Class⽂件, 2、在不同的操作系统上安装不同的JVM&#…

搭建微信小程序环境及项目结构介绍

一、注册 访问微信公众平台,将鼠标的光标置于账号分类中的小程序上, 点击‘查看详情’ 点击“前往注册” 下方也可以点击注册: 小程序注册页面: 步骤a:进入小程序注册页,根据指引填写信息和提交相应的资料&#x…

VR步进式漫游,轻松构建三维模型,带来展示新形式!

引言: 虚拟现实(Virtual Reality,简称VR)已经成为当今科技领域的一项创新力量,它正在逐渐渗透到不同的领域,其中步进式漫游是VR技术的一项重要应用,它能在各个行业的宣传中发挥重要作用。 一&a…

20.1 OpenSSL 字符BASE64压缩算法

OpenSSL 是一种开源的加密库,提供了一组用于加密和解密数据、验证数字证书以及实现各种安全协议的函数和工具。它可以用于创建和管理公钥和私钥、数字证书和其他安全凭据,还支持SSL/TLS、SSH、S/MIME、PKCS等常见的加密协议和标准。 OpenSSL 的功能非常…

驱动作业10.28

驱动程序 #include <linux/init.h> #include <linux/module.h> #include<linux/of.h> #include<linux/of_gpio.h> #include<linux/gpio.h> #include<linux/timer.h> #include <linux/fs.h> #include <linux/io.h> #include &…

SpringCloud复习:(7)@EnableZuulProxy注解的作用

使用zuul时&#xff0c;需要加EnableZuulProxy注解&#xff0c;这个注解定义如下&#xff1a; 可以看到&#xff0c;它引入了一个配置类ZuulProxyMarkerConfiguration,这个类代码如下&#xff1a; 其中定义了一个类型为ZuulProxyMarkerConfiguration.Marker类型的bean. 这个…

QPushButton样式设置

QPushButton样式设置 Chapter1 QPushButton样式设置无样式的按钮改变字体颜色改变字体改变字号改变背景颜色设置上边框为2个像素&#xff0c;样式为实线&#xff0c;颜色为黑色设置所有边框为2个像素&#xff0c;样式为实线&#xff0c;颜色为黑色设置左上角圆角为&#xff1a;…

使用一个Series序列减去另一个Series序列Series.subtract()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 求两个序列中对应位置 的各元素的差 a.subtract(b) [太阳]选择题 关于以下代码的说法中正确的是? import pandas as pd a pd.Series([1,2,3]) print("【显示】a:\n",a) b pd.Seri…

服务运营 |文章精选:运筹学视角下的众包竞赛

作者&#xff1a; Guo 编者按&#xff1a;众包竞赛&#xff08;Crowdsourcing Contests&#xff09;是一种利用大规模在线社群或平台来解决问题、完成任务或创新的方法。众包竞赛通常在在线平台上进行&#xff0c;这些平台提供任务发布、参与者注册、提交作品、评审等功能。一…

向量数据库Weaviate Cloud 和 Milvus Cloud:性能大比拼

最近,随着检索增强生成系统(RAG)的持续火爆,开发者对于“如何选择一个向量数据库”的疑惑也越来越多。过去几周,我们从性能和特性能力两个方面对 Weaviate Cloud 和 MilvusCloud 进行了详细的对比。在对比过程中,我们使用了开源的性能基准测试套件 VectorDBBench,围绕诸…

高精度数字电容传感芯片-MDC04

高精度数字电容传感芯片-MDC04 简介引脚说明PCBA板寄存器说明代码实现单总线通讯时序代码单总线通讯时序代码头文件MDC04驱动代码MDC04驱动代码头文件用户APP调用函数main主程序 简介 MDC04以低成本等优势&#xff0c;可用于智能小家电液位、水箱液位、油液液位、水浸传感、食…

配置Super-VLAN下的DHCP服务器示例

组网需求 如图1所示&#xff0c;某公司拥有两个部门&#xff0c;为了节省IP地址&#xff0c;部门A和部门B规划为同一网段&#xff1b;为了提升业务安全性&#xff0c;将不同部门的用户划分到不同VLAN中。企业管理员为了方便统一管理&#xff0c;希望部门内终端通过DHCP服务器动…

4.4 审计

思维导图&#xff1a; 4.4 审计理解笔记&#xff1a; 1. 审计的重要性&#xff1a; 除了用户身份验证和访问控制&#xff0c;审计是实现数据库安全的关键组成部分。审计是达到高安全标准&#xff08;如TDI/TCSEC的C2级别&#xff09;的必要功能。 2. 审计功能的核心&#xf…