想象你经营一家书店:门口是“热门展示台”——那是缓存;书店深处是“仓库”——那是数据库。顾客来问书,你先看展示台,没有再去仓库翻。正常情况下,展示台覆盖了 80% 的常见问题,仓库管理员很轻松。可一旦出现三种“坏客人”:有人专门问根本不存在的书(缓存穿透)、某本爆款书刚被撤下就涌来几百人要买(缓存击穿)、或者全店书籍约定同一分钟集体下架(缓存雪崩)——仓库管理员就会瞬间被汹涌的查书请求淹没,最终连正常业务都瘫痪。 好消息是,书店有一套完整的应对手册:门口放一本“总目录”(布隆过滤器),拦截不存在的书;柜台设“排队闸机”(互斥锁),避免爆款书空窗期被挤爆;书里夹“内容提示条”(逻辑过期),让读者永远拿得到书;下架时间加上“随机抖动”(随机过期),让集体失效变成错峰失效。这篇文章,就是带你把这套书店安保手册读透,再亲手把它写成代码。
缓存三连击是后端高并发面试的必考题,也是生产事故的高发区。掌握成因辨析与布隆过滤器、互斥锁、逻辑过期、随机过期等方案,是你从“会写 CRUD”迈向“能扛流量”的关键一步。
可视化演示
交互展示缓存穿透、缓存击穿和缓存雪崩三种高并发故障的成因与防护方案,理解缓存防护体系设计。
本节配有交互动效(进入视口后自动加载,约 3KB)。
缓存穿透:查询不存在的数据 请求绕过缓存直达数据库 布隆过滤器拦截无效查询 Bloom Filter 缓存空值防止重复穿透
穿透成因与危害
周六早晨,书店门口排着长队。一个顾客挤到前台,张口就问:“请问有《哈利波特前传:巨怪起源》吗?”店员小李查了展示台——没有;又跑到仓库翻了半天——还是没有。刚喘口气,第二个顾客又来问同一本书,小李只好再跑一趟仓库。如果这时候有个捣蛋鬼拿着脚本,每秒换一个根本不存在的书名来问,小李这一天就别干别的了,全在仓库里翻空气。
这个场景映射到系统里就是“缓存穿透”:客户端请求一个缓存和数据库里都不存在的 key。正常流程是“先查缓存,不中再查数据库”,可当 key 根本不存在时,缓存永远不可能命中,每一次请求都会穿透缓存层直达数据库。单个请求还好,一旦被恶意攻击者利用——比如用不存在的用户 id 批量刷接口——数据库的 QPS 就会被无效流量打满。
设系统入口总流量为 QPS_all,穿透发生时缓存命中率为 0,全部流量落到数据库:
1. 入口总流量 QPS_all:系统每秒钟接收到的所有请求数量,是压力的源头。
2. 缓存命中率归零:因为 key 不存在,缓存永远无法命中,形同虚设。
3. 数据库负载公式:所有流量直接落到数据库,DB_QPS 等于入口 QPS。
所以,对付这样的坏客人,书店光靠展示台是没用的——压根没有这书,怎么摆?必须在大门口就把他拦住:要么让他查“总目录”(布隆过滤器),要么在柜台登记“空查询记录”(空值缓存)。这些办法,我们在后面的章节一个个装上去。
击穿成因与危害
早上九点,书店刚开门,三十个读者同时冲到前台,点名要买《Redis 从入门到放弃》。这本书昨天明明摆在展示台最中央,可店员为了换新书,刚好在这一刻把它撤了下来——旧书还没入库,新书还没上架,展示台上空空如也。三十个人齐刷刷掉头冲向仓库,仓库管理员老赵被围得水泄不通。
这就是缓存击穿:一个真实存在、流量极高的热点 key,恰好在某一瞬间过期失效,导致大量并发请求同时发现缓存没有数据,全部回源数据库。它与穿透的关键区别在于:击穿的 key 是“本来就该有”的热门数据,只是短暂的“空窗期”撞上了高并发;而穿透的 key 是“压根不存在”的数据。
设热点 key 过期瞬间有 N_t 个并发请求同时压过来,叠加正常流量,数据库瞬时压力为:
1. 失效时刻 t:热点 key 的 TTL 到期的那一瞬间,缓存中该 key 被删除。
2. 并发请求数 N_t:恰好在 t 之后同时到达该 key 的请求数量,是尖峰的来源。
3. 数据库瞬时压力:所有并发请求同时落入数据库,QPS 出现脉冲式尖峰。
还好老赵经验丰富——他喊了一声:“第一个人跟我进仓库找书,其他人在门口排队!”这就是互斥锁的思路,我们后面就学习这套“排队放行”的武功。
雪崩成因与危害
书店有个“整点换书”的规矩:每天早上十点,把展示台上所有的书一次性撤下,换上今日推荐。问题来了:十点一到,五十位读者同时发现自己想看的书不见了,呼啦啦全涌进仓库。老赵刚应付完一本畅销书,又要面对五十个人同时查书。仓库门口挤成了早高峰地铁,连进出的路都被堵死了,其他正常采购也全部瘫痪。
这就是缓存雪崩:大量 key 在同一时间窗口集体失效,比如缓存服务重启、或者所有 key 设置了相同的过期时间(比如统一 30 分钟),导致这些 key 的请求几乎同时回源数据库。和击穿不同,击穿是“一根针扎破气球”,雪崩是“整面墙塌下来”——数据库面对的是成倍放大的流量,往往瞬间被打垮,进而引发依赖它的所有服务级联宕机。
设同一时刻失效的 key 集合大小为 K,第 i 个 key 的访问流量为 Q_i,则数据库承受的总压力为:
1. 失效 key 集合 K:在同一时间窗口内过期的 key 总数,数量越大雪崩越猛。
2. 单 key 流量 Q_i:第 i 个 key 在缓存失效后带来的回源请求量。
3. 总 DB 压力:所有失效 key 的流量叠加,构成数据库的瞬间总负载。
所以聪明的店长取消了“整点换书”,改成每本书的下架时间都错开,还让每本书自带不同的“失效小纸条”。这就是随机过期和逻辑过期的由来——雪崩不是运气不好,而是设计上埋了雷。
布隆过滤器
为了治住那些问“不存在之书”的坏客人,店长在门口放了一本巨大的《本书店收录书目总册》。每一位客人来问书,先翻总册:册子里没有这本书,直接告诉客人“本店不收”,连仓库都不用去;册子里说有这本书,再去展示台和仓库找。这册子翻得飞快,因为它只有薄薄几页索引,不存书的内容——它只回答一个问题:“这本书存不存在?”
这个“总册”就是布隆过滤器(Bloom Filter):一个很节省内存的概率型数据结构,底层是一个位数组和 k 个哈希函数。它只回答“key 是否可能存在”,不存任何数据本身。如果布隆过滤器说“不存在”,那必然不存在;如果它说“可能存在”,则有一定误判概率——因为不同 key 的哈希位可能互相覆盖。
布隆过滤器的核心操作:插入时把 k 个哈希位置置 1,查询时看 k 个位置是否全为 1。误判率 P 与位数组长度 m、已插入元素个数 n、哈希函数个数 k 的关系如下:
1. 位数组长度 m:底层比特数组的总位数,决定过滤器能容纳多少“痕迹”。
2. 插入元素数 n:已经加入过滤器的 key 个数,n 越大位数组中 1 越密集,碰撞越多。
3. 哈希函数个数 k:每个 key 映射到 k 个位置,k 越大单个 key 占的位越多,但碰撞也越复杂。
4. 误判率 P:位数组中 1 的密度与 k 共同决定“可能不存在却说存在”的概率。
有了这本总册,坏客人再来问《巨怪起源》,店员翻册子翻个一两秒就能回答“没有这本书”,连仓库的门都不用开。布隆过滤器不能告诉你书在哪儿,但能高效地告诉你“这里肯定没有”——这正是防穿透最需要的“第一道闸门”。
互斥锁方案
面对《Redis 从入门到放弃》被抢空的瞬间,老赵吼了一嗓子:“小李,你一个人进仓库找书!其余人,在柜台前排队等着!”小李翻了一分钟找到书,摆回展示台,排队的人一看展示台上有了,直接拿走。从头到尾,进仓库查书的只有小李一个人——这就是互斥锁的精髓:用一次回源,换来全场等待。
互斥锁方案是解决缓存击穿的经典手段。它的思想是:当热点 key 失效后,只允许第一个请求去数据库回源重建缓存,其他请求在短暂等待后重读缓存。在 Redis 中,最常用的实现是 SETNX(set if not exists):key 不存在时才能设置成功,返回值 1 表示“锁到手”。
设锁的 key 为 lock_key,抢锁与回源流程可以表达为:
1. SETNX 抢锁:第一个请求执行 SETNX 时锁 key 不存在,设置成功返回 1,获得回源资格。
2. 回源重建:拿到锁的请求去数据库查数据并写回缓存,再释放锁。
3. 等待重试:未拿到锁的请求 sleep 一小段时间(如 50ms)后重试读缓存,此时缓存已被重建。
补充一个关键细节:锁必须设置过期时间(如 500ms),否则拿到锁的请求中途崩溃,锁永远不释放,后续请求全部排队到死——这就是死锁。典型实现是 SET lock_key 1 EX 5 NX。
互斥锁就像老赵的“排队放行”:让一个跑腿的人进仓库,其他人在门口等结果,仓库门口永远不会被挤爆。但它也有短板——拿不到锁的请求要等,如果回源很慢,等待者会积压。于是店长又想出了更丝滑的办法:书先别撤,夹个纸条就行。
逻辑过期方案
老赵后来琢磨出一个妙招:《Redis 从入门到放弃》这本书干脆永远摆在展示台上,不撤了。但每本书里夹一张小纸条,写着“本册内容为 6 月版,可能不是最新”。读者来了,直接拿起书就走,高兴极了;营业员呢,则趁客流不大的时候去仓库把新版拿来,悄悄换掉旧书。读者永远买得到书,仓库也永远不挤。
这就是逻辑过期方案:Redis 中不设置物理 TTL,key 永久存在;但 value 中封装一个逻辑过期时间 expireAt。请求读到数据时,如果发现“当前时间 > expireAt”,并不立即丢弃数据,而是先返回旧数据给调用方(保证可用性),同时启动一个异步任务去数据库刷新缓存。由于 key 永不过期,永远不会出现“缓存中查不到”进而集体回源的场面。
value 的结构与过期判定如下:
1. 数据与过期时间封装:每个 value 不再是最朴素的业务数据,而是“数据 + 过期时刻”的结构体。
2. Redis 不设 TTL:key 永不过期,缓存层面永远“查得到”,物理上杜绝了空窗期。
3. 逻辑过期判定:读取时比较当前时间与 expireAt,发现过期则异步刷新。
这里要注意两个坑:一是异步刷新必须加互斥锁(可以复用 SETNX),防止几千个请求同时触发刷新;二是这个方案会有一小段“旧数据”窗口,属于最终一致性,对一致性要求极高的场景要慎用。
逻辑过期把“瞬间的集体恐慌”变成了“无声的暗中替换”——读者体验始终在线,数据库压力也被平摊到空闲时刻。不过,这个方案主要针对“单个热点 key”;如果整店的书都在同一分钟过期,再多的纸条也救不了。这时候就需要“错峰下架”的随机过期了。
随机过期策略
店长终于明白了“整点换书”的荒谬,于是定下新规矩:每本书的下架时间各不相同,而且在下架前五分钟,再拿个骰子随机加减几分钟。书 A 九点三十二分下架,书 B 九点四十七分下架,书 C 十点零九分下架……再也没有一分钟内几十本书同时消失的恐怖时刻,仓库门口恢复了往日的秩序。
随机过期策略是应对缓存雪崩最直接的手段,也是线上最常用的一条基线。做法很简单:给缓存 key 的 TTL 添加一个随机扰动,让原本“整齐划一”的过期时间变得错落有致。比如原本所有 key 都是 30 分钟过期,现在改成“30 分钟 + 0~300 秒随机数”,每个 key 的失效时刻就散开了,数据库承受的压力也从“一堵墙”变成了“一阵小雨”。
设基础过期时间为 TTL_base,随机扰动为 rand(0, Δ),则最终写入 Redis 的 TTL 为:
1. 基础过期时间 TTL_base:业务约定的统一缓存时长,例如 1800 秒。
2. 随机扰动 rand(0, Δ):在 0 到 Δ 秒之间均匀取一个随机数,Δ 一般取 TTL 的 10%~20%。
3. 最终 TTL':每个 key 的过期时间各不相同,形成错峰失效的分布。
对于“Redis 宕机”导致的雪崩,随机过期无能为力,需要借助多级缓存:本地进程内缓存(如 Caffeine)优先于 Redis 返回数据,即使 Redis 挂了,还有本地缓存顶住第一波,给 Redis 恢复争取时间。
随机过期就像给每本书的“下架闹钟”加了抖动,几十个闹钟再也不在同一秒响起。它是防雪崩的第一道防线,简单、高效、零依赖——如果你的系统还没有做任何缓存防护,请从这一条开始。
防护体系落地
书店经历了三连击之后,店长终于拍板:把所有的安保手段整合成一套《书店安全手册》。门口放总目录(布隆过滤器),拦住问不存在之书的坏客人;柜台设排队闸机(互斥锁),避免热门书下架时仓库被挤爆;畅销书夹内容纸条(逻辑过期),读者永远买得到书;下架时间全部错峰(随机过期),杜绝集体涌入。此外,柜台还装了限流闸机,客人太多时先在外面排队,不能让仓库管理员累倒。
一个高可用的缓存防护体系,从来不是靠单一方案,而是“分层设防、逐级兜底”。请求进系统后依次穿过布隆过滤器、本地缓存、Redis 缓存、数据库,每一层都拦截掉一部分流量,真正落到数据库的只是极少数的“漏网之鱼”。同时,限流降级熔断作为“最后的安全带”:一旦数据库压力仍然过大,立刻丢弃部分非核心请求,保证核心链路不瘫痪。
完整的请求链路与落到数据库的流量估算为:
1. 布隆拦截率 P_bloom:布隆过滤器挡掉非法 key 的比例,通常能拦下 99% 以上的穿透攻击。
2. 缓存未命中率 P_miss:经本地缓存与 Redis 之后仍未命中的请求比例,正常情况在 10% 以下。
3. 降级放行率 P_break:限流熔断后允许进入数据库的请求比例,极端情况下可以拉到很低。
4. 最终 DB 负载:各层拦截逐级相乘,数据库承受的流量被压缩到总量的极小比例。
落地的实操建议:先给所有 key 的 TTL 加随机数(成本最低),再为空值缓存加一层短 TTL 兜底;热点 key 用互斥锁或逻辑过期;在入口层引入布隆过滤器拦截非法请求;最后上线限流降级熔断,并定期压测验证。
故事的最后,书店再也没发生过“仓库被挤爆”的事故。坏客人被总目录挡在门外,热门书有排队闸机和内容纸条,全店书籍错峰下架,限流闸机又兜住了最后的风险。你看到的每一道防线,落地到代码里就是布隆过滤器、SETNX、逻辑过期和随机 TTL——这套组合拳,就是后端工程师给数据库穿上的“防弹衣”。
AI 应用场景
场景一:AI 推荐服务遭遇恶意爬虫
阶段:数据读取层 / 在线推理前置缓存
解决的问题:推荐系统对外暴露的“猜你喜欢”接口,每天被爬虫用不存在的用户 id 或 item id 狂刷。传统做法是每次请求都查数据库确认,数据库 QPS 被无效流量打满,真实用户反而得不到响应。引入布隆过滤器后,过滤器把不存在的 id 全部拦截在缓存层之前,数据库只服务真实用户。
具体案例:某资讯推荐 App 的推荐接口被批量脚本刷“收藏状态”,导致 MySQL 慢查询飙升。开发在 Redis 前加了一层布隆过滤器,非法 id 的查询直接返回空,数据库 QPS 从 6 万降到 4000,接口 TP99 从 800ms 降到 120ms。
场景二:大模型推理网关的热点缓存击穿
阶段:推理服务前置缓存(KV Cache / Token Embedding 缓存)
解决的问题:大模型推理网关对 prompt 做 embedding 缓存,当某个热门 prompt 模板的缓存恰好过期,瞬间涌来的请求全部回源到 embedding 服务,导致推理队列雪崩。用互斥锁保证只有一个请求回源重建 embedding 缓存,其余请求短暂等待后直接命中新缓存;对一致性要求不那么高的数据,也可以用逻辑过期“先返回旧值、异步刷新”。
具体案例:某 LLM 应用平台的 prompt 前缀缓存 key 平均 5 分钟过期一次,过期瞬间网关超时率飙升到 30%。改用互斥锁 + 逻辑过期后,超时率下降到 0.2%,缓存命中率稳定在 85% 以上。
场景三:大促活动期间的全链路缓存雪崩防护
阶段:高并发流量治理 / 多级缓存架构
解决的问题:618 大促零点,运营统一配置的营销配置缓存同时过期,所有促销信息请求瞬间打向数据库。通过随机过期错峰 + 本地缓存兜底 + 限流降级,把数据库从“生死边缘”拉了回来。
具体案例:某电商平台对全部营销 key 增加 0~600 秒随机抖动,并在服务本地引入 Caffeine 缓存,Redis 因网络抖动短暂不可用时,本地缓存仍能扛住 5 分钟流量,数据库零故障。
Python 演示
import randomimport threadingimport timefrom concurrent.futures import ThreadPoolExecutor class BloomFilter: def __init__(self, size=1024, k=3): self.bits = [0] * size self.size = size self.k = k def _hash(self, s, seed): h = 0 for c in str(s): h = (h * 31 + ord(c) + seed * 131) % self.size return h def add(self, key): for i in range(self.k): self.bits[self._hash(key, i)] = 1 def might_contain(self, key): return all(self.bits[self._hash(key, i)] for i in range(self.k)) class GuardedCache: def __init__(self): self.cache = {} self.bloom = BloomFilter() self.db_count = 0 self.mutex = threading.Lock() for k in ["hot", "normal"]: self.bloom.add(k) def get(self, key): if key in self.cache: return self.cache[key] if not self.bloom.might_contain(key): return None with self.mutex: if key in self.cache: return self.cache[key] self.db_count += 1 time.sleep(0.01) value = "value:" + key self.cache[key] = value return value def worker(cs, key): return cs.get(key) def main(): cs = GuardedCache() with ThreadPoolExecutor(max_workers=20) as pool: futures = [pool.submit(worker, cs, "hot") for _ in range(100)] for f in futures: f.result() print("100 concurrent requests, DB queries:", cs.db_count) print("Ghost query result:", cs.get("ghost")) print("DB queries after ghost:", cs.db_count) if __name__ == "__main__": main()
for k in ["hot", "normal"]: self.bloom.add(k)预热布隆过滤器,让合法 key 能通过“总目录”检查。if not self.bloom.might_contain(key): return None布隆说“不存在”就直接返回,连数据库的门都不敲。with self.mutex:互斥锁,保证同时只有一个线程能回源数据库,其他人排队等结果。if key in self.cache: return self.cache[key]双检:拿到锁后发现缓存已被别人重建,直接返回,不再重复查库。self.db_count += 1记录真实打到数据库的次数,运行结果会告诉你它只有 1。
常见误区
误区缓存穿透和缓存击穿不都是“缓存没数据”吗?感觉是一回事,不需要区分。
正解两者的病灶完全不同。穿透是“数据根本不存在”,缓存永远不可能命中,属于“白跑的查询”;击穿是“数据存在但刚好过期”,属于“空窗期撞上高并发”。应对方向也因此不同:穿透靠“前置拦截”(布隆过滤器、空值缓存),击穿靠“回源保护”(互斥锁、逻辑过期)。分不清就容易用错方案:用互斥锁去防穿透,拦不住层出不穷的不存在 key;用布隆过滤器去防击穿,挡不住合法热点 key 的集体回源。
为何常见:两者表象都是“缓存 miss 后打到 DB”,初学者看到同一个现象,就容易忽略背后的成因差异。
误区布隆过滤器能判断 key 是否存在,那我把所有数据都放进去,不就不用缓存和数据库了?
正解布隆过滤器只回答“可能存在 / 必不存在”,不存储数据本身。它能拦截不存在的 key,但对“存在的 key”不提供任何值,请求命中后仍然需要去缓存或数据库取数据。而且它有误判率:可能把不存在的 key 判成存在(多查一次 DB 而已),但绝不会把存在的 key 判成不存在(不会漏判)。它的定位是“准入闸门”,不是“数据仓库”。
为何常见:布隆过滤器“查存在性”的能力看起来很像缓存,容易让人误以为它可以当缓存用,其实它连数据都不存。
误区我给热点 key 加了 SETNX 互斥锁,击穿肯定就不会发生了。
正解互斥锁能防击穿,但至少有三个坑:第一,锁必须带过期时间,否则持锁线程崩溃会死锁;第二,拿不到锁的请求要“短暂等待 + 重试”,如果回源很慢,等待请求会积压成新的流量高峰;第三,锁的粒度要 key 级,不能全局一把锁,否则一个冷 key 回源会拖慢所有热点。此外,互斥锁只防“单 key 击穿”,对“大量 key 雪崩”无能为力。
为何常见:“加了锁就安全”是直觉反应,忽略了分布式锁的边界条件(锁超时、重试风暴、粒度)才是真正的考验。
误区既然要错峰,那我给每个 key 随便写个不同的过期时间,比如 1 分钟、5 分钟、10 分钟,这不就是随机过期吗?
正解随机过期的正确姿势是在一个统一基础 TTL 上加小范围随机扰动,如 1800 + rand(0, 300)。这样既保持缓存整体时效可控(大部分数据在 30 分钟左右淘汰),又能让失效时刻错开。如果各 key 的 TTL 差异过大,会出现“有的数据太旧、有的数据频繁失效”的双重问题,反而影响缓存命中率。
为何常见:字面上理解“随机”二字,忽略了工程上“基础值 + 抖动”才是既安全又高效的折中。
误区雪崩不就是 Redis 里的 key 全部失效吗?只要 Redis 不挂,就不会雪崩。
正解雪崩的本质是“大面积 key 同时失效导致数据库过载”,Redis 宕机只是触发方式之一。更常见的触发是“缓存预热时所有 key 设置了相同的过期时间”——Redis 好好的,数据库照样被打垮。而且数据库被打垮后,依赖它的其他服务会连锁超时、堆积,最终引发整个调用链的级联故障。所以防雪崩要同时在“过期错峰、本地缓存、限流熔断”三层布局,而不是只盯 Redis。
为何常见:名字里带“缓存”,容易让人把问题归因于缓存组件,忽略了“流量瞬间转移到下游”才是灾难的根源。
面试题
Q1. 缓存穿透的本质是什么?
- A.热点 key 过期瞬间大量请求压垮数据库
- B.查询缓存和数据库都不存在的 key← 正确答案
- C.大量 key 同时过期导致数据库压力剧增
- D.Redis 实例宕机导致全部请求落入数据库
答案:B。缓存穿透是请求一个缓存和数据库中都不存在的 key,每次请求都穿透缓存直达数据库。选项 A 描述的是缓存击穿,C 描述的是缓存雪崩,D 是 Redis 宕机触发的雪崩场景,三者都不是穿透的本质。
Q2. 布隆过滤器判断 key 不存在的置信度如何?
- A.100% 确定不存在,绝不会出现漏判
- B.100% 确定存在,绝不会出现误判
- C.返回不存在则必不存在,返回存在则可能误判← 正确答案
- D.判断结果与哈希函数个数完全无关
答案:C。布隆过滤器的核心特性是:若返回“不存在”,则该 key 必然不存在;若返回“存在”,则只是“可能存在”,有一定误判概率。选项 A 混淆了“误判”与“漏判”的概念,B 把特性说反了,D 忽略了哈希函数个数是影响误判率的关键参数。
Q3. 缓存穿透与击穿的本质区别是什么?
- A.穿透是合法 key 过期,击穿是非法 key 攻击
- B.穿透查询的数据不存在,击穿是热点 key 失效瞬间← 正确答案
- C.穿透只发生在读请求,击穿只发生在写请求
- D.穿透攻击数据库,击穿攻击 Redis 服务本身
答案:B。穿透是查询“根本不存在”的数据,缓存永远无法命中;击穿是“存在但刚过期”的热点 key 在空窗期遭遇高并发。选项 A 把两者的成因说反了,C 与读写方向无关,D 错误——两者都是流量落到数据库,而不是攻击 Redis 本身。
Q4. 布隆过滤器使用 k 个哈希函数的主要作用是什么?
- A.将 key 映射到位数组 k 个位置以降低碰撞概率← 正确答案
- B.为每个 key 存储 k 份数据副本以便容灾
- C.加速哈希运算从而减少查询延迟
- D.将过滤器的误判率精确降为零
答案:A。k 个哈希函数把 key 映射到位数组中的 k 个位置,多个位置共同标记,能显著降低不同 key 的碰撞概率。B 是数据存储的职责,布隆过滤器不存数据;C 哈希函数越多运算反而越慢;D 误判率可以趋近于零但无法精确为零。
Q5. SETNX 互斥锁必须设置过期时间的原因是什么?
- A.防止多个线程同时获取锁而引发死锁
- B.防止持锁请求崩溃后锁永远无法释放← 正确答案
- C.加快 Redis 内存回收并减少空间占用
- D.保证所有线程都能轮流获取到锁
答案:B。若不设置过期时间,持锁线程在回源数据库时崩溃,lock key 将永远存在,其他请求永远拿不到锁,系统陷入“饿死”状态。A 中互斥锁本身就是防止并发,不设置过期不会引发“同时拿锁”;C 与内存回收无关;D 不是过期时间的作用。
Q6. 逻辑过期方案中,发现 value 逻辑过期后首先应该做什么?
- A.立即丢弃旧数据并同步等待数据库返回
- B.先返回旧数据保证可用,再异步重建缓存← 正确答案
- C.直接删除缓存并让下一次请求回源
- D.将请求重定向到备用数据库实例
答案:B。逻辑过期的核心思想是“先服务再刷新”:发现过期后先把旧数据返回给调用方保证可用性,同时异步去数据库刷新缓存。选项 A 违背了逻辑过期消除等待的初衷,C 会让下一次请求重新经历击穿,D 属于灾难恢复方案而非逻辑过期。
Q7. 给缓存 key 设置随机过期时间的直接目的是什么?
- A.减少缓存占用内存的空间总量
- B.避免大量 key 同时失效冲击数据库← 正确答案
- C.提高热点 key 的缓存命中率
- D.让 Redis 内存淘汰策略更加公平
答案:B。随机扰动让 key 的失效时刻错峰分布,从根源上避免“集体过期”引发的雪崩。A 中随机数与内存占用无关;C 命中率主要取决于容量和访问模式;D 与 Redis 的 LRU/LFU 淘汰策略没有任何关系。
Q8. 哪些情况容易引发缓存雪崩?(多选)
- A.大量 key 设置了相同的过期时间← 正确答案
- B.Redis 集群在高峰期突发宕机← 正确答案
- C.数据库连接池参数配置过小
- D.缓存预热时未加随机化延迟← 正确答案
答案:ABD。雪崩的本质是大面积 key 同时失效导致数据库过载。A 让大量 key 在同一秒过期,D 让预热的 key 在启动后同一时刻集体失效,都是典型诱因;B 是 Redis 宕机导致的流量瞬间转移,也属于雪崩。C 是数据库自身的参数问题,不会“引发”雪崩,只会让雪崩更严重。
Q9. 下列属于缓存穿透防护手段的有?(多选)
- A.对空结果也缓存并设置较短过期时间← 正确答案
- B.用布隆过滤器拦截不存在的非法 key← 正确答案
- C.给热点 key 加分布式互斥锁
- D.为所有 key 增加随机过期时间
答案:AB。空值缓存把“查不到”的结果也缓存起来,布隆过滤器在入口拦截不存在的 key,两者都是为“不存在的数据”兜底,属于穿透防护。C 互斥锁是防击穿的手段,D 随机过期是防雪崩的手段,它们解决的不是同一类问题。
Q10. 关于布隆过滤器误判率的说法正确的是?(多选)
- A.位数组长度增大时误判率逐步降低← 正确答案
- B.插入元素数量增多时误判率逐步上升← 正确答案
- C.哈希函数数量越多误判率必然越低
- D.误判率可以估算但理论上无法降为零← 正确答案
答案:ABD。由误判率公式 P ≈ (1 - e^(-nk/m))^k 可知:m 增大时误判率降低,n 增大时误判率升高,因此 A、B 正确。C 错误——k 存在最优值,超过最优值后位数组快速变满,误判率反而上升。D 正确,哈希碰撞无法完全消除,误判率只能趋近于零。