Skip to content

模块导航: 总入口 · 总纲与方法 · 后端场景 · AI与算法工程 · 国企与金融 · 网络问答


第二章 缓存与 Redis 场景(第23-50题) ​

本章导览(元问题与表面主题逐字取自 《核心考点与元理论》§六)

  • 本章真正考的那一个问题: 用副本换延迟时,失效、一致、热点如何不把 DB 打穿?
  • 表面主题: 穿透/击穿/雪崩、锁、集群
  • 题号与题量: 第 23–50 题,共 28 题;刷题时不要按知识点记,要按上面那一个元问题记。

本章通用基线(原随题逐题重复,现收敛于此一张表;各题【关键数字】只列本题特有数字,涉及下表口径的以本表为准)

参数经验值说明
Redis 单机约 10 万 QPS简单命令;热 key 需打散
缓存命中率目标> 90%低于 80% 需排查
回源 QPS峰值 × (1-命中率)命中率 95%→70% 回源放大约 6 倍

23. 加了缓存后数据库反而更频繁被打挂(缓存三大问题辨析) ​

【考察内容】缓存三兄弟是后端面试出现率最高的缓存题,几乎必考

【题目】你们给数据库加了缓存,但线上还是出现了几次数据库被打挂的事故:一次是大量查询根本不存在的 ID 直接打到数据库;一次是某个爆款 key 刚过期,瞬间请求全部穿透;还有一次是凌晨大量 key 一起过期,数据库瞬间崩了。请区分这三种情况,并分别给出解法。

【参考答案】缓存加了反而更打挂 DB,先辨析三大根因再治:

  1. 容量估算(步进):假设 DB 读容量 1 万 QPS,业务峰值 5 万 QPS,缓存命中率若 95% 则回源 = 5万×5% = 2500 QPS,安全;若命中率掉到 70%,回源 = 5万×30% = 1.5 万 QPS,直接打穿 DB。命中率从 95%→70%,DB 压力放大约 6 倍。
  2. 三大问题辨析:① 穿透:查询的数据在缓存和 DB 都不存在,每次打 DB——解:空值缓存、布隆过滤器、参数校验;② 击穿:热点 key 过期瞬间,大量请求同时回源——解:互斥重建、逻辑过期、热点永不过期;③ 雪崩:大批 key 同时过期或缓存集群整体故障——解:TTL 加随机打散、集群高可用、限流+熔断。
  3. 其他真凶:缓存与 DB 一致性双删/订阅风暴导致重建;大 key 导致节点异常;代码 bug 未走缓存;热点 key 单节点打满。
  4. 失败与降级:回源风暴时对 DB 入口限流、返回兜底数据/排队;本地缓存紧急开启短 TTL;监控命中率、DB QPS、RT,命中率跌破 90% 立即告警、跌破 80% 必须排查。
  5. 口径:缓存不是“加了就快”,而是有命中率前提的流量转化器;命中率崩了,缓存层反而放大回源。

【原理溯源】

  • 三者本质是「缓存失效后流量去哪」的三种不同失效形态:穿透 = key 本就不该存在,缓存对它永远无效,每一次请求都是「白走一趟再打 DB」;击穿 = 单个热点 key 在某一瞬间失效,同一时刻的并发请求全部在缓存侧「撞空」;雪崩 = 成批 key 在同一时刻失效,或缓存整体不可用,失效规模从「一个」放大到「一片」。判断口诀:看「失效的是谁、失效了多少、数据该不该存在」。
  • 为什么缓存永远打不中就等于没有缓存? 缓存的收益来自命中率:DB QPS ≈ 业务 QPS × (1 - 命中率)。穿透场景命中率恒为 0,DB 完全裸奔;且因为「每次都是 miss」,攻击者可以用很低成本制造持续高压。
  • 为什么空值缓存有效? 它把「不存在」也变成一种可缓存的结果:首次 miss 仍会打 DB,但后续相同 key 在 TTL 内直接命中 null,把重复穿透压掉。代价是:①多占一点内存;②如果该 key 随后真的被创建,TTL 内会短暂读到空——所以 TTL 必须短(通常 30s–5min)。
  • 为什么布隆过滤器适合防穿透? 它在 DB 之前用极小空间回答「key 是否可能存在」:说「不存在」一定正确(可直接拦截),说「存在」可能误判(多放一次)。穿透攻击大量使用「保证不存在的 ID」,正中布隆过滤器「一定不存在」的强项。代价是标准版不可删、有误判率,需要预估容量。
  • 为什么击穿必须「只让一个请求回源」? 击穿的杀伤力来自 N 并发同时回源。互斥锁(SETNX)把 N 次回源压成 1 次 + N-1 次等待;逻辑过期把「同步重建」改成「先返回旧值 + 异步重建」,彻底消除同步等待。两者的本质都是:消灭「同一 key 的并发回源」,只是在一致性和延迟上取舍不同。
  • 为什么雪崩要打散 TTL 而不是只靠高可用? 打散 TTL 解决「批量过期」这一诱因;高可用解决「缓存整体宕机」这一更严重的诱因。两者是不同故障模式,不能互相替代。多级缓存(本地 + Redis)是在 Redis 整体不可用时的第二道隔离墙。

【选型判断树】

text
数据库被缓存流量打挂,先问三个问题:
1) 查的数据是否存在?
2) 失效的是单个 key 还是一批?
3) 缓存本身还在吗?

数据根本不存在(ID 非法/已删除/恶意遍历)
  → 穿透:参数校验 → 空值缓存 → 布隆过滤器 → 限流

数据存在,且是单个热点 key 过期瞬间
  → 击穿:强一致 → 互斥锁(Redisson)
        → 高性能可容忍旧值 → 逻辑过期
        → 极热且可后台更新 → 永不过期 + 异步刷新

大量 key 同时过期 / Redis 宕机
  → 雪崩:TTL 加随机 → 集群/哨兵 → 本地缓存兜底 → DB 限流降级

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:40定性「这题本质是三种缓存失效形态:穿透、击穿、雪崩。先区分,再对症」
0:40–2:30三兄弟辨析每种:定义一句 → 失效机理一句 → 主解法两条(穿透/空值+布隆;击穿/互斥锁+逻辑过期;雪崩/TTL随机+高可用)
2:30–3:40为什么有效空值把「不存在」变成可缓存结果;互斥锁把 N 次回源压成 1 次;TTL 随机打散过期时刻
3:40–4:30兜底与边界布隆误判只多放行不误杀;击穿/雪崩都还要 DB 限流;Redis 宕机靠本地缓存+降级
4:30–5:00收尾「一句话:先分清失效的是不存在的数据、单个热点、还是成批 key,再分别用拦截、单飞、打散+高可用」

【关键数字】

参数经验值说明
空值缓存 TTL30s – 5min太长会挡住新数据上线
布隆过滤器误判率常配 0.1%–1%空间与误判率的权衡
亿级 key 布隆空间1 亿元素:误判 1%→约 120MB、0.1%→180MB、0.01%→240MB(m=−n·ln p/(ln2)²,每元素约 9.6~19 bit)空间极省,1GB 是冗余上限口径
逻辑过期异步重建通常 1 个后台线程池防止重建风暴
TTL 随机打散幅度base + random(0, 300s)大促类可加大
DB 保护线连接池使用率 > 70%–80% 开始限流缓存失效时的最后防线,留出缓冲

【追问链】(三层)

L1|「击穿和雪崩到底怎么一句话区分?」 → 击穿是单个热点 key 在过期瞬间被高并发同时 miss;雪崩是成批 key 同时失效或缓存整体不可用。一个是「点」,一个是「面」。

L2|「布隆过滤器说 key 存在,就一定存在吗?误判了怎么办?」 → 不一定。布隆过滤器「一定不存在」是对的,「可能存在」可能误判。误判的后果是多穿透一次到 DB(查无结果),不会返回错误数据。工程上再叠一层空值缓存兜住误判后的重复穿透。

L3|「已经雪崩了,Redis 没挂,DB 快扛不住了,你先做什么?」 → 先止血再根治:①网关/应用层对回源加限流与熔断,保 DB 不死;②紧急对热点 key 设物理不过期或本地缓存预热;③观察是否是 TTL 批量到期——是则临时延长一批热点 key 的 TTL;④事后根治:TTL 随机化、多级缓存、预热。

【评分标准】

档位答案特征
60 分能说出穿透/击穿/雪崩三个名词及大致区别
80 分三种情况各自给出 2 条以上解法,并说清「不存在/单热点/批量」的判定
95 分讲清空值与布隆各自的原理与代价;击穿时能对比互斥锁 vs 逻辑过期;主动区分雪崩的两种诱因(批量过期 vs 缓存宕机)并给出对应防线

【关联题】

【自测】

  1. 判断对错:缓存击穿和缓存雪崩是一回事,只是严重程度不同。 参考答案:错。击穿是单个热点 key 过期瞬间;雪崩是大量 key 同时过期或缓存整体不可用。诱因和解法都不同。
  2. 用户恶意刷一批肯定不存在的商品 ID,你会优先上哪三层防护?每层挡什么? 参考答案:①参数校验挡明显非法 ID;②布隆过滤器在 DB 前拦截「一定不存在」;③空值缓存挡住误判/漏网后的重复穿透;再叠限流保 DB。
  3. 逻辑过期方案里,如果异步重建线程池满了会怎样?怎么防? 参考答案:重建任务被拒绝或积压,旧值会一直被返回直到重建成功。需限制并发重建数、对重建失败重试,并监控「逻辑过期但未重建」的比例。

24. 大量查询不存在的商品 ID,每次都打到数据库(缓存穿透) ​

【考察内容】缓存穿透防护与算法原理

【题目】线上发现:大量请求查询不存在的商品 ID(可能是恶意遍历,也可能数据已删),缓存永远不命中,每次请求都穿透到数据库,DB 压力暴涨。你如何解决?如果引入布隆过滤器,讲讲它的原理和适用场景。

【参考答案】缓存穿透:查不存在的数据,缓存永远不命中,每次打 DB:

  1. 容量估算(步进):假设攻击/爬虫按不存在 ID 洪水查询,峰值 2 万 QPS,DB 只能扛 5000 → 超额 1.5 万 QPS 会把连接池打满,连带正常用户超时。目标:不存在请求在缓存/过滤层被挡住,回源接近 0 或仅首次。
  2. 方案:① 缓存空值:查询为空时写 null/空标记,TTL 30s–5min(防短时重复穿透);② 布隆过滤器:预加载全量合法 ID,请求先过滤,拦截绝大多数不存在 ID(有误判,需配合空值);③ 参数校验:非法 ID 格式直接拒绝;④ 入口限流与黑名单。
  3. 适用:空值缓存实现简单,适合不存在比例高但 key 总量可控(千万级)的常规业务;布隆适合 ID 空间极大(亿级)或遭恶意遍历、且能接受误判与重建成本的场景——判据要和【选型判断树】同一条线,别把同一个条件派给两个方案。
  4. 失败与降级:布隆误判放行少量不存在 ID → 空值缓存兜底;布隆内存不足 → 分片或改空值为主;Redis 挂 → 本地布隆/全局限流;DB 已被压住 → 该接口降级限流。
  5. 监控:空值缓存命中量、布隆拦截率、DB 中“查空”SQL 占比。

【原理溯源】

  • 为什么叫「穿透」而不是「击穿」? 关键不在流量大小,而在数据是否存在。不存在的 key 永远不会被业务成功回填成「有效缓存」(除非你显式缓存空值),所以缓存层对这类请求的命中率恒为 0——流量「穿透」缓存直达 DB。如果只是「忘了回填」,那是工程 bug,不是穿透。
  • 为什么空值缓存有效,代价是什么? 把查询结果 null 也当成一种缓存值写入。因果链:miss → 查 DB 无果 → 写 key → NULL, TTL 短 → 后续同 key 在 TTL 内直接命中,不再打 DB。代价:①占用少量内存;②新数据创建后 TTL 内可能读到旧空值——所以 TTL 必须短,并在「数据真正创建」时主动删空值缓存。
  • 布隆过滤器的因果链: 位数组初始全 0 → 插入 key 时对其做 k 个独立哈希,把对应 k 个位置 1 → 查询时同样算 k 个下标,只要有一个 0,key 一定未插入过(哈希确定性保证);若全为 1,可能是真插入过,也可能是其他 key 碰巧把这 k 位都置 1 → 这就是「假阳性」误判。误判率随位数组变满而上升,所以必须按目标容量预估 m 和 k。
  • 为什么标准布隆不能删? 置 1 的位可能被多个 key 共享。把某一位清 0 可能误删其他 key 的存在证据,导致「假阴性」——把存在的 key 判成不存在,这比假阳性危险得多(会直接漏过合法请求)。因此要么不可删,要么用计数布隆/布谷鸟过滤器。
  • 为什么穿透场景优先用布隆而不是无限扩容 DB? 穿透流量的特征是「key 空间极大且大量不存在」,在 DB 侧无论怎么扩都扛不住恶意遍历;布隆在接入层用 O(k) 且几乎零网络开销把绝大部分非法 key 拦掉,是成本最低的结构性防护。

【选型判断树】

text
大量 miss 打到 DB:
├─ key 明显非法(格式错、负 ID、越权 ID)
│   → 参数校验直接拒绝(最便宜,优先做)
├─ key 合法但业务上可能不存在(商品下架/未创建)
│   ├─ 不存在比例高、key 总量可控(千万级)
│   │   → 空值缓存(实现简单,立刻见效)
│   ├─ 不存在请求量大、key 空间大(亿级/恶意遍历)
│   │   → 布隆过滤器(前置拦截)+ 空值缓存兜底误判
│   └─ 数据会删除/更新频繁
│       → 布隆需定期重建 或 换布谷鸟过滤器;空值 TTL 更要短
└─ 以上都做了仍有洪峰
    → 网关限流 + DB 熔断

判断口诀: 先拦截明显非法,再缓存「不存在」,量大上布隆,最后限流兜底。

【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性「穿透的本质是查询了不存在的数据,缓存永远 miss,DB 裸奔」
0:30–2:00防护分层校验 → 空值 → 布隆 → 限流;每层一句话原理+代价
2:00–3:30布隆原理位数组 + k 哈希;说「不存在一定对,存在可能错」;空间/误判率关系
3:30–4:30工程细节删除问题与变体(计数/布谷鸟);空值 TTL 短;数据删除后如何处理过滤器
4:30–5:00收尾「穿透不是流量问题,是『数据不该存在却反复查』问题,核心是把不存在也变成可拦截/可缓存的结果」

【关键数字】

参数经验值说明
空值缓存 TTL30s – 5min配合业务创建频率
布隆误判率目标0.1% – 1%再低空间成本陡增
位数组大小 mm ≈ -n·ln(p)/(ln2)²n=预计元素数,p=误判率
哈希函数个数 kk = (m/n)·ln2通常 5–10 个
亿级 key 空间120~240MB(误判 1%~0.01%)比存全量 key(1 亿个 key 含 SDS/dictEntry 约 6~8GB)省约 1~2 个数量级
查询复杂度O(k),与总量无关适合放在应用本地

【追问链】(三层)

L1|「布隆过滤器说不存在就一定不存在?说存在就一定存在?」 → 说「不存在」一定不存在;说「存在」可能误判。误判只发生在「存在」方向,后果是多查一次 DB,不会返回错误数据。

L2|「商品删除了,布隆过滤器怎么办?」 → 标准布隆不能精确删。方案:①定期全量重建(每日/每周,双过滤器交替切换);②计数布隆(位改计数器,删除时减);③布谷鸟过滤器(支持删除);④业务侧删除时同步删空值缓存,布隆只作粗过滤。

L3|「空值缓存和布隆过滤器该怎么组合?会不会互相打架?」 → 组合而非互斥:布隆挡「确定不存在」的大头;空值缓存挡「布隆放行但 DB 也没有」的漏网与误判。顺序是:布隆说不存在 → 拦截;布隆说可能存在 → 查缓存 → miss → 查 DB → 有则回填,无则写空值。两者一个在查询前、一个在回源后,职责不同。

【评分标准】

档位答案特征
60 分能说出空值缓存和布隆过滤器两种方案
80 分讲清布隆「位数组+多哈希」原理,以及「一定不存在 vs 可能存在」的方向性
95 分能给出 m/k 估算思路;说清为什么不能删、有哪些变体;能设计「布隆+空值」组合顺序;主动提限流兜底

【关联题】

【自测】

  1. 为什么空值缓存的 TTL 要设得很短? 参考答案:避免数据真的被创建后,用户在 TTL 内仍读到空值。TTL 短则不一致窗口短。
  2. 布隆过滤器把存在的 key 判成不存在,会有什么后果? 参考答案:标准布隆不会发生「假阴性」。若用可删除变体误清了位,可能出现假阴性——合法请求被拦截,这是必须避免的,所以删除机制要非常谨慎。
  3. 一个恶意攻击者专门生成你库中存在的合法 ID 轮询,布隆还有用吗? 参考答案:对「存在的 ID」布隆全放行,防不住。这类攻击应靠限流、风控、鉴权,而不是穿透防护。

25. 爆款商品缓存刚过期,瞬间请求全部打到 DB(缓存击穿) ​

【考察内容】击穿治理的两种主流方案对比

【题目】某爆款商品的缓存 key 在凌晨 0 点过期(价格切换),之后 1 秒内涌入 10 万请求全部穿透到数据库,DB 连接被打满。热点 key 过期瞬间的流量洪峰怎么挡?互斥锁和逻辑过期两个方案各自怎么实现、怎么选?

【参考答案】缓存击穿:热点 key 过期瞬间,并发请求全部打到 DB:

  1. 容量估算(步进):题干该热点接口 1 秒内涌入 10 万请求(QPS 10 万),Redis 单点该 key 过期,50ms 内可能涌入 5000 个 并发回源(10万×0.05),都打同一条 SQL → 连接池被打满、慢查询排队,RT 飙升甚至拖垮库(普通 SELECT 是快照读,不加行锁;压垮点是连接与 CPU)。DB 该表容量假设 2000 QPS——注意单位要换成同一个口径:5000 条/50ms 折 10 万 QPS,是 DB 容量的约 50 倍,不是“数倍”。
  2. 方案:① 互斥重建:分布式锁(Redisson)只允许一个请求查 DB 回填,其余等待或返回旧值/兜底;② 逻辑过期:value 中存过期时间,过期时后台异步刷新,前端仍可读旧值;③ 热点不过期:超高热 key 永不过期,由后台更新;④ 本地缓存分担。
  3. 选择:可短暂陈旧用逻辑过期;必须新鲜用互斥锁;极端热用不过期+后台刷新。
  4. 失败与降级:锁等待超时 → 返回兜底/旧值,不要无限等;DB 真慢 → 触发熔断与限流;回填失败 → 重试与告警。
  5. 预防:TTL 加随机(如 3600±300s)避免集体到期;监控热 key 与回源 QPS。

【原理溯源】

  • 为什么热点 key 过期会打出 10 万 QPS 到 DB? 正常路径是 QPS_db ≈ QPS × (1-命中率)。key 存在时命中率接近 100%,DB 几乎无压;key 过期瞬间命中率掉到 0,同一毫秒内所有在途请求全部 miss,形成「齐步走」回源。这不是流量变大了,是缓存挡板突然消失。
  • 互斥锁为什么能把 10 万回源压成 1 次? 用 SET lock NX EX 让并发请求抢锁:只有一个线程拿到锁去查 DB 并回填,其余请求自旋等待或退避后重读缓存。因果链:并发 miss → 抢锁 → 单线程回源 → 回填 → 释放 → 其余请求命中缓存。代价是锁等待增加 P99 延迟,且锁必须设过期防死锁。
  • 逻辑过期为什么几乎无阻塞? 它取消了「物理过期时刻」——缓存永不过期,过期语义存在 value 里。查询时若已逻辑过期:立刻返回旧值(用户无感),同时投递一个异步任务重建。因果链:逻辑过期 → 同步路径零等待 → 异步路径只被触发有限次(可用单飞合并)。代价是旧值会多活一段时间,属于最终一致。
  • 为什么生产常用「逻辑过期 + 互斥锁」组合? 逻辑过期负责用户体验(不阻塞),互斥锁负责防止「同一 key 被成百上千个异步任务同时重建」——两个问题正交:一个是同步路径要不要等,一个是异步路径要不要合并。
  • 为什么热点 key 更好的策略是「永不过期 + 后台刷新」? 对真正极热的 key,任何「过期瞬间」都是人造风险。物理永不过期 + 定时/按访问频次后台刷新,把「失效时刻」从请求路径上彻底拿掉。

【选型判断树】

text
热点 key 过期洪峰:
├─ 业务能容忍短暂旧值(秒级)?
│   ├─ 能 → 逻辑过期(主) + 异步重建限流(推荐默认)
│   └─ 不能(价格/库存刚切换)
│       ├─ 并发可控 → 互斥锁同步回源
│       └─ 并发极高 → 互斥锁 + 本地缓存挡一层 + DB 限流
├─ key 是长期热点(日活爆款)?
│   → 永不过期 + 后台刷新(消灭过期瞬间)
└─ 重建可能失败?
    → 锁必须有 TTL;失败重试 + 旧值/降级数据兜底

【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性「击穿是单个热点 key 过期瞬间,高并发同时 miss,流量齐步打 DB」
0:30–2:00互斥锁SETNX NX EX;单线程回源;强一致;有等待;必须设过期
2:00–3:20逻辑过期物理不过期;先返回旧值;异步重建;最终一致;零阻塞
3:20–4:20怎么选一致性优先→锁;性能优先→逻辑过期;极热→永不过期+刷新
4:20–5:00风险锁不设过期会死锁;重建失败要有重试;Redisson 看门狗续期

【关键数字】

参数经验值说明
互斥锁 TTL5s – 30s需覆盖一次 DB 回源
锁等待超时200ms – 1s超时可降级返回旧值
逻辑过期后旧值存活数百 ms – 数秒取决于异步重建延迟
异步重建线程小池 2–8 线程 + 队列防止重建风暴
热点判定单 key QPS > 总量 1% 或 > 5000视集群规模
刷新周期(永不过期)30s – 5min按价格变更频率

【追问链】(三层)

L1|「互斥锁为什么必须设置过期时间?」 → 防止持有锁的进程崩溃/网络分区后锁永不释放,造成死锁。所以加锁必须是原子的 SET NX EX,不能先 SETNX 再 EXPIRE。

L2|「逻辑过期方案,重建失败了会怎样?」 → 旧值会一直被返回,数据越来越旧。必须:重建失败重试(有上限)、超过阈值告警、极端时切换为互斥锁强制重建,或直接返回降级数据。

L3|「用 Redisson 实现互斥锁和裸写 SETNX 有什么本质区别?」 → Redisson 在正确性细节上做了封装:①加锁原子(SET NX EX);②解锁用 Lua 保证「比对 value + 删除」原子,防止误删他人锁;③看门狗默认 30s 锁并每 10s 续期,解决「业务执行超过锁时长」;④可重入(Hash + 重入计数)。裸写 SETNX 极易在解锁原子性和续期上踩坑。

【评分标准】

档位答案特征
60 分能说出互斥锁、逻辑过期两种方案名称
80 分两种方案各自的实现流程、优缺点、选型标准说清楚
95 分讲清「N 并发回源」的成因;指出锁必须 NX EX 原子且可续期;逻辑过期要防重建风暴;能提出「极热 key 永不过期+后台刷新」的终局方案

【关联题】

【自测】

  1. 逻辑过期和「物理 TTL + 互斥锁」最本质的区别是什么? 参考答案:逻辑过期取消了物理失效时刻,查询永不因过期而阻塞,代价是短暂旧值;互斥锁保留物理过期,用锁把并发回源压成 1 次,强一致但有等待。
  2. 为什么互斥锁方案里,其他请求可以选择「返回旧值」而不是等待? 参考答案:热点场景下等待会拖高 P99 甚至拖垮线程池;返回旧值(若业务允许)可以立刻释放线程,体验更好。这是「一致性 vs 可用性」的局部取舍。
  3. 判断对错:只要加了互斥锁,DB 就绝对安全。 参考答案:错。锁只减少「同一 key」的并发回源;若大量不同 key 同时失效(雪崩),或锁本身故障,仍会打挂 DB。还需要限流与多级缓存。

26. 凌晨大量缓存同时过期,数据库瞬间被打挂(缓存雪崩) ​

【考察内容】雪崩治理

【题目】某天凌晨 3 点,缓存里一大批 key 同时过期(很多业务都把过期时间设成了同一时刻),数据库瞬间被穿透流量打挂,服务大面积告警。这类“雪崩”怎么预防?已经发生了怎么快速恢复?

【参考答案】缓存雪崩:大批 key 同时失效,或缓存层整体不可用:

  1. 容量估算(步进):假设平时命中 95%,峰值 5 万 QPS 时 DB 只回源 2500;雪崩时命中→0,回源 5 万,为 DB 容量(1–2 万)的 2.5–5 倍,必挂。必须把“雪崩概率”当成容量问题管理,而不是小概率事件。
  2. 预防:① TTL 加随机数,分散过期;② 大促前预热热点,避免刚上线全量回源;③ 缓存集群高可用(主从/集群/多可用区),避免整体挂;④ 互斥与限流兜底。
  3. 运行时保护:回源到 DB 的并发用互斥/信号量限制;网关对该接口限流;超阈值直接返回兜底数据/排队页。
  4. 失败与降级:缓存集群故障 → 读从库/本地缓存+全局限流(阈值调低到 DB 可承受,如 3000 QPS)——「从库」要说是谁的从库:Redis 自己的从节点同属这个故障域(主从一起挂或复制中断时读它没意义),可用的是 DB 从库;且要先按第 119 题那样算清降级后的量(本地只挡 30% 时落 DB 的 QPS 与 DB 实测能力差几倍),再决定限流阈值与只保哪些接口;核心只保写与少数读;非核心接口降级。
  5. 恢复:缓存恢复后逐步放量预热,避免瞬间回流再次打挂;对账检查因降级丢弃的请求。
  6. 监控:命中率突降告警(如 1 分钟内 <80%)、DB QPS/RT/错误率联动。

【原理溯源】

  • 雪崩的两个诱因本质不同,不能混为一谈:
    • 诱因 A:批量 TTL 到期。 根因是「过期时刻高度相关」——同一段代码写死了 EXPIRE 3600,同批写入的 key 就会同秒过期。因果链:写入时间相关 + TTL 相同 → 过期时刻聚簇 → 瞬间 miss 率从 5%(第 1 条的命中 95%)跳到 50%+ → DB QPS 被放大十倍量级。
    • 诱因 B:缓存整体不可用(宕机/网络/集群故障)。这时不是「一部分 key 失效」,而是缓存层整个消失,100% 流量打 DB,通常比 A 更致命。
  • 为什么 TTL 加随机值能治诱因 A? 它打破「过期时刻相关性」:即使写入时间相同,过期时间也在一个区间内均匀分布,miss 流量从「洪峰」变成「缓坡」,DB 看到的是接近稳态的回源流量。随机幅度应大于一次回填耗时的若干倍(通常分钟级)。
  • 为什么多级缓存能治诱因 B? 本地缓存是进程内的,Redis 宕机时它仍然有效。因果链:Redis 挂 → 第一层 miss 率上升,但热点仍可被本地缓存挡住 → DB 只承受「本地也 miss」的子集。代价是各实例数据不一致,需要短 TTL 或广播失效。
  • 为什么必须有限流降级作为最后防线? 预防手段(TTL 随机、高可用)降低故障概率,但无法把概率降到 0。一旦发生,保 DB 活着比返回准确数据更重要——限流保护连接池,降级返回静态/旧数据,服务可用性优先。
  • 预热为什么能防雪崩? 大促时流量是「已知的陡增」。预先把热点加载进缓存,避免活动开始瞬间的「冷缓存集体回源」——那在效果上等同于一次人造雪崩。

【选型判断树】

text
雪崩风险治理:
├─ 预防
│   ├─ 批量过期 → TTL = base + random,禁止写死固定 TTL
│   ├─ 缓存宕机 → 集群/哨兵 + 本地缓存层
│   ├─ 大促流量 → 提前预热 + 错峰刷新
│   └─ 极热数据 → 永不过期 + 后台更新
└─ 应急(已经发生)
    ├─ 先保 DB → 网关/连接池限流、熔断
    ├─ 再保体验 → 降级返回旧数据/静态页
    └─ 后恢复 → 逐步放量、补预热、修 TTL 规则

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:40定性「雪崩是成批 key 同时失效或缓存整体不可用;和击穿的单 key 不同」
0:40–2:20两个诱因A 批量 TTL(相关过期);B 缓存宕机(整层消失)——分别对应解法
2:20–3:40四件套TTL 随机、高可用、多级缓存、限流降级;补预热
3:40–4:30应急顺序先限流保 DB → 降级 → 再恢复缓存
4:30–5:00收尾「雪崩治理核心是打破过期相关性 + 给缓存层做冗余 + 给 DB 留活路」

【关键数字】

参数经验值说明
TTL 随机幅度+0 ~ 300s大促可到 0~1800s
本地缓存 TTL(多级缓存第一层)1s–60s越热越短,配置类可放到分钟级
限流触发DB 连接池使用率 > 70%–80%留出缓冲
降级开关响应秒级手动/自动事故应急
预热启动时间活动前 30min – 2h视数据量
哨兵故障转移通常 10s–30s 级期间需降级

【追问链】(三层)

L1|「TTL 用 24 小时,是不是就不会雪崩?」 → 不会减轻,只会把雪崩推迟到 24 小时后同一时刻。关键不是 TTL 长短,而是同批 key 的过期时刻是否聚集。必须加随机扰动。

L2|「Redis 整个挂了,本地缓存还能顶多久?」 → 取决于本地 TTL 与命中率。本地缓存 TTL 短(如 10s)则很快全量打 DB;TTL 长则数据更旧。工程上:热点 key 本地 TTL 适当拉长 + 降级数据 + DB 限流,三者配合撑过故障转移窗口(通常 10–30s)。

L3|「限流降级时,怎么决定返回什么?」 → 分级:①可降级接口返回静态/缓存快照;②可延迟功能直接关闭(如推荐流);③核心交易链路保 DB、砍次要字段与关联查询;④页面层显示兜底文案。原则是牺牲非核心保核心,而不是所有接口一律失败。

【评分标准】

档位答案特征
60 分能说出 TTL 加随机、集群高可用
80 分分清批量过期与缓存宕机两种诱因,并给出多级缓存、预热、限流
95 分讲清「过期时刻相关性」是根因;应急顺序是先保 DB 再恢复;能设计降级分级策略

【关联题】

【自测】

  1. 为什么生产禁止 EXPIRE key 3600 这种写死 TTL? 参考答案:同批写入的 key 会在同一时刻过期,造成雪崩。应改为 3600 + random。
  2. 缓存宕机雪崩和批量过期雪崩,解法上最大的不同是什么? 参考答案:批量过期靠 TTL 打散;宕机靠高可用和本地缓存层——前者是时间分布问题,后者是可用性冗余问题。
  3. 已经雪崩、DB CPU 100%,你按什么顺序处理? 参考答案:①限流/熔断保护 DB;②开降级返回兜底数据;③恢复/重建缓存与预热;④事后修 TTL 规则和监控。

27. 改了数据库价格,用户看到的还是旧价格(缓存一致性) ​

【考察内容】缓存一致性是缓存类最高频考点

【题目】运营修改了商品价格(数据库已更新),但用户端看到的还是旧价格——缓存没更新。你怎么设计“更新数据库 + 更新缓存”的流程,保证数据最终一致?各个方案各自的坑是什么?

【参考答案】改了 DB 价格,用户仍看到旧价:典型的缓存一致性问题:

  1. 背景估算(步进):价格更新 QPS 很低(可能每天几百次),但读 QPS 上万。不能为了极致一致取消缓存;目标:不一致窗口控制在秒级~分钟级,资金/结算价则不允许用陈旧缓存做权威。
  2. 策略:① 先更新 DB,再删除缓存(最常用)+ TTL 兜底;② 更新频繁时用 延迟双删(更新后休眠一小段再删一次)或删除失败重试;③ 订阅 binlog(Canal)异步刷新/删除缓存,对业务无侵入;④ 强一致字段不缓存或 TTL 极短,结算时读 DB。
  3. 为什么删除而不是更新缓存:更新缓存有多写竞态与无用计算;删除让下次读时重建,逻辑更简单。
  4. 失败与降级:删除失败 → 重试队列+TTL 过期;binlog 延迟 → 监控消费位点;用户看到旧价下单 → 以订单/结算服务校验的权威价为准,旧缓存只影响展示。
  5. 监控:缓存与 DB 抽样对比差异率、平均不一致时长;运营改价后可主动刷新接口。

【原理溯源】

  • 为什么是「删缓存」而不是「更新缓存」? 并发写场景下,「更新缓存」会互相覆盖:线程 A 读旧值、线程 B 写新值并更新缓存,随后 A 才把旧值写回缓存 → 缓存长期脏。删除缓存则不同:它不写入任何具体值,让缓存失效;下次读请求从 DB 回填当前真实值。因果链:删除 → 缓存无状态 → 回填必然来自最新 DB → 规避写覆盖。

  • 为什么推荐「先更新 DB,再删缓存」? 对比两个顺序的不一致窗口:

    • 先删缓存再更新 DB:删后、DB 更新前,并发读 miss → 从 DB 读到旧值 → 回填缓存。若回填发生在 DB 更新之后且第二次删除之前,脏值可长期存在(直到 TTL)。
    • 先更新 DB 再删缓存:DB 已是新值,删缓存前的读可能命中旧缓存(毫秒级窗口),删除后即恢复一致。窗口短且可自愈。
    • 结论:后者不一致概率更低、恢复更快,是工程默认。
  • 为什么删除失败必须有补偿? 「删除」和「更新 DB」不在同一事务里:DB 提交成功后、删缓存前进程崩溃/网络失败,缓存就永久脏(直到 TTL)。因果链:跨系统无法原子 → 必须补偿。手段:重试队列、MQ、订阅 binlog(Canal)在 DB 变更事件后异步删缓存。

  • binlog 订阅为什么更可靠? 它以 DB 的提交事实 为唯一真相源:只要 DB 变更成功,binlog 一定产生(MySQL 保证),Canal 消费后删缓存。不依赖应用层「记得发删除」,天然覆盖应用崩溃场景。代价是延迟(毫秒~秒级)和额外组件。

  • 为什么最终一致是合理目标? 跨进程/跨存储无法在性能可接受前提下做到强一致(要强一致就得分布式事务或串行化读)。缓存的本质是「用可容忍的旧数据换延迟与吞吐」,所以目标应是:不一致窗口极短 + 必然自愈 + 关键业务可旁路。

  • 元理论对应: 原理 4「时间差、多副本视图与一致性窗口」+原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§4.7+§6.7)

【选型判断树】

text
缓存一致性怎么选:
├─ 数据是否强一致(钱、库存扣减结果)?
│   └─ 是 → 核心读路径直连 DB / 读主库,缓存只做旁路加速非关键读
├─ 一般业务数据(商品、资料、配置)
│   └─ Cache Aside:先更 DB,再删缓存
│       ├─ 删除可能失败 → 重试 + binlog 异步删
│       └─ 仍有并发回填旧值 → 见延迟双删 / 版本号(第 28 题)
├─ 写多读少且要简单
│   └─ 可考虑 Write Through(同步写 DB)
└─ 可容忍丢失(计数、日志类)
    └─ Write Behind(第 29 题)

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性「缓存一致性推荐 Cache Aside:读旁路、写先更库再删缓存,目标是最终一致」
0:30–2:00读写流程读:cache miss → DB → 回填;写:update DB → delete cache
2:00–3:20两个关键选择为什么删不更新(覆盖问题);为什么先库后删(窗口更短)
3:20–4:20失败补偿删除失败重试 / MQ / Canal binlog;短 TTL 自愈
4:20–5:00收尾「缓存不能和 DB 做成强一致事务,工程上用短窗口+必然自愈逼近一致」

【关键数字】

参数经验值说明
删除失败重试3–5 次,间隔指数退避仍失败进补偿队列
binlog 异步删延迟通常 10ms–1s取决于 Canal 消费
兜底 TTL(缓存一致性的自愈兜底)热点 1–10 分钟;普通可更长权衡一致性与命中率
「先删后更」脏值存活可达一个 TTL 周期为什么不用这个顺序
「先更后删」不一致窗口毫秒级(删缓存前)可接受

【追问链】(三层)

L1|「能不能更新缓存而不是删除?」 → 可以但不推荐。并发下两个线程更新缓存可能旧值覆盖新值;删除让缓存无状态,回填一定来自最新 DB。只有在写极不频繁、且能保证串行时,更新缓存才可接受。

L2|「延迟双删解决的是删除失败吗?」 → 不是。删除失败要靠重试/binlog。延迟双删解决的是「先删缓存再更新 DB」或并发读在窗口内把旧值回填的问题——是时序竞态问题,不是失败补偿问题。两者经常被混为一谈。

L3|「如何做到接近强一致?」 → ①关键读直连 DB 或读主库;②写后 N 秒内强制读主(会话粘滞/读己之写);③缓存 value 带版本号,回填时比对;④分布式锁把「更新 DB + 删缓存」串行化(代价高,少用)。一般业务不值得为强一致付出这些成本。

【评分标准】

档位答案特征
60 分能说出先更 DB 再删缓存
80 分讲清「删 vs 更新」「先库 vs 先删」两个选择的理由,并给出失败补偿
95 分区分「删除失败」与「并发回填旧值」两类问题;能说清 binlog 方案为何更可靠;指出强一致场景应绕过缓存而非硬做

【关联题】

【自测】

  1. 为什么「先删缓存再更新 DB」不一致窗口更大? 参考答案:删后、DB 更新前的并发读会把旧值回填,且可能发生在 DB 更新之后,导致脏值长期存在。
  2. 删除缓存失败了,最可靠的兜底是什么? 参考答案:订阅 MySQL binlog(Canal),以 DB 提交为真相源异步删缓存;辅以重试和短 TTL。
  3. 判断对错:只要用 Cache Aside,缓存和 DB 就一定一致。 参考答案:错。Cache Aside 是最终一致,删除失败或极端并发仍有窗口,需要补偿与 TTL。

28. 先改库再删缓存,旧数据还是被写回了缓存(延迟双删) ​

【考察内容】缓存一致性方案的深入理解

【题目】更新商品价格的流程是“先改数据库、再删缓存”,但并发下仍出问题:一个请求刚删完缓存,另一个并发请求已把旧价格写回缓存,用户继续看到旧价。怎么解决?常说的“延迟双删”是怎么做的?它又有什么缺陷?

【参考答案】先改库再删缓存,旧数据仍被写回——经典竞态,用延迟双删 + 兜底:

  1. 竞态过程(步进估算):读线程 A 缓存 miss、读到旧 DB 值准备回填 → 写线程 B 更新 DB 并删缓存 → A 的“重建逻辑”用读到的旧 DB 快照(或删除前读的数据)写回缓存 → 新值被旧值覆盖。时间窗通常几毫秒~几十毫秒,高并发下每天可发生多次。
  2. 延迟双删:B 更新 DB 后删除缓存,延迟一小段时间(略大于一次读的耗时,如 500ms–1s)再删一次,覆盖写回竞态。实现可用延迟队列/MQ/Spring 事件。
  3. 更稳方案:① 订阅 binlog 删缓存(最终以 DB 变更事件为准);② 给缓存值带版本号,重建时 CAS/仅当版本更新才写入;③ 重建时重新查 DB(不要用请求内旧变量);④ 必要时读写分布式锁(代价高,少用)。
  4. 失败与降级:延迟删除失败 → TTL 兜底+重试;仍短暂不一致 → 业务允许范围内的展示误差;价格/账户以 DB/订单校验为准。
  5. 口径:一致性是概率与延迟的权衡;展示类允许秒级不一致,交易类必须权威读。

【原理溯源】

  • 为什么「先更 DB 再删缓存」仍可能写回旧值? 严格时序上的经典竞态:

    1. 读请求 R1 缓存 miss,去查 DB,读到旧价格;
    2. 写请求 W 完成「更新 DB + 删除缓存」;
    3. R1 才把查到的旧价格回填缓存。 结果:DB 是新价,缓存是旧价,且会一直脏到 TTL 过期。延迟双删的「第二次删除」就是想覆盖步骤 3 的回填。
  • 延迟双删的因果链(本题口径:先更 DB 再删缓存。通用判定见第 38 题——先更后删时双删并非必须,本题是因实测回填竞态明显才叠加双删): 更新 DB → 第一次删 → 等待 Δt → 第二次删,第二次删就是为了清掉窗口期内被 R1 回填的旧值。Δt 必须 大于一次「DB 查询 + 缓存回填」的耗时,否则第二次删发生在回填之前,删了也没用。另有"先删缓存再更新 DB"的变体,它的双删是 删→更库→Δt→再删;两种写法的第二次删都必须落在回填之后,这才是双删成立的唯一条件。

  • 为什么它只是概率性方案? 若 R1 的回填延迟超过 Δt,或第二次删除后又有更早的读请求回填,脏值仍会出现。它把不一致概率大幅降低,但没有消除竞态本身——这是和 binlog 方案的本质差异。

  • 为什么 binlog + 异步删除更可靠? 它不猜时序,而是以「DB 已提交」为触发点删除缓存。只要最终消费了该 key 的变更事件,就会再删一次。即便应用层回填了旧值,binlog 删除仍可纠正(前提是删除晚于回填;若删除先于回填,仍需短 TTL 兜底——所以实践中 binlog 删除常配较短 TTL 或版本号校验)。

  • 版本号方案为何能根治回填旧值? 缓存 value 带单调递增版本/时间戳。回填前比对:只有 DB 版本 ≥ 缓存版本才允许写入。旧读请求的回填因版本更小被拒绝。这从「值是否过期」升级为「值是否够新」,是更根本的解法。

  • 元理论对应: 原理 4「时间差、多副本视图与一致性窗口」(题群见《核心考点与元理论》§4.7)

【选型判断树】

text
先更库再删缓存仍出现旧值回填:
├─ 不一致可容忍(秒级、低概率)
│   └─ 现有 Cache Aside + 短 TTL 即可,不必上双删
├─ 需要显著降低概率
│   └─ 延迟双删(Δt > 一次回源耗时)
│       └─ 仍要重试保障第二次删除成功
├─ 需要可靠纠正
│   └─ Canal binlog 异步删(以 DB 为真相源)
└─ 需要从机制上防旧值覆盖
    └─ 缓存带版本号/时间戳,回填时校验

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:40定性「这是 Cache Aside 下的并发回填竞态:旧读回填发生在更新之后」
0:40–1:40竞态时序画三步:读旧 DB → 写完成删缓存 → 读回填旧值
1:40–2:50延迟双删更库、删、延迟、再删;Δt 怎么定;为什么只是概率方案
2:50–4:00更可靠方案binlog 异步删;版本号校验;短 TTL 兜底
4:00–5:00缺陷与选型双删延迟难定、删除会失败;强一致数据干脆不走缓存

【关键数字】

参数经验值说明
延迟双删 Δt500ms – 1s(常见)必须 > 一次回源+回填耗时
回源耗时量级5–50ms(缓存 miss 查 DB)Δt 的下限依据
binlog 删除延迟10ms – 1s仍建议配短 TTL
版本号DB 版本列 / 更新时间戳回填前比对
兜底 TTL(延迟双删的自愈窗口)1–5 分钟覆盖「读线程已拿到旧值」的最坏延迟

【追问链】(三层)

L1|「延迟双删的延迟时间怎么定?」 → 应大于一次「读 DB + 写缓存」的最大耗时,并留余量。线上常见 500ms–1s。更稳妥的是用 binlog,不依赖猜时长。

L2|「延迟期间用户读到什么?」 → 多数时候读到旧值(缓存里的或刚回填的),属于可容忍窗口。若价格等场景不可接受,关键读应直连 DB 或带版本号。

L3|「Canal 方案和双删能叠加吗?还有必要双删吗?」 → 可叠加,常见做法是:应用层先删一次(快速收敛)+ binlog 删除(可靠兜底)。若只保留一种,优先 binlog——它不依赖应用时序,且覆盖进程崩溃场景。双删在 binlog 齐备后往往可退役。

【评分标准】

档位答案特征
60 分知道延迟双删是删两次,中间隔一段时间
80 分能画出旧值回填的竞态时序,说清 Δt 依据,并指出概率性缺陷
95 分区分「回填竞态」与「删除失败」;给出 binlog/版本号等更根本方案;说清何时可不用双删

【关联题】

【自测】

  1. 延迟双删的第二次删除,必须发生在什么之后、什么之前? 参考答案:必须发生在「可能的旧值回填」之后;理想上在业务可接受的不一致窗口结束之前。所以 Δt 要大于回填耗时。
  2. 为什么版本号方案比延迟双删更根本? 参考答案:双删靠「多删一次碰运气」,版本号靠「旧值根本写不进去」——从概率纠正变为机制拒绝。
  3. 判断对错:上了延迟双删就再也不用担心缓存不一致。 参考答案:错。极端并发、删除失败、Δt 设短都会导致仍不一致,还需要补偿与 TTL。

29. 缓存读写有四种经典策略,怎么给业务选?(缓存策略选型) ​

【考察内容】缓存策略全景

【题目】架构评审会上,有人提到缓存读写有 Cache Aside、Read Through、Write Through、Write Behind 四种策略,团队讨论该用哪种。请讲清四种策略的机制、各自适合什么业务(比如强一致要求 vs 写多读少),以及你们为什么最终选了某一种。

【参考答案】缓存读写策略选型,按一致性×读写形态:

  1. 四种经典策略:① 旁路缓存(Cache Aside):读 miss 查库回填;写先更库再删缓存——最常用,实现简单;② 读写穿透(Read Through/Write Through——两个策略常并成一类讲,下表拆成两行):应用只和缓存交互,缓存内部同步/异步加载与回写 DB;③ 写回(Write Behind):只写缓存,异步批量落库,性能最好,丢失风险高;④ 只读缓存/全量预加载:配置与字典类。
  2. 容量估算示例(步进):读 5 万、写 500 的商品业务 → Cache Aside + 命中率 95%;写密集日志类不适合“每次写都更缓存”;写回只适合可丢、可最终一致的数据(浏览计数、点赞数、行为日志);购物车、库存这类丢了要出事故的不适用。
  3. 选型判断:能否接受短暂不一致?接受 → 删缓存+TTL;不能接受 → 不缓存权威字段或强一致组件;读极大写极小 → 缓存优先;写极大 → 削峰异步,缓存少碰。
  4. 失败与降级:任一策略都要有缓存故障时直连 DB 的降级开关;删除/回填失败重试与对账。
  5. 口径:没有银弹;互联网业务默认 Cache Aside + 删除失败重试 + TTL,再按特殊场景换型。选它的三条理由:① 没有强缓存中间件可以做 Through/Behind 的落地;② 数据不可接受静默丢失,故禁 Write Behind;③ 读多写少,删缓存的成本最低、一致性策略还能在代码层精确控制。

【原理溯源】

  • 四种策略的本质差异在「谁负责回源 / 谁负责落库 / 同步还是异步」:

    策略应用读谁应用写谁谁查 DB谁写 DB时机
    Cache Aside缓存(miss 则应用查 DB)DB(再删缓存)应用应用同步
    Read Through只读缓存(写通常另配)缓存组件—同步
    Write Through可只读缓存只写缓存组件回源组件同步写 DB同步
    Write Behind只读缓存只写缓存组件组件异步批量写异步
  • 为什么 Cache Aside 最常用? 读写路径都在应用手里,和任意 DB/缓存组合兼容,不需要缓存中间件支持「loader」或「同步写」。代价是业务代码要自己写对「先更库再删缓存」。互联网团队更愿意用代码控制换取灵活性。

  • 为什么 Write Through 写延迟高? 每次写都要「写缓存 + 同步写 DB」串行完成才能返回,写 RT 被 DB 拖住。它换来的是「写完成即持久化」,适合写不多但不能丢的配置类数据。

  • 为什么 Write Behind 最快也最危险? 写只进缓存(内存),后台批量合并刷盘——写路径没有磁盘 DB,吞吐极高。风险:缓存进程/机器宕机时未刷盘的数据永久丢失;批量写还可能乱序,需要版本控制。所以只适合「可丢、可最终一致」的数据(浏览计数、点赞数、行为日志)。

  • Read Through 和 Cache Aside 的读路径差异: 前者「缓存组件内嵌 loader,对应用透明」,后者「应用自己写 if-miss-then-load」。功能等价,差别在封装层次与侵入性。组件化平台倾向 Read Through,灵活业务倾向 Cache Aside。

  • 一致性强度排序(写路径): Write Through(同步落库)≥ Cache Aside(删后回填)> Write Behind(异步落库)。性能大致相反。

  • 元理论对应: 原理 4「时间差、多副本视图与一致性窗口」(题群见《核心考点与元理论》§4.7)

【选型判断树】

text
选缓存策略:
├─ 数据丢了是否可接受?
│   ├─ 不可接受 → 禁止 Write Behind
│   └─ 可接受(计数/日志/行为)→ Write Behind 性能最优
├─ 读写谁更频繁?
│   ├─ 读多写少,互联网默认 → Cache Aside
│   └─ 写也不少,且希望应用简单
│       ├─ 可容忍同步写延迟 → Write Through
│       └─ 不能 → 回到 Cache Aside 或 Write Behind(若可丢)
└─ 是否有统一缓存中间件要屏蔽细节?
    └─ 是 → Read/Write Through(组件内嵌 loader/落库)

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性「四种策略的差异在:谁回源、谁落库、同步还是异步」
0:30–2:30逐个讲每种一句机制 + 一句适用 + 一句代价
2:30–3:40对比维度一致性强度 vs 写性能 vs 应用侵入性
3:40–4:30落地推荐「我们默认 Cache Aside;可丢数据用 Write Behind;组件平台可上 Read Through」
4:30–5:00收尾「没有最好,只有和数据丢失容忍度、写 QPS、团队封装能力匹配的方案」

【关键数字】

参数经验值说明
Write Behind 刷盘间隔100ms–5s批量合并
Write Behind 丢失窗口等于刷盘间隔+故障瞬间评估可丢窗口
Write Through 写延迟≈ DB 写延迟(ms 级)同步落库
Cache Aside 读路径1 次 Redis(命中)或 +1 DBmiss 时
点赞/浏览计数适合 Write Behind允许少量丢失

【追问链】(三层)

L1|「Write Through 和 Write Behind 一字之差,差在哪?」 → Through 是同步写穿到 DB,写完成即持久化;Behind/Back 是异步先写缓存再批量刷 DB,更快但可能丢。

L2|「点赞数为什么适合 Write Behind?」 → ①允许最终一致和少量丢失(丢几个赞业务可接受);②写入频率极高,同步写 DB 会拖垮写路径;③后台可合并计数,减少 DB 写放大。

L3|「你们线上为什么最终选 Cache Aside?」 → 团队用的是自建 Spring + Redis,没有强缓存中间件做 Through;数据大多不可接受静默丢失(禁止 Behind);读多写少,删缓存成本低;需要在代码层精确控制一致性策略(TTL、补偿)。这三条决定了 Cache Aside 是性价比最高的默认。

【评分标准】

档位答案特征
60 分能列出四种策略名称并大致区分
80 分讲清各自机制、适用场景、一致性/性能取舍
95 分能用「谁回源/谁落库/同步异步」统一框架对比;给出本团队默认选择及三条理由;指出 Write Behind 的丢失窗口量级

【关联题】

【自测】

  1. 哪种策略写性能最高?代价是什么? 参考答案:Write Behind。代价是缓存宕机可能丢未刷盘数据,且一致性最弱。
  2. Cache Aside 和 Read Through 的读路径本质差别是什么? 参考答案:前者应用自己在 miss 时查 DB;后者缓存组件内嵌 loader 自动回源,对应用透明。
  3. 核心交易流水缓存,能用 Write Through 吗? 参考答案:可以(同步落库较安全),但更常见是交易路径直连 DB 或 Cache Aside + 强补偿,避免缓存成为写路径必经点。

30. 某业务 Redis 操作卡顿,查到一个超大 key(大 Key 治理) ​

【考察内容】大 Key 是 Redis 线上事故头号元凶之一,必考

【题目】线上偶发 Redis 命令超时、集群节点 CPU 抖动,排查发现某个业务的 key 里存了几十万条数据(大 key)。大 key 会带来哪些问题?怎么提前发现?发现了怎么处理?

【参考答案】大 Key 治理:发现→拆分/瘦身→预防:

  1. 容量估算(步进):Redis 大 Key 一般指 String >10KB(>1MB 强治理),或集合元素数 >5000 关注、>10 万严重。假设一个 hash 有 50 万元素,单次 HGETALL 可能返回数 MB,耗时几百 ms,阻塞单线程服务,引发该分片上所有 key 的 RT 抖动。节点带宽 1Gbps 时,若多个客户端同时读大 Key,瞬间打满网卡。
  2. 发现:redis-cli --bigkeys(采样)、RDB 离线分析、云厂商大 key 扫描、延迟监控与慢日志;业务侧标注可能的大对象(购物车、关注列表、全量排行榜)。
  3. 治理:① 拆分:hash 拆成 cart:{uid}:0..N 分页/分桶;② 瘦身:去掉无用字段、压缩、外置到 DB/对象存储,Redis 只存索引;③ 避免全量命令:HGETALL → HSCAN/HGET 必要字段;④ 非核心可迁到本地缓存或专用存储。
  4. 失败与降级:迁移过程双读/双写灰度;删除大 key 用 UNLINK 异步,避免 DEL 阻塞;紧急时业务降级不再读全量。
  5. 预防:容量规范(单 key 建议 <10KB)、集合元素上限、上线前评审;监控单节点带宽与慢查询。

【原理溯源】

  • 为什么大 key 在单线程 Redis 里是「核弹」? Redis 主线程同一时刻只做一件事。操作一个几百 MB 的 value:网络读入、反序列化、内存分配、命令执行、序列化、网络写出——任一步都会独占主线程,期间所有其他客户端请求排队。因果链:单线程无并行 → 大 key 操作耗时放大为全局阻塞 → 上游超时 → 重试 → 更堵。
  • 为什么删除大 key 特别危险? DEL 是同步删除:要遍历释放该 key 的全部内存/结构,几十万元素可能阻塞数百毫秒甚至秒级。UNLINK 把释放工作交给后台线程,主线程只做逻辑删除,几乎不阻塞。所以生产禁 DEL 大 key,用 UNLINK 或分批 HDEL/SPOP。
  • 为什么大 key 会拖垮主从和持久化? 主从首次全量同步要传输 RDB,其中包含大 value;部分重同步/命令传播时,对大 key 的写命令也会被放大。AOF 重写和 RDB fork 后写大对象会拉长耗时。大 key 把「单点阻塞」扩展为「复制链路和持久化链路一起抖」。
  • 为什么集群会内存倾斜? Cluster 按 key 哈希分片,一个 key 只落在一个节点。超大 key 会让该节点内存/CPU 显著高于其他节点,形成木桶短板——扩容加节点也分不走这个 key,必须拆 key。
  • 为什么 String 存大 JSON/大 Hash 不如拆开? 结构内部:整读整写是 O(value 大小);拆成小 field 或多个 key 后,业务可以只读写需要的片段,把 O(大) 变成 O(小)。这是用数据模型换性能,比加机器根本。

【选型判断树】

text
发现大 key:
├─ 是否正在造成阻塞/超时?
│   ├─ 是 → 紧急:UNLINK 或 SCAN+分批删;必要时从库顶、摘流量
│   └─ 否 → 择期治理
├─ 业务如何访问?
│   ├─ 整存整取且可拆 → 按业务维度拆多个 key
│   ├─ 只需部分字段 → 拆 Hash field / 子 key,按需 HGET
│   └─ 元素无序列表 → 拆多个小 List/ZSet,按时间或 ID 分段
├─ 是否只是序列化膨胀?
│   → 换更紧凑编码(MessagePack/Protobuf)或去掉冗余字段
└─ 预防
    → 写入前校验大小;组件层限 value;监控 bigkeys 周期任务

【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性「大 key 的危害根因是单线程:一个 key 的操作会阻塞所有人」
0:30–2:00危害链阻塞主线程 → 接口超时;带宽;主从同步;内存倾斜
2:00–3:00发现--bigkeys / --memkeys / 慢日志 / 监控
3:00–4:20处理拆分模型;UNLINK/分批删;压缩序列化
4:20–5:00预防写入限制 + 周期扫描 + 告警

【关键数字】

参数经验值说明
大 key 判定(String)> 10KB 需关注,> 1MB 强治理可按业务调
大集合判定元素数 > 5000 关注,> 10 万严重hash/zset/list
慢日志阈值> 10ms 记入,> 100ms 告警单线程下很致命
UNLINK vs DEL大对象优先 UNLINK后台线程释放
分批删除每批 100–1000 元素,间隔 10ms+防止阻塞
集群单 key 上限迁移友好:<10MB;治理红线:>100MB 必须拆迁移/删除耗时口径;上面两行的 10KB/1MB 是发现与治理口径,两套量级不同、别互相换算

【追问链】(三层)

L1|「大 key 为什么不能直接 DEL?」 → DEL 同步释放全部内存,单线程下会阻塞所有请求。用 UNLINK(异步释放)或 SCAN 分批删。

L2|「一个 50 万元素的 Hash 怎么拆?」 → 按业务维度分片:如 hash:order:{userId} 拆成 hash:order:{userId}:{shard},shard=field(订单号/元素序号)%N——按 userId 取模对同一个用户是常量,50 万元素仍全落在同一个 shard,等于没拆;或改多个小 key。原则是拆完后单次操作元素量下降两个数量级,且业务可只访问需要的 shard。

L3|「大 key 治理过程中服务不能停,你怎么做灰度?」 → ①双写:新拆分结构与旧 key 同时写;②读切流:先从新结构读,miss 回退旧 key;③观察命中与延迟;④全切后 UNLINK 旧 key;⑤回滚开关保留一段时间。禁止一把梭直接改写。

【评分标准】

档位答案特征
60 分能说出大 key 会慢、要拆
80 分讲清单线程阻塞传导,给出 bigkeys 发现与 UNLINK 删除
95 分说清主从/持久化/内存倾斜等次生危害;能设计拆分与灰度双写方案;主动提监控预防

【关联题】

【自测】

  1. 大 key 和热 key 的区别与叠加风险? 参考答案:大 key 是体积大,热 key 是访问频。既大又热时阻塞最严重——操作耗时 × 高频 = 主线程几乎常驻在该 key 上。
  2. 为什么 UNLINK 比 DEL 安全? 参考答案:UNLINK 将真正的内存释放交给后台线程,主线程快速返回;DEL 同步释放,会阻塞。
  3. 用 String 存一个 2MB 的用户对象 JSON,每次改一个字段整读整写,有什么问题? 参考答案:读写放大、网络占用大、阻塞时间长。应拆 Hash 按字段读写,或只存变化部分。

31. 点赞/排行榜/购物车/关注/未读数,各用什么结构存储(数据结构选型) ​

【考察内容】数据结构选型是 Redis 场景题最高频题型

【题目】五个需求摆在你面前:①记录用户是否点过赞(亿级用户);②游戏积分实时排行榜;③购物车(用户→多商品→数量);④用户的关注列表;⑤聊天未读数(要求原子 +1)。分别用 Redis 的哪种数据结构实现?为什么?

【参考答案】数据结构选型按“读写模式 + 查询维度”:

  1. 常见业务对位:① 点赞/计数:String + INCR 或 hash 分桶;② 排行榜:ZSET(score=积分);③ 购物车:hash(field=商品,value=数量);④ 关注/粉丝:set(差集、共同关注)或 zset 存关注时间;⑤ 未读数:hash 或多个 counter;⑥ 社交 feed 点赞名单量大时用 bitmap/布隆或外置。
  2. 容量估算(步进):假设 1 亿用户未读,每用户 5 个会话 key,每个 hash 100B → 约 50GB,需集群与拆分;ZSET 百万元素单 key 内存可达数十 MB,属于大 key,要分榜/分页/裁剪。写热点:明星点赞 INCR 可达数万 QPS,需分桶聚合(like:{id}:{shard} 再求和)。
  3. 选型原则:要范围排名 → ZSET;要集合运算 → SET;要对象字段 → HASH;要极简计数 → STRING;要存在性 → BITMAP/Bloom。
  4. 失败与降级:热 counter 打满节点 → 本地聚合+定时写回;ZSET 更新延迟可接受最终一致;Redis 挂 → 计数以 DB 对账修复。
  5. 口径:先说查询形态,再选结构;结构选错会导致只能全量扫描或改业务。

【原理溯源】

  • 选型的核心不是「功能能实现」,而是「哪个结构的命令原生支持该操作且原子」:

    需求核心操作匹配结构关键命令为什么
    点赞状态判存在/防重String/ BitMap/ SetSETNX / SETBIT / SADD去重 + O(1) 判定
    排行榜按分排序取 TopNZSetZINCRBY / ZREVRANGE底层跳表按 score 有序
    购物车用户→多商品→数量HashHINCRBY / HGETALLfield=SKU,value=数量
    关注列表集合运算SetSINTER / SUNION共同关注=交集
    未读数原子累加Hash/StringHINCRBY / INCR单线程原子
  • 为什么排行榜必须是 ZSet 而不是 List? List 只有下标有序,没有「按分数排序并更新某人分数」的原生操作;若用 List 自己排序,每次分数变化都要全量重排,O(N) 且非原子。ZSet 用「哈希表(member→score)+ 跳表(按 score 排序)」,更新 O(logN),取 TopM 为 O(logN+M)。

  • 为什么购物车用 Hash 而不是 String 存 JSON? String JSON 每次改数量都要 GET 整包 → 反序列化 → 改 → 序列化 → SET,无法原子改单个商品,且并发覆盖。Hash 的 HINCRBY 直接对某 field 原子 ±,HGETALL 一次取全车,结构与业务模型一一对应。

  • 为什么关注关系用 Set? 关注/粉丝天然是集合;「共同关注」「我关注的人也关注了谁」需要交并差。Set 的 SINTER/SUNION/SDIFF 原生支持,且结果可以是新 Set 或直接返回列表。

  • 点赞为什么有时用 BitMap? 亿级用户的「是否点过赞」是 1-bit 信息。BitMap 把用户 ID 映射到位,1 亿用户只需约 12MB。代价是只能回答「点没点」,不能枚举「谁点了」(要遍历)。按查询形态选:只判存在 → BitMap/String;要列表/交集 → Set。

  • Redis 单线程为什么让 INCR/HINCRBY 天然原子? 无并发交错,命令队列串行执行。所以计数器不需要应用层加锁——这是选 Redis 做限流、未读数、秒杀计数的底层原因。

【选型判断树】

text
需求落结构:
├─ 只要「是否」?(点赞、签到、在线)
│   ├─ 用户基数大、只判存在 → BitMap / String+SETNX
│   └─ 还要列表或交集 → Set
├─ 要「排序 TopN」?(积分榜、销量榜)
│   → ZSet(score=指标)
├─ 要「一个主体下多个字段」?(购物车、用户属性)
│   → Hash
├─ 要「集合运算」?(关注、共同好友、标签)
│   → Set(有序取范围才用 ZSet)
├─ 要「原子计数」?(未读、限流、库存预扣)
│   → INCR / HINCRBY
├─ 要「先进先出队列」?(异步任务、可靠消费)
│   → List(LPUSH + BRPOP / BRPOPLPUSH 可靠队列)
└─ 要「定长最近列表」?(最近浏览、用户动态)
    → List(LPUSH + LTRIM 截断,LRANGE 按下标读)

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性「选型标准是:哪个结构的原生命令能原子完成核心操作」
0:30–3:00五需求逐个对应点赞→String/ Set/ BitMap;榜→ZSet;车→Hash;关注→Set;未读→HINCRBY
3:00–4:00讲两个「为什么」榜为何不能用 List;车为何不能用 String JSON
4:00–4:40底层一句ZSet=哈希+跳表;单线程保证 INCR 原子
4:40–5:00收尾「结构匹配业务模型,原子性来自原生命令+单线程」

【关键数字】

参数经验值说明
BitMap 1 亿用户约 12MB1 bit/人
ZSet Top100O(logN+100)毫秒内
ZSet member 内存约 20–80 字节亿级约几 GB
Hash 小对象< 100 field 较优过大变大 key
Set 交集 100 万×100 万可能秒级注意复杂度
INCR 吞吐单实例 10 万+/s简单命令

【追问链】(三层)

L1|「点赞亿级用户,为什么有人用 BitMap?」 → 每用户 1 bit,1 亿仅约 12MB,且 SETBIT/GETBIT O(1)。适合只判「点没点」;若要「谁点赞了」则应使用 Set 或倒排到业务库。

L2|「ZSet 底层是什么?为什么更新和取 TopN 都快?」 → 哈希表(O(1) 找 member→score)+ 跳表(按 score 有序,插入删除查找 O(logN))。ZINCRBY 改 score 后调整跳表;ZREVRANGE 从最高分向下取 M 个,复杂度 O(logN+M)。

L3|「关注列表用 Set,粉丝也用 Set,共同关注怎么算?慢吗?」 → SINTER follow:me follow:he。复杂度与较小集合大小相关。若一方是大 V(粉丝百万级)会慢——应对:限制对超大集合做交集、只算前 N、或离线预计算共同关注。

【评分标准】

档位答案特征
60 分五个需求能对上五个常见结构
80 分每个能说出关键命令和「为什么是它不是别的」
95 分讲清 ZSet 双结构、BitMap 容量优势、String JSON 的并发覆盖问题;能提醒大集合交集的性能陷阱

【关联题】

【自测】

  1. 「共同关注」用什么命令?两个结构是什么? 参考答案:SINTER,两边都是 Set(我关注的 / 他关注的)。
  2. 为什么用 List 做实时积分榜不合适? 参考答案:List 无法按 score 原子更新某人名次;要全量重排,非原子且 O(N)。
  3. 购物车为什么推荐 Hash 而不是 String JSON? 参考答案:Hash 的 HINCRBY 可原子改单个商品数量,避免整包读写与并发覆盖,结构与「用户→多 SKU→数量」同构。

32. 面试官:Redis 单线程,凭什么支撑 10 万+ QPS?(Redis 高性能原理) ​

【考察内容】Redis 为什么快是必考送分题,但要答出层次

【题目】有人质疑:Redis 是单线程模型,怎么可能扛得住 10 万+ QPS?请从 IO 模型、数据结构、内存操作、上下文切换等角度解释 Redis 为什么快,并说明单线程模型带来的限制。

【参考答案】Redis 单线程却能 10 万+ QPS,本质是内存 + IO 多路复用 + 协议简单:

  1. 容量估算(步进):单次简单命令服务端处理约 1–10 微秒;若 10 万 QPS,则服务端占用 ≈ 10万×5µs = 0.5 CPU 秒/秒,远低于 1 核饱和,说明单线程不是瓶颈。真正限制通常是网卡与客户端 RTT:1KB×10万 = 100MB/s ≈ 800Mbps,已经贴近千兆网卡上限(1Gbps≈125MB/s),要跑满这个量级需万兆网卡(1.25GB/s)或聚合带宽——带宽是比 CPU 更早撞到的墙。
  2. 高性能原因:① 纯内存操作,无磁盘等待(持久化另有子进程);② 事件驱动 epoll/kqueue,单线程避免锁与上下文切换;③ 命令大多 O(1)/O(logN),高效数据结构;④ 协议简单、流水线 pipeline 减少 RTT;⑤ 单线程保证命令原子性,业务无需锁竞争心智负担。
  3. 何时不够:大 key、复杂命令、fork 导致 COW 内存压力、网卡打满;此时需要集群分片、从库读、或用更高版本多线程 IO(Redis 6+ IO 线程处理网络,命令执行仍单线程)。
  4. 失败与降级:慢命令阻塞 → 清理慢查询/禁用危险命令;带宽打满 → 压缩、拆分、就近本地缓存。
  5. 口径:不是“单线程快”,而是瓶颈不在 CPU 串行,在内存与 IO 调度方式;单线程还带来简单可预测的性能模型。

【原理溯源】

  • 为什么“单线程”反而快? 不是“线程少所以快”,而是单线程消除了两类开销:① 多线程保护共享数据必须加锁,锁竞争本身就是成本;② 线程切换要保存/恢复上下文(寄存器、栈),高并发下切换开销可能超过业务本身。Redis 的操作都是内存级(纳秒到微秒),一旦引入锁和切换,开销占比会急剧上升——所以单线程在这里是收益大于代价的选择。
  • 为什么 epoll 能扛住大量连接? 对比三代 IO 模型:
    • select:每次调用要把全部 fd 集合从用户态拷贝到内核态,返回后还要线性遍历找就绪的,复杂度 O(n),且 fd 上限 1024;
    • poll:去掉了 fd 数量上限,但仍需线性遍历,O(n);
    • epoll:用红黑树管理 fd(增删 O(log n)),用就绪链表存放已就绪事件,epoll_wait 直接返回就绪链表,取就绪事件 O(1),且不需要每次拷贝全量 fd。
    • 结论:连接数越多,epoll 相对 select/poll 的优势越明显——这正是 Redis 单线程能同时处理海量连接的原因。
  • 为什么 6.0 只让网络 IO 多线程,不让命令执行多线程? 因为瓶颈不在命令执行而在网络读写(尤其大 value 的解析与回包)。让命令执行多线程会引入锁与执行顺序问题——Redis 很多命令(INCR、SETNX)的语义依赖“原子执行”,多线程要么加锁(抵消收益),要么改变语义(破坏兼容性)。所以只把“读请求 + 写响应”并行化。
  • 单线程带来的真实限制: ① 任一慢命令(KEYS *、大 key 的 HGETALL、SORT)会阻塞全部请求;② 无法利用多核做命令执行,单机 QPS 有天花板;③ 因此必须靠集群分片横向扩展,而不是单机加核。

【选型判断树】

text
需要更高吞吐时怎么办?先定位瓶颈:

├─ 慢命令 / 大 key 阻塞      → 治理慢命令、拆大 key(第 30 题)
├─ 热 key 打满单节点         → 本地缓存 + 读写分离 + key 打散(第 13 题)
├─ 网络带宽打满              → 6.0+ 开 IO 多线程、pipeline、压缩 value
├─ 单核 CPU 打满             → 集群分片(第 35 题)
└─ 内存不足触发淘汰          → 治理内存 + 扩容(第 48 题)

通用原则:Redis 靠“分片”扩展,不靠“单机加核”。

【口述骨架】(5 分钟;框架 A,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“Redis 快不是单点原因,我会从内存、IO 模型、单线程机制三个层面讲,并纠正一个常见误解”
0:30–2:30三个层面内存(无磁盘 IO)→ 单线程(免锁免切换)→ epoll(IO 多路复用);数据结构作为补充
2:30–3:30纠正误解主动说“严格说 Redis 不是完全单线程,6.0+ 网络 IO 是多线程的”
3:30–4:30讲限制慢命令阻塞、单核天花板、必须靠分片扩展
4:30–5:00收尾“一句话:Redis 的快来自‘内存 + 免锁 + epoll’三件套,它的扩展方式是分片而不是加核”

【关键数字】

参数经验值说明
单机 QPS约 10 万简单命令;pipeline 可更高
epoll 就绪事件获取O(1)vs select/poll 的 O(n)
fd 上限select 1024;epoll 无硬上限epoll 的核心优势之一
慢命令判定slowlog-log-slower-than 默认 10ms 记入慢日志;生产常加严到 >1ms 即告警单线程下会阻塞所有请求
大 key 判定String > 10KB;集合元素 > 5000需拆分
IO 多线程默认关闭,需显式配置 io-threads6.0+ 特性
单机内存建议≤ 16GB(RDB fork 时可能翻倍)与持久化开销相关

【追问链】(三层)

L1|“那为什么不用多线程执行命令,不是能利用多核吗?” → 因为会引入锁与执行顺序问题。Redis 很多命令的语义依赖“原子执行”,多线程要么加锁(抵消收益),要么改变语义(破坏兼容性)。而实际瓶颈多在网络 IO,所以 6.0 只并行化了 IO。

L2|“epoll 和 select 到底差在哪?为什么 epoll 更快?” → 三点:① select 每次要把全量 fd 从用户态拷到内核态,epoll 只在注册时拷贝一次;② select 返回后要线性遍历 O(n) 找就绪的,epoll 直接返回就绪链表 O(1);③ select 有 1024 fd 上限,epoll 没有。所以连接数越大,epoll 优势越明显。

L3|“如果线上真出现一个慢命令把 Redis 卡住了,你怎么处理?” → 先止血:SLOWLOG 定位慢命令,必要时 CLIENT KILL 掉来源;用 redis-cli --bigkeys 找大 key;--hotkeys 底层是 OBJECT FREQ,只有 maxmemory-policy 为 LFU 时可用(默认 noeviction/allkeys-lru 下直接报错),且会全库扫描——只在低峰或从节点用;常态发现热 key 仍靠 MONITOR 抽样、代理层统计与云厂商热 key 分析。再根治:拆大 key、把 KEYS 换成 SCAN、大 hash 改成分批取、对热 key 加本地缓存。最后加防线:客户端/代理层做命令白名单与超时熔断。

【评分标准】

档位答案特征
60 分能说出“内存 + 单线程 + IO 多路复用”三点
80 分能解释单线程为什么反而快(免锁、免切换),并说出 6.0 IO 多线程
95 分能讲清 epoll vs select 的三点差异;能说出单线程的真实限制与应对(慢命令阻塞、靠分片扩展);能给出慢命令的排查处置路径

【关联题】

  • 同一知识簇(Redis 性能与治理): 第 30 题(大 Key 治理)、第 13 题(热 Key 治理)、第 48 题(内存治理)、第 33 题(持久化选型)、第 35 题(集群方案)
  • 对比参照: 第 86 题(Kafka 高吞吐原理)——同属“单机为什么能扛”类问题,可对比记忆
  • 上游基础: 第 31 题(数据结构选型)

【自测】

  1. 判断对错:Redis 是单线程的,所以不可能利用多核 CPU。 参考答案: 错。Redis 6.0 起支持多线程网络 IO,但 io-threads 默认为 1(不显式改就还是单线程模型),且命令执行始终单线程;此外靠部署多实例 / 集群同样能吃满多核。
  2. KEYS * 为什么在生产环境禁用?替代命令是什么? 参考答案: 它遍历全部 key 且是 O(n) 阻塞操作,单线程下会卡住所有请求;替代用 SCAN(游标分批、非阻塞)。
  3. 简述 epoll 比 select 快的两个原因。 参考答案: ① fd 只在注册时拷贝一次,select 每次调用都要全量拷贝;② 就绪事件通过就绪链表直接返回 O(1),select 需线性遍历 O(n)。

33. Redis 重启后数据全丢了,老板要求持久化(RDB/AOF 选型) ​

【考察内容】持久化机制与数据安全权衡

【题目】某次机器宕机重启后,Redis 里的数据丢了不少(配置里只开了默认持久化),业务受损。你负责给 Redis 配置持久化:RDB 和 AOF 各是什么机制?怎么搭配才能“尽量少丢数据”又“不拖慢性能”?

【参考答案】RDB/AOF 选型按“丢数据容忍 × 性能”:

  1. 容量估算(步进):假设内存 20GB 数据,RDB fork 写一份约 20GB 快照,磁盘 IO 与 COW 峰值内存要预留;AOF 体积可达数据量的 1–3 倍(与写入量相关),写 QPS 5 万 × 每条命令约 200B = 10MB/s 级追加写(读写混合时要把 5 万换成写占比),按 86400 s/天算日增约 864GB(未计 rewrite 前的膨胀),必须靠 auto-aof-rewrite-percentage 及时重写,否则磁盘会先被打满。
  2. RDB:定时快照,恢复快、体积小,两次快照之间数据会丢;适合备份与容灾、可丢几分钟数据的缓存场景。
  3. AOF:追加写命令;everysec 每秒刷盘约丢 1 秒;always 最安全性能最差;no 依赖 OS。适合把 Redis 当存储、可丢窗口要小的场景。
  4. 推荐组合:RDB + AOF everysec 一起开;重要业务再配合主从/集群与复制缓冲。
  5. 失败与降级:AOF rewrite 期间压力大 → 低峰执行;磁盘满 → 告警并暂停写;恢复演练要定期做(很多团队备份了却没演练过);纯缓存数据也可接受清空,直接从 DB 重建(预热限流)。
  6. 口径:先问“丢多少秒可以接受、恢复要多快”,再选机制。

【原理溯源】

  • RDB 的因果链: 触发条件满足(N 秒内 M 次修改)→ fork 出子进程 → 子进程利用写时复制(COW)读内存生成快照 → 父进程继续服务。丢失窗口 = 两次快照间隔;fork 瞬间若内存大,可能造成短暂阻塞与内存峰值(COW 下写会复制页)。
  • AOF 的因果链: 每个写命令追加到 AOF 缓冲 → 按 appendfsync 策略刷盘:
    • always:每条命令 fsync,最多丢 0,性能最差;
    • everysec:每秒 fsync,最多丢 1 秒,工程默认;
    • no:交给 OS,丢多少不可控。 文件会无限变大,所以需要 AOF 重写:后台根据当前内存数据生成最小命令集,替换旧文件。
  • 为什么「双开」是主流? 两者互补:AOF 丢得少但恢复慢、文件大;RDB 恢复快、适合备份与复制,但丢得多。重启时优先加载 AOF(更全),RDB 作为冷备与快速拉起手段。
  • 为什么纯缓存可以关持久化? 缓存数据可从 DB 重建,持久化的收益是「重启后不用回源」,代价是磁盘 IO、fork 开销、磁盘占用。当重建成本低时,关掉更划算。
  • 为什么大内存机器要警惕 fork? fork 要复制页表,内存越大越慢;COW 下父进程大量写会触发页复制,内存可能近似翻倍。经验上单实例内存建议 ≤16GB,或用 4.0+ 的混合持久化降低重写成本。

【选型判断树】

text
数据丢了能不能接受?
├─ 能(纯缓存,可回源重建)
│   └─ 关闭持久化 或 仅 RDB 做冷备
├─ 不能(计数/会话/队列/业务态)
│   └─ AOF everysec(默认)+ RDB 双开
│       ├─ 一秒都不能丢 → AOF always(性能换安全,慎用)
│       └─ 文件太大/恢复慢 → 开混合持久化 + 定期 rewrite
└─ 高可用要求高
    └─ 持久化只是底线,还要主从/集群(第 35 题)

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性「RDB 快照、AOF 日志;一个丢得多恢复快,一个丢得少恢复慢」
0:30–2:20两种机制RDB:fork+COW;AOF:追加+fsync 策略+重写
2:20–3:30怎么选双开;纯缓存可关;敏感数据 everysec
3:30–4:20代价fork 阻塞与内存翻倍;AOF 文件与重放耗时
4:20–5:00收尾「持久化解决的是单机掉电,高可用解决的是机器没了——两件事都要」

【关键数字】

参数经验值说明
默认 RDB 规则900s 内 1 次 / 300s 内 10 次 / 60s 内 10000 次(6.2 起、7.x 有变,逐版对照见表下「RDB 默认值沿革」)save 配置,须按所部署版本核对
AOF everysec 丢失≤ 1 秒写入工程默认
AOF always接近 0 丢失吞吐可能降一个量级
单实例内存建议≤ 16GB控制 fork/COW
恢复速度RDB ≪ AOF(AOF 要重放)混合持久化可折中
AOF rewrite文件增长 100% 可触发auto-aof-rewrite-percentage
  • RDB 默认值沿革: 900s 内 1 次 / 300s 内 10 次 / 60s 内 10000 次(上游 redis.conf 各版本默认值不同,报数前先确认版本:2.8 / 3.0 / 6.0 是 save 900 1 / 300 10 / 60 10000;6.2 起改成 save 3600 1 / 300 100 / 60 10000;7.0 起合并写成一行 save 3600 1 300 100 60 10000)

【追问链】(三层)

L1|「AOF 一定比 RDB 好吗?」 → 不是。AOF 丢得少,但文件大、恢复慢、重写有开销。备份和快速拉起更依赖 RDB。生产通常双开。

L2|「AOF 重写会阻塞主线程吗?」 → 重写本身在子进程/后台执行;但 fork 瞬间可能短暂阻塞,且重写期间写压力会导致 COW 内存上升。应避开高峰执行 BGREWRITEAOF。

L3|「从库还要开持久化吗?」 → 建议开。主库全挂且从库被提升为主时,从库若有持久化可避免「提升后又重启丢数据」。纯缓存从库可关闭以省资源。

【评分标准】

档位答案特征
60 分能说出 RDB 快照、AOF 日志,建议都开
80 分讲清丢失窗口、fsync 策略、恢复速度差异,按数据敏感度选型
95 分说清 fork/COW 成本;AOF 重写原理;纯缓存可关持久化;持久化与高可用的边界

【关联题】

【自测】

  1. AOF everysec 最多丢多少数据? 参考答案:约 1 秒内的写命令。
  2. 为什么说 RDB 恢复比 AOF 快? 参考答案:RDB 是内存二进制镜像直接加载;AOF 要重放全部写命令。
  3. 判断对错:持久化配置好了,单点 Redis 就安全了。 参考答案:错。持久化只解决进程/机器重启后数据找回,不解决高可用与自动切换,还需要主从/哨兵/集群。

34. 内存 8G 用满、key 过期却不释放,怎么办(过期与淘汰策略) ​

【考察内容】内存管理机制

【题目】线上 Redis 内存 8G 已经用满,业务反映写入开始报错;同时有人疑惑“有些 key 明明设了过期时间,内存却一直不降”。请讲清 Redis 的过期删除策略与内存淘汰策略的区别,以及你们该怎么配置。

【参考答案】内存用满 key 不释放:分清过期删除与内存淘汰:

  1. 容量估算(步进):实例 8G,已用 90%(7.2G)。假设写入 50MB/s、过期速率若 < 写入,则内存持续上涨。Redis 只在“过期抽样 + 内存满时淘汰”场景处理,不会立刻释放每一个到期 key。
  2. 过期删除策略:惰性删除(访问时删)+ 定期删除(每 100ms 抽样一批过期 key,周期由配置项 hz(默认 10 → 每 100ms 一轮)控制;server.hz 只是 C 里的变量名,CONFIG GET server.hz 查不到)。因此长期不访问的过期 key 可能仍占内存,直到定期删除抽到或内存满触发淘汰。
  3. 内存淘汰(maxmemory-policy):达到 maxmemory 时按策略淘汰:共 8 种:noeviction(默认,写命令报错)、allkeys-lru、allkeys-lfu、allkeys-random、volatile-lru、volatile-lfu、volatile-random、volatile-ttl(后四种只作用于设了 TTL 的 key);纯缓存实例用 allkeys-*(volatile-* 在没设 TTL 时会退化成报错)。生产缓存常用 allkeys-lru 或 allkeys-lfu;把 Redis 当存储则 noeviction + 告警扩容。
  4. 治理:确认是否真的该过期但没设 TTL;大 key/碎片(内存碎片率);MEMORY USAGE 分析;必要时 MEMORY PURGE/重启整理碎片;集群扩容。
  5. 失败与降级:noeviction 导致写失败 → 业务报错,要监控;淘汰过猛命中率下降 → 回源 DB,需限流兜底。

【原理溯源】

  • 两个机制回答的是不同问题,必须分开:
    • 过期删除:「TTL 到了的 key 什么时候真正从内存消失?」
    • 内存淘汰:「内存满了,该踢掉哪些(可能还没过期的)key?」
  • 为什么不能「到期立刻删」? 若每个 key 精确定时删除,需要海量定时器,CPU 不可接受。Redis 用:
    • 惰性删除:读写时检查,过期则删。零额外扫描成本,但从不被访问的过期 key 会一直占内存——这正是「设了过期内存却不降」的主因。
    • 定期删除:每 100ms 抽样一部分带 TTL 的 key,删掉已过期的;若过期比例仍高则继续。折中 CPU 与内存回收。
  • 为什么默认 noeviction 在缓存场景很危险? 内存满后写命令直接报错,而不是踢掉冷数据。业务若没处理错误会失败。缓存场景应显式改为 allkeys-lru 或 allkeys-lfu,用「牺牲缓存」换「写入可用」。
  • LRU vs LFU 的本质差异: LRU 看「最近是否用过」——一次性扫描大量冷 key 可能把热点踢掉;LFU 看「使用频率」——刚被扫过一次的 key 不会因「最近」而留下。热点保护场景 LFU 更稳,但实现更重(需要计数器)。
  • volatile- 系列为什么常不够用?* 它们只在「设置了 TTL 的 key」里淘汰。若缓存里混有大量无 TTL key(错误配置),volatile 系列可能无 key 可踢,仍然写失败。所以纯缓存集群更常用 allkeys-*。

【选型判断树】

text
内存满了 / 写报错:
├─ 先分清是「过期没删」还是「数据真的多」
│   ├─ 大量过期 key 残留 → 确认定期删除正常;必要时 UNLINK 清理
│   └─ 有效数据超容量 → 走淘汰或扩容
├─ 业务类型?
│   ├─ 纯缓存(可丢)→ allkeys-lru / allkeys-lfu
│   ├─ 业务数据(不可丢)→ noeviction + 扩容/集群;禁止 allkeys
│   └─ 混有 TTL 与无 TTL → 优先 allkeys,避免 volatile 无候选
└─ 热点怕被清 → allkeys-lfu;或本地缓存 + Redis 双层

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:40定性「过期删除管 TTL 到期;内存淘汰管满了踢谁——两套机制」
0:40–2:00过期删除惰性 + 定期;解释「设了过期内存不降」
2:00–3:20淘汰策略noeviction / LRU / LFU / volatile-*;缓存默认 allkeys-lru/lfu
3:20–4:20选型与坑不可丢数据不能开 allkeys;LFU 保护热点
4:20–5:00收尾「先确认过期回收,再按可丢性配置淘汰,最后才是扩容」

【关键数字】

参数经验值说明
定期删除周期每秒 hz(默认 10)次 → 约每 100ms 一轮;每轮最多占用约 25% 的时间片(是单轮时长上限,不是常态 CPU 占用)抽样,非全量;配置项名是 hz,不是 server.hz(见第 2 条)
惰性删除仅访问时冷 key 会残留
缓存淘汰建议allkeys-lru 或 allkeys-lfu业务缓存
maxmemory物理内存的 50%–75%留复制/fork 缓冲
内存告警80% 预警 / 90% 告警与第 48 题同口径
OOM 行为noeviction 时写失败、读仍可错误要处理

【追问链】(三层)

L1|「为什么用了惰性删除,内存还是不降?」 → 从不被访问的过期 key 不会被惰性删除碰到;若定期删除抽样也未覆盖到,会长期占内存。可主动 SCAN 清理,或确认 maxmemory-policy 与定期删除正常。

L2|「LRU 和 LFU 怎么选?」 → 访问模式接近「最近使用即可能再用」用 LRU;有稳定热点、怕被一次性冷扫描冲掉用 LFU。Redis 的 LRU/LFU 都是近似算法(采样),不是全局精确。

L3|「淘汰会不会把正在用的热 key 踢掉?」 → 可能,尤其 LRU + 采样。防护:①lfu;②本地缓存扛热点;③关键 key 不要依赖 Redis 单层;④监控命中率,命中率骤降要排查是否被误淘汰。

【评分标准】

档位答案特征
60 分能说出有过期删除和内存淘汰两种东西
80 分讲清惰性+定期、主流淘汰策略,并能按缓存/业务数据选型
95 分解释「过期不释放」根因;对比 LRU/LFU;指出 noeviction 对缓存的危害与 maxmemory 留白

【关联题】

【自测】

  1. 「设了 TTL 但内存不降」最常见原因是什么? 参考答案:惰性删除只在访问时触发;冷 key 残留。还有可能是数据量本身超过容量。
  2. 纯缓存 Redis 应该用什么淘汰策略? 参考答案:allkeys-lru 或 allkeys-lfu,避免 noeviction 导致写失败。
  3. noeviction 下内存满会怎样? 参考答案:写命令返回 OOM 错误,读仍可服务。必须让业务处理该错误或提前扩容。

35. Redis 单节点宕机业务就断,要搞高可用(集群方案选型) ​

【考察内容】Redis 高可用体系是必考大题

【题目】现在 Redis 是单节点,上次宕机所有依赖缓存的业务全部不可用。团队想上高可用方案,听到三个名词:主从复制、哨兵、Cluster。它们各自解决什么问题?数据量大(几百 G)和只求不丢服务两种场景分别怎么选?

【参考答案】Redis 高可用按业务容忍度选型:

  1. 容量估算(步进):单节点约 10 万 QPS、内存假设 25GB 可用。先分清 15 万是读还是写:读超单机可以先加从库扩读(读写分离就能扛,不必分片);只有写超单机、或数据量装不下(如 40GB 且仍增长),才是必须分片集群。若只要求“挂了能切”,数据量不大,主从+哨兵即可。故障切换时间目标秒级~30 秒内。
  2. 方案对比:① 主从复制:读扩展、数据备份,但主挂不能自动切;② 哨兵(Sentinel):自动选主切换,适合中小规模一主多从;③ Cluster 集群:数据分片 + 高可用(主从分片),水平扩展容量与吞吐,是大厂默认。
  3. 客户端:Cluster 感知客户端(Jedis/Lettuce/Redisson)处理 MOVED/ASK;业务要接受跨 slot 不支持多 key 事务(或用 hash tag)。
  4. 失败与降级:主从切换瞬间可能短时不可用 → 客户端超时重试+本地缓存兜底;集群节点挂 → 从升主,监控 failover;脑裂/复制延迟 → min-replicas-to-write 等参数;极端时降级为本地缓存/DB 限流。
  5. 口径:先估 QPS 与数据量决定是否分片,再定切换自动化级别;预案必须演练,不是画架构图。

【原理溯源】

  • 三者是递进关系,解决的问题不同:

    方案解决什么不解决什么
    主从读扩展、数据备份主挂自动切换、写扩展、容量
    哨兵主挂自动故障转移单机内存上限、写扩展
    Cluster容量水平扩展、写扩展、高可用跨 slot 事务/多 key 原子
  • 为什么主从不能自动切换? 主从只是数据复制关系,没有「谁来判定主挂了、谁来提升从」的协调者。哨兵补上这个角色:多个哨兵独立探测,达到 quorum 后选举新主并通知客户端。

  • Cluster 为什么用 16384 个 slot? 把 key 空间均匀映射到固定数量的槽,槽再分配到节点。扩容时迁槽而不是重哈希全部 key。CRC16(key)%16384 路由,客户端或代理按槽表转发。16384 是「心跳包大小 vs 槽粒度」的工程折中。

  • 为什么 Cluster 下多 key 操作受限? 不同 key 可能在不同节点,无法在单节点事务里原子执行。解法:{hashtag} 让相关 key 落在同一 slot;或改设计避免跨 key 原子。

  • 脑裂为什么危险? 网络分区时旧主仍在收写,哨兵已把从提升为新主;分区恢复后旧主降级,其上「多写的未同步数据」被丢弃——表现为用户写成功后数据消失。防护:min-replicas-to-write + min-replicas-max-lag,副本不足或延迟过大时拒绝写。

  • 元理论对应: 原理 3「故障是分布式系统的默认输入」(题群见《核心考点与元理论》§3.7)

【选型判断树】

text
要高可用 / 扩展:
├─ 只是读多,写少,数据量单机放得下
│   └─ 主从(若还要自动切 → 哨兵)
├─ 要自动故障转移,数据量 < 单机内存上限
│   └─ 哨兵(3 节点起)
├─ 数据量大(几十 G~几百 G+)或写 QPS 单机不够
│   └─ Cluster(分片 + 每片主从)
│       └─ 需要多 key 原子 → hash tag 或改模型
└─ 不可接受脑裂丢写
    └─ min-replicas-to-write / max-lag;关键写走 DB 或强一致组件

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性「主从管读扩展,哨兵管自动切换,Cluster 管容量与写扩展」
0:30–2:30三者机制复制方向与延迟;哨兵 quorum;slot 路由
2:30–3:30场景选型不丢服务→哨兵;几百 G→Cluster
3:30–4:20坑脑裂、跨 slot、主从延迟
4:20–5:00收尾「先问数据量和写扩展,再问要不要自动切换」

【关键数字】

参数经验值说明
slot 数量16384Cluster 固定
哨兵节点≥3,奇数quorum 通常 2
故障判定down-after-milliseconds 常 5–30s太小易误切
主从延迟通常 ms 级,网络差可到秒见第 46 题
单分片内存建议 ≤ 16GB(4.0+ 混合持久化、低写压可放到 32GB)同第 33 题口径:控 fork/COW 与恢复时长
脑裂防护min-replicas-to-write=1按副本数调整

【追问链】(三层)

L1|「哨兵和 Cluster 有什么区别?能不能一起用?」 → 哨兵解决「主挂了自动换」,数据仍在单机;Cluster 解决「数据切多片+高可用」。Cluster 自带主从切换,一般不需要再套哨兵。传统哨兵适合数据量可控场景。

L2|「Cluster 下为什么不能随便 MGET 十个 key?」 → 十个 key 可能落在不同 slot/节点,无法单节点原子执行。要么 hash tag 强制同槽,要么客户端拆成多次网络往返(非原子)。

L3|「脑裂具体怎么发生,配置怎么防?」 → 分区中旧主仍可写但不可同步;哨兵已选新主。恢复后旧主数据被丢弃。防:旧主在「同步副本数不足或延迟过大」时拒绝写(min-replicas-to-write / min-replicas-max-lag)。

【评分标准】

档位答案特征
60 分能说出主从、哨兵、Cluster 三个名词
80 分三者解决的问题与选型场景清晰;说清 slot 路由
95 分讲清脑裂与 min-replicas;跨 slot 限制与 hash tag;能按「数据量 × 写扩展 × 自动切换」三维选型

【关联题】

【自测】

  1. 数据 200GB,单机放不下,该上什么? 参考答案:Cluster,按 slot 分片到多节点,并为每片配主从。
  2. 只要求「主挂了能自动切换」,数据只有 5GB,选什么? 参考答案:哨兵。数据量可控,核心诉求是自动故障转移。
  3. Cluster 里 {user:1001}:a 和 {user:1001}:b 为什么能在同一节点? 参考答案:hash tag {user:1001} 参与槽计算,相同 tag 的 key 落同一 slot。

持续学习,持续积累。