Theory · Redis · Cache Patterns

缓存三大问题与双写一致性

穿透 / 击穿 / 雪崩 + Cache Aside 双写推演 —— 缓存面试的主战场,每道题都有"问题机制 → 方案 → 边界"三层

三大问题

穿透(查不存在)、击穿(热点过期)、雪崩(批量过期/宕机)——共同本质:请求越过缓存直打 DB

双写一致性

先更新库再删缓存的主流地位、两个并发时序的反例推演、延迟双删与 binlog 异步删除兜底

大 key / 热 key

判定标准(String>10KB、集合>5000)、UNLINK 惰性删除、热 key 本地缓存与打散

这份 deck 是缓存面试的主战场:三大问题和双写一致性,几乎每场后端面试都会抽到一两道。组织逻辑是每道题三层:问题的并发时序机制、方案清单、方案的边界与代价。双写一致性是重中之重,两个推演时序图要能手画;大 key 热 key 是工程实践题,判定标准记阿里云口径。所有 Redis 命令行为都对着官方文档核过,包括 UNLINK 的后台线程语义和 8.4 新增的 DELEX。

Why Cache Problems Matter

先看事故:缓存"只是失效了一下",全站 502

// 一个商品详情接口,大促当天的真实压力分布
读 QPS        30000/s   // 峰值,全部要读商品详情
缓存命中率      99.2%   // 只有约 240/s 真正落到 MySQL
MySQL 承载上限  2000/s   // 单实例极限,再多就排队

// 凌晨 03:00,预热脚本把 10 万个商品灌进缓存
SET item:1 {...} EX 1800   // TTL 都是 1800 秒
SET item:2 {...} EX 1800   // ……一模一样

// 03:30:00 —— 10 万个 key 在同一秒集体过期
 30000/s 全部打到 MySQL(是它上限的 15 倍)
 连接池占满,查询开始排队,响应从 5ms 变 3s
 上游超时后重试,请求量再翻倍
 03:30:12 全站 502。此时 Redis 完全健康。
关键观察:MySQL 的容量是按"缓存一直有效"这个假设规划的(2000/s 足够接住漏下来的 240/s)。所以缓存一旦漏了,DB 不是"慢一点",而是被 15 倍流量直接压死——缓存失效是一次容量规划的瞬间崩塌

如果根本不用缓存会怎样

30000/s 全程打 MySQL:要么堆到十几台只读从库(成本与运维复杂度都上一个量级),要么接受几百毫秒的响应。缓存用"一份放在内存里的副本 + 允许短暂读到旧值",换掉了"为峰值堆数据库"这件事——这是它存在的唯一理由。

代价:系统从一层存储变成了两层

本来只有 MySQL 一个真相来源,现在多了一份可能过期、可能缺失、可能被挤出去的副本。本 deck 讲的四类故障,全都是"两层"这个结构自带的,单层系统里它们根本不存在。

四类故障,各自的触发条件

穿透:要查的数据本来就不存在,缓存永远填不上,每次都打库;
击穿:一个热点 key 恰好过期的那一瞬间,成千请求同时去重建;
雪崩:一大批 key 同时过期(就是上面这场事故),或 Redis 整体挂掉;
双写不一致:库改了、缓存还是旧值——两层存储各说各话。
另外还有两个"单 key 病":大 key(一次操作太重)与热 key(访问太集中)。

本 deck 的路线

先给四种缓存模式的坐标系 → 三大问题各按"机制 → 方案 → 边界"讲透 → 双写一致性的两个并发时序推演(要能手画)→ 大 key / 热 key 的工程治理 → 速查页 + 16 道 QA。读完你应该能回答:"线上打挂了先查什么"和"为什么必须先更库再删缓存"。

动机页:用一场"Redis 全程健康但全站挂掉"的雪崩事故开场,把缓存的价值先量化——MySQL 容量是按缓存命中率规划的,所以命中率崩塌等于容量崩塌。再点明四类故障都是"两层存储"结构自带的,为后面 12 页建立统一的问题来源。禁止一上来就贴方案表,这页只讲"为什么会出事"。

Prerequisites & Glossary

先把词认全:后面每一页都会用到它们

术语一句话理解(细节后面展开)
命中 / 未命中
(cache hit / miss)
要读的 key 在缓存里找到了 = 命中;没找到 = 未命中(miss),miss 就得去数据库查
回填(backfill)miss 之后从库里查到值,再写回缓存,让下次能命中——三大问题几乎都出在这一步
TTL(Time To Live)给 key 设的存活时间,到点自动删除;缓存靠它自动淘汰旧数据
Cache Aside
(旁路缓存)
最主流的用法:应用自己读写缓存和数据库两边,缓存只是"旁边放的一份副本"
双写(dual write)一次业务修改要同时改数据库和缓存两个地方——它们无法一起原子成功,这就是不一致的根源
最终一致
(eventual consistency)
允许短暂不一致,但保证过一段时间(如 TTL 到期)一定收敛到相同值
QPS每秒请求数(Queries Per Second),衡量流量大小的基本单位
大 key / 热 key大 key = 单个 key 体积大或元素多(一次操作很重);热 key = 单个 key 被访问得特别频繁
单飞
(single-flight)
同一时刻只让一个请求去重建缓存,其余请求等它的结果——击穿的两个方案都是它的变体
假阳性
(false positive)
判断"存在"但其实不存在(误报);布隆过滤器只会误报存在,绝不会漏掉真实存在的

不在本 deck 内的前置,按需回看

过期删除与内存淘汰 → TTL 到期后 Redis 究竟什么时候真删、内存不够时谁被踢掉
单线程模型 → 为什么一条慢命令能卡住所有人(大 key 危害的根源)
分布式锁 → 击穿方案里 SET NX 抢锁的完整细节与坑
主从 / 哨兵 / Cluster → 雪崩第二种成因(实例宕机)的高可用防线

本 deck 怎么用这些词

为了不在名词上打转,后面统一说"读 miss 后回填"这一个动作;只有讲具体命令时才出现 SET NXUNLINK 这类原文。数据库一律以 MySQL 为例,但结论对任何持久化存储都成立。

最小心智模型(后面所有方案都在改它)

整份 deck 只围绕一个动作:"读不到就去库里查,查到再放回缓存"
· 三大问题 = 这句话的前半句失灵(该拦的没拦、同时去查、集体去查);
· 双写一致性 = 这句话的后半句放错了值(放回去的是旧值);
· 大 key / 热 key = 这个动作本身太重或太密
看到任何新方案,先问它在改哪一段——就不会记成一堆彼此无关的招式。

阅读提示:术语不用背,忘了回这页查。真正要能默写的只有第 9 页的时序推演和倒数第 4 页的速查表。
前置页:十个术语先定义再使用,尤其"回填"和"单飞"两个词是后面所有方案的公共零件。右侧四条前置链接分别对应 TTL 语义、单线程阻塞、SET NX 细节、高可用防线。最小心智模型把三大问题/双写/大热 key 统一挂到同一个动作的三个环节上,避免读者把它们记成互不相关的招式。

Caching Patterns Overview

四种缓存模式:先全景,再聚焦 Cache Aside

开场那场事故里的写法——应用自己读缓存、miss 后查库回填、写时删缓存——只是四种模式中的一种(Cache Aside)。先把坐标系立起来,才知道后面讨论的一致性问题属于哪一种模式

模式读写路径一致性能力适用与代价
Cache Aside(旁路缓存,主流)应用自己同时读写缓存与 DB:读 miss 回填、写"更新库 + 删缓存"最终一致(有并发窗口,见第 8-10 页)应用层完全掌控、实现简单;一致性推演是面试核心
Read-Through应用只访问缓存层,缓存层负责 miss 时从 DB 加载同 Cache Aside,但逻辑下沉需要缓存组件支持(如代理层);应用更薄
Write-Through写请求经缓存层同步写库后写缓存强(写路径双写原子性由缓存层保证)写延迟 = 库延迟 + 缓存延迟;冷数据也被写入
Write-Behind(Write-Back)先写缓存立即返回,异步批量刷库弱(缓存宕机丢未刷数据)写性能极高;需 crash 恢复设计——适合点赞/计数类

为什么业务代码普遍用 Cache Aside

Read/Write-Through 需要"缓存中间层"承担 DB 访问逻辑,自研组件成本高;Cache Aside 只需应用代码配合(先库后缓存),Redis 官方文档的 caching 场景也按"lazy loading"语义描述——控制权在应用、故障面最小

答题定位句

"四种模式的差别是'谁来维护缓存与谁对一致性负责':Cache Aside 由应用维护,Through 系列由缓存层维护,Write-Behind 用丢一致性换写性能。业务系统默认 Cache Aside,极少数写密集计数场景考虑 Write-Behind。"

先立全景再聚焦:四种缓存模式的区别在于谁来维护缓存、谁对一致性负责。Cache Aside 由应用自己读写两侧;Read-Through 和 Write-Through 把逻辑下沉到缓存层;Write-Behind 用异步刷库换写性能但可能丢数据。业务代码里九成以上是 Cache Aside,因为不需要额外的缓存中间层,控制权在应用。这页的价值是把后三页的 Cache Aside 细节放进坐标系里讲。

Cache Aside · Read / Write Flow

Cache Aside 细节:为什么"删缓存"而不是"更新缓存"

读流程(lazy loading)

① 读缓存命中 → 直接返回;② miss → 读 DB → 回填缓存并设置过期时间 → 返回。要点:回填的值来自 DB 读取那一刻,并发下可能回填旧值(第 9 页推演)。

写流程(先库后缓存)

① 更新 DB;② 删除(而非更新)缓存。下次读 miss 自动从 DB 加载最新值。删除失败是唯一残留风险 → 消息队列重试 / binlog 异步删除(第 10 页)。

追问:为什么不更新缓存?答案
① 并发写覆盖两个写并发"写 A 值→写缓存"与"写 B 值→写缓存"可能交叉执行,缓存最终落的是先发的旧值;删除是幂等的,不存在覆盖序问题
② 写多读少浪费每次写都更新缓存,若该 key 读频率低,大量更新白写(还占内存);删除后按需懒加载,只缓存真正被读的值
③ 更新≠写整对象缓存常是 DB 多表聚合结果(如商品详情),写库只动一列——更新缓存得重算整个聚合;删除让下次读自然重建
④ 那为什么不"先删缓存再更新库"?读并发会把旧值回填进缓存并长期驻留——比"更新缓存"更糟,完整推演见第 9 页
标准答案:"Cache Aside 写路径 = 更新 DB + 删缓存。删而不更,一是并发写会交叉覆盖产生脏缓存,二是懒加载避免为低频 key 白写缓存,三是删除幂等、失败可用重试和 binlog 补偿兜底。"
Cache Aside 的读写流程不难,难的是"为什么删不更"这道连环追问。四个理由按说服力排序:并发写交叉覆盖会产生脏缓存,而删除是幂等的;写多读少时更新缓存是白写;缓存常是聚合结果,写库只动一列没法定点更新;最后反过来讲先删缓存再更新库更糟,为第九页的推演埋钩子。背熟这四条,追问链就断在你手里。

Cache Penetration

缓存穿透:查"不存在",缓存形同虚设

问题机制

请求的 key 在缓存和 DB 都不存在(恶意伪造 ID、爬虫扫库):每次都 miss → 打 DB → 查不到 → 无值可回填 → 下次继续 miss。DB 被恒定流量穿透,而不是瞬时脉冲——这是它和击穿的最大区别。

方案一:缓存空值(短过期)

DB 查不到也回填一个占位值(如 "" / 约定 JSON),配短 TTL(秒级~分钟级)。代价:可能内存占用大(大量不同假 key)、且 DB 新插入该 key 后有短暂不一致窗口——TTL 即上限。适合 key 集合有限的场景。

方案做法边界与代价
缓存空值miss 且 DB 无 → set 占位值 + 短 TTL简单有效;防不住海量随机 key(占内存);有插入后的短暂不一致
布隆过滤器(下页详解)全量合法 key 预热进 BF,读前先过 BF,"不存在"直接拒挡在缓存之前,DB 零压力;有假阳性;删除困难
参数校验 / 风控格式校验(ID 必须 >0、长度/枚举检查)、用户级限流、黑名单治理恶意流量的第一道闸,工程上必做——但防不住"合法格式"的伪造 key
组合拳(推荐答法)参数校验 → 布隆过滤器 → 空值缓存兜底 → DB 限流熔断层层递进:每层把更少的流量放进下一层
与击穿/雪崩的一句话区分:"穿透是数据不存在导致恒定打库;击穿是热点 key 过期的瞬时脉冲打库;雪崩是大面积 key 同时失效或缓存宕机的全局打库——三个问题、三种流量形态,方案也完全不同。"
穿透的关键特征是数据在缓存和库里都不存在,恶意流量恒定打库。方案三层加一层兜底:参数校验和风控治非法格式;布隆过滤器把"肯定不存在"的请求挡在缓存之前;空值缓存兜住过滤器漏掉的部分,但要配短 TTL 防内存膨胀。最后用一句话把穿透击穿雪崩的流量形态区分开:恒定流量、瞬时脉冲、全局崩溃——这句话能直接检验你对三个问题的理解是否 MECE。

Bloom Filter

布隆过滤器:用位数组 + k 个哈希判"一定不存在"

布隆过滤器位数组与 k 个哈希函数的置位判定 插入 x 时用 k 个哈希函数算出 k 个下标并置 1;查询 y 时计算 k 个下标,任一为 0 即"一定不存在",全为 1 则"可能存在"(假阳性可能)。 插入 x h1(x)=2 · h2(x)=5 · h3(x)=9 3 个下标置 1 位数组 m bit(初始全 0) 0 0 1 0 0 1 0 0 0 1 0 … 下标 2/5/9 查询 y(h1/h2/h3) 任一位为 0 → 一定不存在 k 位全 1 → "可能存在"(假阳性区间) 仍需读缓存/DB,但只漏少量、不误杀 任一位为 0 → 一定不存在 → 直接拒绝 不会漏判:真实存在的 key 必然全 1 三大性质(面试必背) ① 空间效率高:1 亿 key ≈ 120MB(误判 1%) ② 有假阳性(误判"可能存在"),绝无假阴性 ③ 不支持删除:一位被多 key 共享(Counting BF) 删除问题与解法 标准 BF 删 key 会误伤共享位 → Counting Bloom Filter(每位计数器)或 定期全量重建(离线 task 重放合法 key) 参数直觉 误判率 p 越低 → 位数 m 与哈希数 k 越多 经验:p=1% 时约 9.6 bit/元素 · k≈7 预估容量错 → 实际误判率显著上升 Redis 里的实现 RedisBloom 模块:BF.ADD / BF.EXISTS (Cuckoo 滤波支持删除:CF.*) 自研:SETBIT 位图 + 多哈希函数
布隆过滤器一句话:k 个哈希把 key 映到 m 位位数组,查询时任一位为 0 就一定不存在,全为 1 只是可能存在。三大性质必背:空间效率高、有假阳性但绝无假阴性、不支持删除。删除问题给两个解法:计数布隆或定期重建,RedisBloom 里的 Cuckoo 滤波原生支持删除。参数直觉记一条:百分之一的误判率大约每元素九点六位。注意它判"可能存在"后仍要查缓存和 DB,所以只漏少量、不误杀。

Cache Breakdown / Hotspot Invalid

缓存击穿:热点 key 过期瞬间的并发重建风暴

问题机制

某个高并发热点 key 到期失效的一瞬间,成百上千请求同时 miss → 同时读 DB → 同时回填。DB 承受的是瞬时脉冲(QPS 尖刺),可能直接打挂;常出现于秒杀商品、爆款详情。

与穿透/雪崩的边界

穿透:key 根本不存在(恒定流量);击穿:key 存在但恰好过期(单点脉冲);雪崩:大量 key 同时失效或缓存实例整体宕机(全局故障)。流量形态决定方案选型。

方案机制优点代价与边界
互斥锁重建miss 后先 SET lock:hot NX PX 10000 抢锁;抢到的查 DB 回填缓存再释放;没抢到的自旋/睡眠重试读缓存保证 DB 只被一个请求打到;强一致性好其他请求等待有延迟;锁本身可能死(持锁节点挂)→ 必须带 TTL;实现相对复杂
逻辑过期不设 Redis TTL;value 里内嵌过期时间戳。读到"已逻辑过期"→ 抢互斥锁,抢到的异步线程重建缓存,所有人立即返回旧值零阻塞、体验最好;不真过期就没有击穿窗口短暂返回旧数据(最终一致);内存常驻热点数据;实现要改 value 结构
热点永不过期热点 key 直接不设 TTL,靠后台定时任务主动刷新最简单内存占用;更新有延迟;不适合大规模使用
答题模板:"击穿的本质是'过期瞬间 × 高并发重建'。互斥锁让重建串行化——保 DB、牺牲等待延迟;逻辑过期让重建异步化——保延迟、接受秒级旧数据。两者都以'单飞重建'为核心,选型看业务更怕 DB 挂还是更怕旧数据。"
击穿是热点 key 过期瞬间的脉冲。两个主流方案背出权衡关系:互斥锁重建是"串行化重建",保数据库但其他请求要等;逻辑过期是"异步化重建",value 里内嵌过期时间戳,永不在 Redis 层真过期,发现逻辑过期后异步刷新、所有人立刻拿旧值返回,保体验但接受短暂旧数据。答完方案主动补一句:互斥锁必须带 TTL 防死锁,这个细节体现工程素养。这两个方案和分布式锁那本 deck 的 SET NX 是同一件武器。

Cache Avalanche

缓存雪崩:大面积失效的全局故障与四层防御

成因一:大量 key 同时过期

批量预热的 key 设了相同 TTL(如凌晨统一导入),同一秒集体失效 → 请求洪峰全部压向 DB。本质是"击穿"的规模化。

成因二:缓存实例整体不可用

Redis 宕机 / 网络故障 / 主从切换中,全部请求落到 DB——DB 瞬时过载 → 变慢 → 更多超时 → 雪球滚大。这是故障放大链,也是雪崩最危险的形态。

防御层手段要点
① 过期打散TTL 加随机抖动ttl = base + rand(0, 600s);预热任务错峰导入治"同时过期",一行代码消除大头;必答
② 高可用架构主从 + 哨兵 / Cluster(replication-cluster deck),避免单点;多机房容灾治"实例宕机";与成因二对应
③ 多级缓存本地缓存(Caffeine / Go: ristretto, bigcache)+ Redis 组成 L1/L2;热点前置到应用内存Redis 不可用时 L1 兜住热点读;注意各层一致性窗口与容量控制
④ 限流熔断降级DB 前加限流(令牌桶/信号量)、熔断器、降级返回默认值/静态页;队列削峰异步重建最后的保命层:宁可部分不可用,不让 DB 被"打死"——保护核心链路
答题框架:"雪崩两层成因四个对策:同时过期 → TTL 随机抖动;实例宕机 → 哨兵/Cluster 高可用;运行时防线 → 多级缓存前置 + 限流熔断降级保 DB。方案与成因一一对应,这是 MECE 的答法。"
雪崩要按两层成因给对策,才是 MECE 的答案。同时过期用 TTL 加随机抖动治,一行代码解决大头;实例宕机用哨兵或 Cluster 的高可用治;再补两道运行时防线:多级缓存把热点前置到应用内存,限流熔断降级保住数据库最后的底线。答题时强调降级的价值取向:宁可部分功能不可用,也不能让 DB 被打死,这是保护核心链路的工程判断。

Cache-DB Consistency · 1/3

主流方案:先更新库、再删缓存——残余的小窗口在哪?

先更新数据库再删缓存的不一致窗口推演 写线程先更新数据库再删缓存;并发读线程在写线程更新库之后、删缓存之前读缓存命中旧值,或缓存 miss 时读到旧库值并回填。窗口只在删除完成前存在,删除成功后收敛为一致。 写线程 W(更新值 v1 → v2) 读线程 R(并发读同一 key) 存储(DB + Redis) ① UPDATE db SET v = v2 DB: v2(已提交)· 缓存: v1(旧) ② GET cache → miss 或 命中 v1? (此刻删除还没发生) ③ DEL cache 缓存: 空(v1 已被删) ④ 读 DB → 拿到 v2 → 回填缓存 缓存: v2(新)· 收敛一致 残余窗口(②~③ 之间) 删缓存前 R 若命中缓存 → 拿到旧 v1 窗口 ≈ 一次 DEL 的耗时(毫秒级) 删除失败 → 窗口无限延长 → 需兜底(下页) 为什么仍是最优解:不一致窗口 = "删缓存耗时"量级(毫秒级),且要求"读 miss 恰好插入写库与删缓存之间"——概率极小 对比"先删缓存再更新库":读线程回填的旧值会在缓存里长期驻留(直到 TTL)——严重得多(下一页推演)
先更新库再删缓存是主流方案,但面试官一定会问它漏不漏。这页推演给出精确答案:漏,但窗口极小。写线程更新库到删缓存之间,并发读要么命中旧缓存拿旧值,要么 miss 读旧库回填——注意第二步如果读线程在删除前 miss 并回填旧值,删除发生在回填之后的话缓存会留旧值,这个更极端的序要求读线程的库读发生在写库提交前,概率非常小。主结论:窗口是毫秒级 DEL 耗时量级,删除成功即收敛。删除失败才是真风险,引出下一页兜底。

Cache-DB Consistency · 2/3

反例推演:先删缓存再更新库——旧值回填为什么致命

先删缓存再更新数据库导致旧值回填的并发时序 写线程先删缓存,读线程在删缓存后、写库提交前 miss 并读到旧库值 v1 回填缓存;写线程随后提交 v2,但缓存里长期驻留 v1 直到 TTL 过期——不一致窗口被拉长到 TTL 量级。 t1t2 t3t4t5 写线程 W t1:DEL cache(先删) 缓存: 空 t4:UPDATE db SET v = v2(后更新) DB: v2 —— 但缓存里已是 v1(旧) 结果:缓存 v1(旧)· DB v2(新) 不一致持续到 TTL 过期(可能数分钟+) 之后每次读都拿到旧值——业务可见的脏读 (除非碰巧有人再写一次触发重删) 读线程 R(并发) t2:GET cache → miss(刚被 W 删掉) 转去读 DB t3:读 DB → v1(写线程还没提交 v2!) 读到的就是更新前的旧值 回填:SET cache v1(旧值入库!) 回填发生在 W 删缓存之后、写库提交之前 ——旧值越过 W 的删除动作,重新占据缓存 触发条件:读写并发的 t1~t4 交错,读慢于删、快于写 对比结论:先删后更 = 旧值回填且驻留至 TTL(分钟级、业务可见)· 先更后删 = 毫秒级窗口、删除成功即收敛 —— 所以"先更新库再删缓存"胜出 该问题在"读多写多 + 真并发"下必然出现概率;低并发业务几乎观测不到,但不能赌它不发生
这页是整份 deck 的核心推演,建议能手画。时序五步:写线程 t1 先删缓存,读线程 t2 恰好 miss 转去读库,t3 读到更新前的旧值 v1 并回填,写线程 t4 才提交 v2,于是缓存里长期驻留旧值直到 TTL。致命点在两点:回填的旧值越过了删除动作,窗口从毫秒级被拉长到 TTL 量级。对比上一页结论:先更新库再删缓存窗口毫秒级,先删缓存再更新库窗口到 TTL,所以前者胜出。手画这张图是缓存面试的分水岭。

Cache-DB Consistency · 3/3

兜底三件套:延迟双删、binlog 异步删、TTL

① 延迟双删

针对"先删后更"或担心删后回填的业务:删缓存 → 更新库 → 延迟 N(500ms~1s)→ 再删一次。第二次删除清掉并发读回填的旧值,N 要 ≥ 一次"读库+回填"耗时。代价:N 是经验值、延迟删除需异步执行。

② 订阅 binlog 异步删除(Canal 思路)

业务只管写库;Canal 伪装成 MySQL 从库订阅 binlog → 解析变更行 → 投 MQ → 消费端删对应缓存。把"删缓存"从业务里剥离:重试、幂等、顺序都交给中间件,删失败可无限重试直到成功。

③ 过期时间兜底(最后一道)

所有 key 必须设 TTL:即使删除链路彻底失败(进程挂、MQ 丢),不一致也最多持续到 TTL 到期。TTL 长度 = 业务能容忍的最大不一致窗口——用"最终一致"给整个方案封顶。

方案解决什么成本适用
先更新库 + 删缓存(基础)绝大多数场景,窗口毫秒级最低(两行代码)默认起点;配合 TTL 已满足多数业务
延迟双删极端读写交错下的旧值回填低(+一次延迟删除)没上 MQ 的中小系统快速加固
binlog 订阅删除删除失败重试、解耦业务、多缓存统一失效中(Canal+MQ 基建)一致性要求高、写路径复杂的核心系统
TTL 兜底一切兜不住的尾部风险无条件必配
结论话术(背下来):"缓存与 DB 是两个独立组件,强一致需要分布式事务,代价不成比例;工程答案是最终一致:先更新库再删缓存把窗口压到毫秒级,binlog 异步删除保证删除最终成功,过期时间兜底保证不一致有上界。面试先说清楚'做不到强一致',再说这三层,就是完整答案。"
兜底三件套按成本递进。延迟双删针对先删后更的场景,第二次删除清掉回填的旧值,延迟要盖住一次读库加回填的耗时。Canal 的思路最工程化:伪装成 MySQL 从库订阅 binlog,变更投到 MQ 异步删缓存,把重试和顺序交给中间件,业务代码里彻底不见删缓存。最后强调过期时间是不可省的兜底:删除链路全挂也不一致不过 TTL。结论话术先定性最终一致,再讲三层防线,这是满分结构。

Big Keys

大 key:判定、危害、发现、治理

判定标准(阿里云 Tair 口径)

String 的 value > 10KB;集合类型(List/Hash/Set/ZSet)元素个数 > 5000。危害与元素总量相关而不仅是单元素大小——百万成员的 ZSet 即使每条很小也是大 key。

危害链

① 慢:读写单 key 耗时长,阻塞主线程(单线程模型,thread-model deck);② 阻塞:DEL 一个百万元素集合是 O(N) 同步释放;③ 网络:一次取整 key 打满带宽;④ 复制倾斜:主从同步/迁移时单 key 巨包;⑤ 内存倾斜(Cluster 单槽热点)。

环节工具 / 做法要点
线上发现redis-cli --bigkeys(SCAN 采样,返回各类型最大 key)· --memkeys(按内存)· -i 0.1 每 100 次 SCAN 休眠 0.1s 限速采样统计非精确;MEMORY USAGE key 精查单个
离线分析对 RDB 文件离线全量分析(rdb-tools / redis-rdb-cli / 云厂商离线 key 分析)不影响线上;可输出 top N 与分布报表
删除治理UNLINK 替代 DEL(4.0+):先从键空间摘除,内存释放在后台线程执行,命令本身不阻塞UNLINK 官方原文:"performs the actual memory reclaiming in a different thread";配套 lazyfree-lazy-* 配置
结构治理拆分(Hash 分桶 hash:{id%100})、压缩(value 序列化压缩/只存 ID 查详情)、ZSet 限制长度+定期 ZREMRANGEBYRANK、大 String 转独立小 key 或压缩目标:单 key 元素数千以内、value KB 级
事故视角答题:"线上 CPU 突刺/主从切换变慢,先查大 key:--bigkeys 找到后不要 DEL(百万元素同步释放阻塞主线程),用 UNLINK 异步释放;长期治理靠拆分和压缩。SCAN 加 -i 限速,别让排查本身变成事故。"
大 key 四段式:判定、危害、发现、治理。判定背阿里云口径:字符串超 10KB、集合超 5000 个元素。危害核心是单线程模型下的阻塞和删除的 O(N) 释放。发现两条路:线上一把梭 redis-cli bigkeys 加 i 限速采样,离线分析 RDB 文件更全面。治理的杀手锏是 UNLINK:先摘键空间、后台线程释放内存,和 DEL 的同步释放本质不同,这个命令行为差异是必考点。结构上拆分压缩,把单 key 元素压到数千以内。

Hot Keys

热 key:单 key 的高频访问与打散方案

问题与判定

单个 key(或单个槽)承载不成比例的 QPS:Redis 单线程处理命令,热 key 让单节点 CPU 打满,其余节点闲置——Cluster 里热点槽成为整体瓶颈。参考阈值:阿里云 DAS 以 QPS > 5000 记为热 key(可自定义)。典型场景:秒杀商品、爆款直播、明星微博。

发现手段

redis-cli --hotkeys(依赖 LFU 淘汰策略 maxmemory-policy 为 lfu 系);② MONITOR 短时观察(有开销,慎用);③ 客户端埋点统计;④ 云厂商热 key 实时统计(DAS/控制台 Top Key)。业务大促前应做热 key 预演

方案做法要点与代价
本地缓存(L1)应用进程内缓存热 key(Go: ristretto/bigcache),Redis 退为 L2;本地 TTL 设秒级容忍旧值最有效——热 key 流量根本不出口;代价是各实例一致性窗口与缓存击穿需在本地重做(单飞/逻辑过期)
读写分离/副本分摊读请求分摊到多个副本(主从 + 读写分离,Cluster 用 READONLY 读副本)扩展读能力;写热点(如计数)不适用
key 打散(分片)写时复制 N 份 key:1..N(N = 预估 QPS/单节点承受力),读随机命中一份把单点 QPS 摊到 N 个 key/节点;写放大 N 倍,一致性维护变复杂
业务削峰计数类改本地累加 + 定期回写;秒杀类前置队列/令牌桶从业务形态上消灭"必须高频读同一 key"
与大 key 的关系:"大 key 是单个操作太重(体积/元素多),热 key 是访问频率太高(QPS 集中)——一个大而热兼有的 key 最危险:先拆小(治大),再打散/本地缓存(治热)。两者排查工具不同(--bigkeys vs --hotkeys)。"
热 key 和大 key 要分清:大是单个操作太重,热是访问频率太高。判定参考阿里云 DAS 的 QPS 五千阈值。发现工具对偶:bigkeys 对 hotkeys,后者依赖 LFU 淘汰策略。治理首选本地缓存,把热 key 流量直接挡在应用进程内,Redis 退居 L2;写热点不能用读写分离,要用 key 打散复制 N 份随机读。最后一句加分:大而热兼有的 key 最危险,先拆小再打散,两套工具分别排查。

Cheat Sheet

一页带走:定性、方案、一致性结论、上线清单

① 定性:线上出事,先分清是哪一类

现象定性与首选动作
DB QPS 恒定偏高,查的 ID 大多查不到数据穿透 → 参数校验 + 布隆过滤器 + 空值缓存(短 TTL)
DB QPS 瞬时尖刺,集中在同一个 key击穿 → 互斥锁单飞重建,或逻辑过期异步重建
DB QPS 整体飙升,缓存命中率断崖雪崩 → 查 TTL 是否同批设置 / Redis 是否宕机;限流降级先保 DB
读到的值和库里不一样,过一会儿又对了双写不一致 → 检查是否"先删缓存再更库"、删缓存是否失败
单节点 CPU 打满、其余节点空闲热 key--hotkeys 定位,本地缓存 / 打散
偶发毫秒级卡顿、主从同步变慢大 key--bigkeys 定位,UNLINK 删、拆分治理

② 三大问题方案对照(各记一主一备)

穿透主:布隆过滤器(判"不存在"直接拒);备:空值缓存 + 短 TTL;必做:参数校验
击穿主:互斥锁重建(保 DB、牺牲等待);备:逻辑过期(保延迟、接受秒级旧值)
雪崩主:TTL 随机抖动 base + rand();备:高可用 + 多级缓存;兜底:限流熔断降级

③ 双写一致性:一句话结论

先更库 + 缓存默认解。窗口 ≈ 一次 DEL 耗时(毫秒级),删成功即收敛
先删缓存 + 再更库反例。旧值被回填并驻留到 TTL(分钟级),业务可见脏读
为什么删不更并发写会交叉覆盖;写多读少白写;缓存常是聚合结果没法定点改
删失败怎么办MQ 重试 → Canal 订阅 binlog 异步删 → TTL 兜底封上界
能不能强一致不能。跨异构系统要分布式事务,代价不成比例 → 只做最终一致

④ 大 key / 热 key 口径

大 key 判定String value > 10KB;集合元素 > 5000(阿里云 Tair 口径)
热 key 判定单 key QPS > 5000(阿里云 DAS 默认口径,可自定义)
删大 keyUNLINK(后台线程释放内存)而不是 DEL(同步 O(N) 阻塞)
危害机制大 key = 单次操作重(阻塞主线程);热 key = 访问密(单节点 CPU 满)

⑤ 上线检查清单

① 每个缓存 key 必须设 TTL,且 TTL 带随机抖动;
② 写路径统一"先更库、再删缓存",删除失败有重试路径;
③ 热点 key 的 miss 路径要有单飞(互斥锁或逻辑过期),不能裸打库;
④ DB 前必须有限流/熔断/降级——宁可部分不可用,不让 DB 被打死;
⑤ 上线前跑一次 --bigkeys(带 -i 0.1 限速),确认没有巨型 key。
速查页是"可检索性优于一次性"的落点:五块按线上使用顺序排——先按现象定性,再取方案,再核对一致性结论与大热 key 口径,最后是上线清单。回看时只看这一页即可。

Interview QA · 1/2

三大问题 8 连问

先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的直接翻回上一页速查表。

1 · 穿透、击穿、雪崩一句话区分?

流量形态定方案

穿透:查不存在的数据,恒定流量打库;击穿:单个热点 key 过期,瞬时脉冲打库;雪崩:大量 key 同时失效或缓存宕机,全局流量打库。一个是"数据缺席"、一个是"时间差"、一个是"规模失效"。

2 · 布隆过滤器为什么"误判但绝不漏判"?

假阳性无假阴性

插入时 k 个哈希置位;真实存在的 key 其 k 位必然全为 1(都是自己置的)→ 绝不漏判。但别的 key 可能恰好把这些位全占了 → 假阳性"可能存在"。结论:BF 说"不存在"可放心拦截,说"存在"还得走正常链路。

3 · 布隆过滤器怎么删除元素?

位共享Counting BF / Cuckoo

不能直接删:一个位可能被多个 key 共享,清零会误伤。解法:Counting Bloom Filter(每位用计数器,删除减一);Cuckoo 滤波(RedisBloom CF.*,原生支持删除);或定期全量重建(离线重放合法 key 集)。

4 · 击穿的互斥锁方案怎么实现?有什么坑?

SET NX PX自旋等待锁带 TTL

miss 后 SET lock:key uuid NX PX 10000 抢锁,抢到者查库回填再释放(释放校验 uuid + Lua);未抢到者 sleep 后重试读缓存。坑:锁必须带 TTL(持锁节点崩溃导致死锁)、等待超时要有上限、重试读不到要降级。

5 · 逻辑过期为什么能消灭击穿窗口?

永不过期异步重建返回旧值

Redis 层不设 TTL,value 内嵌过期时间戳:逻辑过期后仍能命中缓存(值还在),先返回旧值,异步线程抢锁重建。击穿的根源是"miss 瞬间",逻辑过期让 miss 永不发生——代价是秒级旧数据。

6 · 雪崩的 TTL 抖动怎么设计?

base + rand预热错峰

过期时间 = 基础时长 + rand(0, 抖动窗口)(如 30min + rand(0,10min)),同一批导入的 key 失效点被摊开。配合预热脚本分批错峰、多级缓存把热点前置。抖动窗口过小无效,过大伤命中率——按业务过期模式调。

7 · 缓存挂了为什么 DB 会跟着挂?

故障放大链限流熔断

全量流量压向 DB → DB 响应变慢 → 应用连接池/线程池堆积 → 超时重试更多 → 雪球滚大。防线:DB 前限流(宁可拒绝部分请求)、熔断降级返回默认值、缓存侧做高可用(哨兵/Cluster)缩短不可用窗口。

8 · 什么数据不适合放缓存?

一致性敏感访问模式

强一致要求的数据(余额扣减等直接走 DB 事务)、命中率极低的冷数据(浪费内存还污染缓存)、超大 value(大 key 危害);频繁全量变更但从不读热的数据也不适合。缓存适合"读多写少 + 可容忍短暂旧值"的数据。

三大问题这组的记忆锚点是流量形态。第二三题布隆过滤器是原理题,误判不漏判的证明一句话说清。第四五题互斥锁和逻辑过期要给出权衡而不是背方案:一个保 DB 一个保延迟。第六题 TTL 抖动的窗口大小是工程细节。第八题反向题常被用来测边界感:强一致数据、低命中率冷数据、超大 value 不进缓存。每题都主动补代价,比背八股高一档。

Interview QA · 2/2

双写一致性与大 key 8 连问

同样先自答。这一组每题都要"结论 + 一句代价或边界"才算完整——只给结论一定会被追问。

9 · 为什么"先更新库再删缓存"而不是反过来?

旧值回填驻留至 TTL毫秒级 vs 分钟级

先删后更:读写并发时读线程 miss 读到旧库值回填,旧值驻留到 TTL(分钟级、业务可见)。先更后删:窗口只有"删除前读命中旧缓存"的毫秒级,删除成功即收敛。后者不一致窗口小两个数量级。

10 · 先更新库再删缓存,删失败怎么办?

MQ 重试binlog 异步TTL 封顶

三层:同步重试(有限次);把删除动作投 MQ,消费失败重试直至成功(不能阻塞主流程);Canal 订阅 binlog 异步删除,彻底与业务解耦。最坏情况 TTL 兜底——不一致有上界。

11 · 延迟双删的"延迟"怎么定?双删针对什么场景?

≥ 读库+回填耗时先删后更的补救

用于"先删缓存再更新库"或担心回填的业务:删 → 更新库 → 延迟 N → 再删。N 要覆盖"并发读从 miss 到回填完成"的耗时(几百毫秒~1s 经验值),延迟删除最好异步执行;N 估不准是固有缺陷。

12 · 为什么说缓存与 DB 做不到强一致?

两个独立组件无分布式事务

缓存与 DB 是异构独立系统,跨系统原子提交要分布式事务(2PC/Seata),延迟与复杂度不可接受。工程答案是最终一致:删缓存压窗口、异步重试保成功、TTL 封上界。

13 · 大 key 的判定标准?DEL 有什么问题?

String>10KB / 集合>5000UNLINK 后台释放

阿里云口径:String >10KB、集合元素 >5000。DEL 同步 O(N) 释放,百万元素集合能阻塞主线程数百 ms;UNLINK(4.0+)先摘除键、内存释放交后台线程,命令本身 O(1) 返回。

14 · redis-cli --bigkeys 的原理与注意点?

SCAN 采样-i 限速

游标 SCAN 遍历键空间,按类型用 TYPE/STRLEN/LLEN/HLEN 取大小,输出各类型最大 key。注意:采样统计、执行有 CPU 占用(-i 0.1 限速);只报"最大"不报"总量",精确内存查 MEMORY USAGE 或离线分析 RDB。

15 · 热 key 怎么治理?写热点为什么不能用读写分离?

本地缓存key 打散副本只分摊读

读热点:本地缓存(L1)最有效,其次副本分摊读。写热点(计数/秒杀库存):副本都有全量数据,写仍集中主库——只能 key 打散(复制 N 份随机写再聚合)、本地累加定期回写、或业务削峰。

16 · 大 key 与热 key 的治理优先级?

先拆大再治热工具成对

大而热兼有最危险:先拆小(分桶/压缩/UNLINK),再打散与本地缓存。工具成对记:--bigkeys 对 --hotkeys。大 key 害在单次操作阻塞、热 key 害在单节点 CPU,方案不能混用。

一致性这组的答案骨架在第九十二题:先更后删窗口毫秒级,先删后更窗口到 TTL,这是整份 deck 的核心结论。第十一题延迟双删要能说 N 的取法。第十三题 UNLINK 和 DEL 的线程差异是命令级考点。第十六题收尾把大 key 热 key 的危害机制对齐:一个阻塞主线程、一个打满单节点 CPU,方案不能混用,这套对比能体现系统性思维。

Related & References

相关知识点与参考

Redis 领域 · 同批 deck

主从 / 哨兵 / Cluster → 雪崩的高可用防线、热 key 副本分摊
过期删除与内存淘汰 → TTL 语义是全部方案的底层约束
Redis 分布式锁 → 击穿互斥锁的 SET NX 与看门狗
单线程模型 → 大 key / 热 key 危害的执行模型根源

跨领域 · 答题串联

布隆过滤器 → 穿透防御第一道闸:负判断零误差
LRU 缓存(手撕)→ 同为"缓存 + 惰性加载",但一致性责任在应用层
通用线索:穿透 / 击穿 = 缓存 miss 的并发放大最终一致 + TTL 兜底是跨系统缓存的通用范式

参考来源(★ = 最值得原文精读的一篇)

★ 美团技术团队《缓存那些事》双写一致性两个时序推演(先删后更 vs 先更后删)与"更新缓存 vs 删除缓存"的取舍——本 deck 核心两页的原文出处(tech.meituan.com/2017/03/17/cache-about.html)
redis.io/docs · caching 开发指南 + AWS《Database Caching Strategies using Redis》Cache Aside / lazy loading 语义与四种缓存模式定义
redis.io/commands/unlink · /delUNLINK 在后台线程回收内存;DEL 同步阻塞
redis.io/docs · redis-cli(--bigkeys / --memkeys / --hotkeys / -i)SCAN 采样统计各类型最大 key 与限速参数;--hotkeys 依赖 LFU 策略
阿里云 Tair · 大 key 与热 key 识别与处理String >10KB、集合 >5000 元素;DAS 以 QPS >5000 记为热 key
github.com/alibaba/canal伪装 MySQL slave 订阅 binlog 的异步缓存失效链路
收尾串联:雪崩的高可用防线直接引用主从哨兵那本 deck;击穿的互斥锁就是分布式锁的 SET NX 应用;大 key 热 key 危害的根源在单线程模型。跨领域对照 Buffer Pool:同样是缓存加惰性加载,但 BP 的一致性由存储引擎保证,Cache Aside 的一致性责任在应用层——这个对照能把缓存题拉到系统设计高度。参考来源里大 key 判定标准出自阿里云 Tair 文档,双写推演出自美团技术团队文章,都是可点开验证的一手链接。