Skip to content

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


36. 多实例扣库存还是超扣了,要用分布式锁(分布式锁实现) ​

【考察内容】分布式锁是 Java 后端社招必考,考的是“正确性细节”

【题目】系统多实例部署后,用单机 synchronized 锁库存已经不生效,出现超扣。改用 Redis 实现分布式锁后,又出了新问题:锁忘了释放导致死锁、锁被别的线程误删、主从切换后锁丢失。请给出 Redis 分布式锁的正确实现,并指出所有常见坑。

【参考答案】多实例扣库存超扣,用分布式锁 + 条件扣减双保险:

  1. 容量估算(步进):库存 1000,峰值扣减 QPS 2 万,若靠应用内锁或无锁 read-modify-write,并发下超卖几乎必然。分布式锁把同一库存的并发扣减串行化,代价是吞吐上限被锁内耗时锁死:上限 = 1 ÷ 临界区耗时——临界区 1ms 只有 1000 TPS、5ms 只有 200 TPS(见第 5 题口径),要撑到 2 万 QPS 必须把锁内压到 50µs 以内。而锁内含一次 DB 条件更新(下面第 2 步)根本不可能做到 µs 级,所以同一 key 的扣减必须绕开全局串行:Redis 单线程内用 DECR/Lua 原子预扣(无显式锁、由单线程保证串行且每次 µs 级),锁只用于"防重复下单"这类低频判定,不能用来扛 2 万 QPS 的扣减主路径。
  2. 实现:① Redis SET lock:key unique NX PX 30000 获取;② 业务扣减(优先 DB 条件更新 WHERE num>0);③ finally 用 Lua 比对 unique 再 DEL,防误删他人锁;④ Redisson 提供可重入与看门狗续期。
  3. 为什么锁之外还要条件更新:锁可能失效(进程 GC、网络、时钟)、可能误实现;DB WHERE num>0 是最后权威防线,锁只是降低冲突,不是正确性的唯一来源。
  4. 失败与降级:锁获取失败 → 快速失败或排队;Redis 锁服务挂 → 降级为 DB 原子扣减+调低限流;业务超时关单要释放占用并回补。
  5. 口径:分布式锁解决“互斥窗口”,条件更新解决“最终正确”,两者都要有。

【原理溯源】

  • 为什么单机锁在多实例下失效? synchronized 只在本 JVM 进程内互斥,多个应用实例各有各的锁对象,对共享库存的临界区毫无保护。分布式锁的本质是:把「谁持有锁」这件事放到所有实例都看得见的地方(Redis/ZK/DB)。

  • 为什么加锁必须是 SET NX EX 一条命令? SETNX 不带过期 → 进程崩溃锁永存(死锁);先 SETNX 再 EXPIRE 两步 → 两步之间崩溃仍死锁。Redis 单命令原子,SET key val NX EX seconds 一次完成「抢锁+设过期」。

  • 为什么解锁要用 Lua「比对 + 删除」? 若先 GET 比对再 DEL,两步之间锁可能已过期并被别人拿到,你的 DEL 会删掉别人的锁。Lua 在 Redis 内原子执行,保证「是我的才删」。

  • 为什么 value 必须唯一? 否则无法区分「我的锁」和「别人的锁」,解锁就无法防误删。UUID/线程 ID 是身份凭证。

  • 为什么主从切换会丢锁? 主写锁成功但尚未复制到从就宕机,从被提升后没有这条锁 → 其他客户端仍能抢到。这是异步复制的固有问题。RedLock 试图用多节点多数派写缓解,但仍有争议;强正确场景更常用 ZooKeeper/etcd(基于共识日志)。

  • Redisson 相对手写的优势在哪? 把正确性细节产品化:原子加锁、Lua 解锁、看门狗续期、可重入、阻塞等待、公平锁选项。手写极易漏掉其中某一项。

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

【选型判断树】

text
要分布式互斥:
├─ 精确性要求多高?
│   ├─ 允许极小概率重复 → Redis 单节点/主从锁 + 看门狗(性价比最高)
│   ├─ 资金/库存强正确 → 不要用锁扛主路径:库存类 2 万 QPS 时第 1 条已判「锁临界区哪怕 1ms 也只有 1000 TPS」,RedLock 五节点多数派单单加锁就 ~0.9ms,反而把吞吐压到约 1100 TPS。正解是「Redis 原子预扣 + DB 条件更新为权威」,锁只做低频防重;ZK/etcd 锁仅用于低频、长临界区、且要求不丢锁的协调场景
│   └─ 仅防重复点击 → 幂等 token 也许就够(第 110 题)
├─ 临界区耗时是否可控?
│   ├─ 可控且短 → 固定 leaseTime
│   └─ 不可控 → 必须看门狗续期
└─ 是否需要可重入/可中断/公平?
    └─ 用 Redisson,不要手写

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

时间任务内容
0:00–0:30定性「分布式锁要把持锁状态放到共享存储;核心是原子加锁、原子解锁、防死锁、防误删」
0:30–2:00正确实现SET NX EX;value=UUID;Lua 解锁
2:00–3:00三大坑死锁(无过期)、误删(非原子/不唯一)、提前释放(无续期)
3:00–4:00主从丢锁异步复制窗口;RedLock/ZK;业务幂等兜底
4:00–5:00收尾「主路径走原子预扣+DB 条件更新为权威,Redisson 只做低频防重;强一致不等于上更重的锁」

【关键数字】

参数经验值说明
默认锁时长(Redisson)30s看门狗续期
看门狗周期锁时长 1/3,即 10s续期检查
业务临界区建议上限=1÷临界区耗时:1ms→约 1000 TPS、5ms→约 200 TPS;2 万 QPS 需 ≤50µs、1 万 QPS 需 ≤100µs(含 DB 写都不可能做到)所以锁扛不了扣减主路径,只能低频防重
RedLock 节点通常 5 个独立主多数派加锁
等待锁超时业务相关,如 3–10s避免无限等

【追问链】(三层)

L1|「为什么 SETNX 不能直接用?」 → 不带过期会死锁;且常见写法「先 setnx 再 expire」非原子。应使用 SET key val NX EX seconds。

L2|「看门狗怎么工作?为什么进程挂了不会死锁?」 → 未显式 leaseTime 时,Redisson 启动后台任务每 10s(30s 的 1/3)检查并续期。进程崩溃 → 看门狗线程消失 → 不再续期 → 锁到期自动释放。

L3|「主从切换锁丢了怎么办?业务上怎么兜底?」 → ①接受 Redis 锁概率性失效,在业务层做幂等/版本号/DB 条件更新(update stock set n=n-1 where n>0);②强一致且低频、长临界区、要求不丢锁才用 ZK/etcd(高并发扣减主路径上它同样受串行化限制,不是「更重所以更强」);③不要把 Redis 锁当成资金安全的唯一防线。

【评分标准】

档位答案特征
60 分能说出 SETNX + 过期 + 唯一 value
80 分加锁解锁都原子(SET NX EX / Lua);能指出三大坑
95 分讲清主从丢锁与业务幂等;能推荐 Redisson 看门狗;不把分布式锁神化

【关联题】

【自测】

  1. 解锁为什么必须 Lua? 参考答案:保证「判断 value 是自己的」和「删除」原子,防止误删他人锁。
  2. 锁没设置过期时间会怎样? 参考答案:持有者崩溃后锁永存,死锁。
  3. 判断对错:Redis 分布式锁可以 100% 保证互斥。 参考答案:错。主从切换、时钟漂移、RedLock 争议等都可能导致极小概率失效,强正确要业务幂等兜底。

37. 加了过期时间的锁,业务没跑完锁先没了(看门狗机制) ​

【考察内容】分布式锁框架实现原理

【题目】给 Redis 锁设置过期时间后,新的麻烦出现了:业务逻辑执行超过锁的过期时间,锁被提前释放,另一个实例拿到锁,两个实例同时改数据。换用 Redisson 后这个问题消失了——它说的“看门狗”机制到底是怎么工作的?

【参考答案】加了过期时间的锁,业务没跑完锁先释放——需要看门狗续期 + 安全解锁:

  1. 容量估算(步进):锁 TTL 若设 5s,而业务长事务偶发 10–30s(慢 SQL、下游抖动),则有概率锁先过期,其他实例进入造成临界区并发。假设高峰 100 个持锁事务、5% 超过 TTL,则约 5 次/窗口 的互斥失效风险,对库存/账户不可接受。
  2. 看门狗机制:Redisson Watchdog 默认每 TTL/3 续期一次(默认锁 TTL 30s → 每 10s 检查客户端存活并续期回 30s);业务结束手动 unlock 停止续期。原理:持有者还活着就延长互斥,死亡则不续,锁最终过期,死锁可自愈。
  3. 工程要点:① 不要用固定 TTL 硬扛长业务,也不能 TTL 过大导致死锁时间过长;② 可重入锁支持嵌套;③ 解锁必须校验持有者(Lua);④ 关键路径尽量缩短持锁范围,锁内别调不确定的慢下游。
  4. 失败与降级:看门狗所在实例宕机 → 锁到期自动释放(正确性优先于活性);续期失败告警并让业务失败回滚;锁竞争激烈时用分段锁/库存分桶降低热点;Redis 锁不可用 → DB 条件更新兜底。
  5. 口径:锁的正确性 = “互斥窗口覆盖业务临界区”,TTL 静态设置很难覆盖动态 RT,所以要自动续期。

【原理溯源】

  • 锁时长与业务时长的矛盾: 过期时间是死锁的保险丝,但写死了就会「业务未完成锁先没了」。看门狗用动态续期化解:锁时长只作为「持有者失联后的最长存活时间」,而不是「最长持有时间」。

  • 为什么续期间隔取锁时长的 1/3? 要容忍一次续期失败(网络抖动/Redis 短暂不可用)后仍有机会重试。30s 锁、10s 一续,失败一次还有 20s 缓冲。间隔太密浪费 CPU,太疏增加「来不及续就过期」风险。

  • 为什么进程宕机不会死锁? 看门狗是进程内的后台线程;进程死则线程死,无人续期,锁在最多一个锁时长后自动释放。这是「租约(lease)」模型:持有者必须持续证明自己活着。

  • 为什么指定 leaseTime 后看门狗不工作? 显式租约意味着用户自己承诺「业务一定在 T 内结束」。框架不再越权续期,避免「用户以为 T 后必释放,结果被无限续」的语义冲突。

  • 底层为什么用 Hash + Lua? Hash 存 field=线程唯一标识 → value=重入次数,支持同线程可重入加锁(计数+1)与解锁(计数-1,归零才删)。加锁/续期/解锁的「读-判-写」都在 Lua 里原子完成。

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

【选型判断树】

text
锁过期策略:
├─ 临界区耗时能否精确预估?
│   ├─ 能且稳定 → 显式 leaseTime(略大于 P99 业务耗时)
│   └─ 不能(下游 RPC/DB 可能抖)→ 看门狗自动续期(默认)
├─ 是否必须在某时刻一定释放?
│   └─ 是(对账窗口等)→ leaseTime + 业务超时熔断
└─ 实现方式
    └─ 优先 Redisson;手写需自己实现续期线程与 Lua

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

时间任务内容
0:00–0:30定性「问题是固定过期 vs 业务不确定时长;解法是租约+续期」
0:30–2:00机制流程默认 30s;每 10s 续期;unlock 停止;宕机不续
2:00–3:00为什么不死锁进程内线程消失 → 不续 → 锁到期
3:00–4:00leaseTime 分支显式指定则不续期;语义差异
4:00–5:00收尾「看门狗把 TTL 从『业务时限』变成『失联时限』」

【关键数字】

参数经验值说明
默认锁时间30sRedisson
续期周期10s(1/3)可容忍一次失败
leaseTime用户指定关闭看门狗
重入计数Hash value归零才真正解锁
续期失败处理下一周期再试;过期则锁失效可告警

【追问链】(三层)

L1|「看门狗是任务执行完才释放锁吗?」 → 不是。unlock 时释放;若一直不 unlock,看门狗会一直续,直到进程退出。它不感知「业务是否逻辑结束」,只感知「持有者是否还持有/还活着」。

L2|「手动指定 leaseTime 会怎样?还要看门狗吗?」 → 不续期。必须自己保证业务在 leaseTime 内完成,否则锁提前释放导致并发。不确定耗时就不要指定,用默认看门狗。

L3|「续期时 Redis 挂了怎么办?会不会两把锁?」 → 续期失败会重试;若锁最终过期,其他客户端可加锁,此时若原业务还在跑,就可能出现并发——这是 Redis 锁的固有窗口。业务上用幂等/条件更新兜底,不要只靠锁。

【评分标准】

档位答案特征
60 分知道看门狗会自动延长锁时间
80 分说清 30s/10s、宕机不续、leaseTime 关闭续期
95 分讲清租约模型与 1/3 间隔的原因;Hash+Lua+可重入;指出续期失败窗口与业务兜底

【关联题】

【自测】

  1. 为什么续期间隔是锁时长的 1/3? 参考答案:为续期失败预留重试窗口,平衡开销与安全。
  2. 指定了 leaseTime,业务又超时了怎么办? 参考答案:锁会按 leaseTime 释放,可能并发。要么别指定,要么业务内做超时控制与幂等。
  3. 判断对错:看门狗可以防止死锁,所以锁可以不设过期。 参考答案:错。看门狗本身依赖「有过期时间且会续期」;不设过期就没有租约语义,进程崩溃会死锁。

38. “先改库还是先删缓存”——顺序之争(更新顺序) ​

【考察内容】缓存一致性最经典一问

【题目】团队对缓存更新顺序吵起来了:A 说先删缓存再改库,B 说先改库再删缓存。两种顺序分别会踩什么坑(并发下会出现什么脏数据)?最终推荐哪种?为什么?

【参考答案】“先改库还是先删缓存”按一致性权衡选:

  1. 常见顺序:① 先更 DB,再删缓存(推荐默认):窗口期最多是“旧缓存被读到”,TTL 兜底;② 先删缓存,再更 DB:删除后到更新完成前,读 miss 会把旧 DB 数据写回缓存,不一致窗口更容易拉长,一般不推荐单用;③ 延迟双删 / binlog 订阅:作为①的强化。
  2. 容量与影响估算(步进):写 QPS 低、读 QPS 高时,策略选择影响“展示错误率”。假设读 2 万 QPS,不一致窗口 100ms,则约 2000 次读可能看到旧值;把窗口压到 10ms(快速删+短 TTL)错误曝光降一个量级。
  3. 例外:写多读少且极度怕脏读 → 可短暂加分布式读写锁(代价高);缓存为权威的计数等 → 用 Redis 原子命令,不走“删缓存”心智。
  4. 失败与降级:删除失败 → 重试队列 + TTL;仍不一致 → 交易/资金读 DB 权威值;监控差异抽样。
  5. 口径:互联网业务的务实选择是 “更新 DB + 删除缓存 + TTL + 失败重试”;强一致字段直接不缓存。

【原理溯源】

  • 先删缓存再更 DB 的脏读时序:

    1. W:DEL cache;
    2. R:miss → 读 DB 得旧值 → 回填 cache=旧值;
    3. W:UPDATE DB=新值。 若 2 发生在 3 之后(或回填晚于 3),缓存长期为旧值,且直到 TTL 或下次写才会修——不一致窗口可达分钟级。
  • 先更 DB 再删缓存的时序:

    1. W:UPDATE DB=新值;
    2. R:命中 cache=旧值(极短窗口);
    3. W:DEL cache → 下次读回填新值。 不一致只存在于 1–3 之间,通常毫秒级;但"删除后必然收敛"要加两个前提——删除真的成功(失败要靠重试/binlog 补偿),且 2 的读没有在 3 之后才回填(否则旧值会一直脏到 TTL,见第 28 题回填竞态)。
  • 为什么删除优于更新? 更新缓存要「写入一个具体值」。并发写时:A 计算出旧值准备写缓存,B 已写新值,A 再写就把新值覆盖成旧值。删除不写值,让下一次读从 DB 拿当时的真实值,从机制上避开覆盖。

  • 为什么这不是强一致方案? 跨 DB 与 Redis 无法在性能可接受时做分布式事务。目标是把不一致窗口压到毫秒级并保证必然自愈(删除或 TTL)。要强一致:关键读直连 DB,或写后读主。

  • 和「延迟双删」的关系: 若采用「先删再更」,延迟双删可补救回填竞态;若采用「先更再删」,双删通常非必须,重点是删除失败补偿(重试/binlog)。顺序问题与失败问题是两个维度。

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

【选型判断树】

text
更新顺序:
├─ 默认互联网场景
│   └─ 先更 DB → 再删缓存(Cache Aside)
├─ 若因历史原因必须「先删再更」
│   └─ 加延迟双删 + 短 TTL + binlog
├─ 删除可能失败
│   └─ 重试队列 / Canal 异步删
└─ 强一致要求(钱、库存终态)
    └─ 不要赌缓存顺序:读主库/直连 DB + 版本号

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

时间任务内容
0:00–0:30结论先行「推荐先更库再删缓存;先删后更窗口更大」
0:30–2:20两个时序分别画出并发读回填导致的脏数据路径
2:20–3:20为什么删不更新覆盖问题;删除让回填来自最新 DB
3:20–4:20失败与强一致删除失败补偿;强一致绕开缓存
4:20–5:00收尾「选窗口短、可自愈的顺序,而不是理想化地追求零窗口」

【关键数字】

参数经验值说明
先更后删不一致窗口毫秒级可接受
先删后更脏值存活可达一个 TTL(分钟)不可接受的来源
删除失败重试3–5 次 + 异步补偿必须有
binlog 纠正延迟10ms–1s真相源驱动
兜底 TTL(先更库后删缓存的自愈窗口)1–10 分钟自愈

【追问链】(三层)

L1|「先更库再删缓存,删之前用户读到旧价,可以吗?」 → 一般业务可以,窗口毫秒级。价格展示场景若不可接受,应缩短 TTL、写后主动删、或关键读直连 DB。

L2|「更新缓存为什么不好?」 → 并发写会旧值覆盖新值;且更新逻辑若依赖旧值计算(如 +1),还可能算错。删除+回填把「写缓存」变成「读时从 DB 取真值」。

L3|「有没有办法做到顺序无关的强一致?」 → 有代价更高的路:①缓存带版本,只允许新版本覆盖;②分布式锁串行化「更库+删缓存」;③直接读 DB。多数业务不值得,用「先更后删+补偿+TTL」已足够。

【评分标准】

档位答案特征
60 分知道推荐先更库再删缓存
80 分能分别描述两种顺序的并发脏数据时序
95 分讲清「删 vs 更新」的原因;区分顺序问题与失败问题;强一致场景给绕行方案

【关联题】

【自测】

  1. 画出「先删缓存再更 DB」产生长期脏缓存的三步时序。 参考答案:DEL cache → 并发读 miss 读 DB 旧值并回填 → UPDATE DB 新值。若回填在更新后,缓存长期旧。
  2. 为什么不推荐「更新缓存」? 参考答案:并发下旧值可能覆盖新值;删除让下次读从 DB 回填最新值。
  3. 推荐顺序下删除失败怎么办? 参考答案:重试 + binlog 异步删 + 短 TTL 自愈。

39. 每个用户每分钟最多 10 次,多实例怎么统一计数(分布式限流) ​

【考察内容】分布式限流的工程实现

【题目】业务规则:每个用户每分钟最多调用某接口 10 次,超了直接拒绝。系统是多实例部署,单机计数不共享。怎么用 Redis 实现多实例统一的计数限流?滑动窗口怎么落地?为什么用 Lua?

【参考答案】分布式限流计数:多实例统一额度,必须共享状态 + 原子操作:

  1. 容量估算(步进):每用户每分钟最多 10 次,假设 DAU 500 万、高峰 10% 同时活跃 = 50 万用户在线,每个用户窗口一个计数 key,Redis 内存 ≈ 50万×100B ≈ 50MB,完全可行。QPS:高峰读写计数可能 5–10 万级,接近单节点上限时要分片。
  2. 实现:① Redis INCR + EXPIRE(固定窗口,有临界突刺,实现最简单;但两条命令非原子——INCR 后崩溃会让 key 永不过期、该用户被永久限流,要么用 SET key 1 EX 60 NX + 后续 INCR,要么一段 Lua,要么只在 INCR 返回 1 时补 EXPIRE 并接受残留风险);② 滑动窗口 ZSet(score=时间戳,更平滑);③ Lua 保证“读-判-写”原子;④ 云/中间件 Sentinel 也可下发集群规则。
  3. 本地+分布式组合:先本地粗限流(保护 Redis),再分布式精确总量;避免每个请求都打 Redis。
  4. 失败与降级:Redis 挂 → 降级单机限流(阈值=总量/实例数),宁可略紧不可全开;时钟问题影响窗口 → 以 Redis 服务端时间为准;误杀调阈值动态配置。
  5. 清理:TTL 设为窗口长度;key 设计 rate:{user}:{minute} 控制基数。

【原理溯源】

  • 为什么单机计数不行? 负载均衡下每个实例只看到自己那份流量,N 个实例各自限 10 次,用户实际能打 10N 次。限流阈值必须定义在用户全局上,计数器就要共享存储——Redis 单线程原子计数是天然选择。
  • 固定窗口为什么有边界突刺? 窗口按「整分钟」切开。用户在 59.5s 用满 10 次,在 00.0s 新窗口又是 10 次——1 秒内 20 次。根因是窗口对齐与流量突发不匹配,不是计数错误。
  • 滑动窗口如何消除突刺? 记录每次请求的时间戳,统计的是「最近 60s 内」而不是「当前这一个整分钟」。任何 60s 滑动区间内都不会超过阈值。ZSet 的 score=时间戳,ZREMRANGEBYSCORE 删窗外,ZCARD 计窗内。
  • 为什么必须 Lua? 一次判定需要「删旧 → 数量 → 决定 → 添加」多步。若在应用里多次调用 Redis,两次调用之间其他请求会插入,导致超卖限额。Lua 在 Redis 内单线程原子执行整段逻辑,等价于一个临界区。
  • 令牌桶为什么更适合带突发的业务? 固定/滑动窗口限制的是「次数」,令牌桶限制的是「平均速率 + 桶容量突发」。桶满可突发放行,空则匀速恢复——更接近「每分钟 10 次但允许前 3 秒用 5 次」这类真实产品语义。
  • 为什么还要网关层限流? 应用内 Redis 限流已经走到业务代码,连接建立、鉴权、序列化都发生了。网关/入口层能更早拒绝,保护下游一切资源;两层形成纵深。

【选型判断树】

text
限流方案:
├─ 规则是「固定时间窗内次数」且能接受边界突刺
│   └─ INCR + EXPIRE(最简单,但两条命令非原子:INCR 后进程崩溃会留下永不过期的 key,计数从此卡死不降——要么 `SET key 1 EX win NX` 起步+INCR,要么 Lua 里 INCR 后判返回值==1 才补 EXPIRE)
├─ 不能接受边界突刺
│   └─ ZSet 滑动窗口(Lua 原子)
├─ 规则是「平均速率,允许一定突发」
│   └─ 令牌桶 / 漏桶(Lua 维护时间与余量)
├─ 维度是用户/接口/IP?
│   └─ key 设计带上维度;避免大 key(用户极多时按 user%shard 分桶)
└─ 入口层
    └─ 网关限流 + 应用内限流双层

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

时间任务内容
0:00–0:30定性「多实例必须共享计数;先讲固定窗口,再讲它为什么不够」
0:30–1:40固定窗口INCR+EXPIRE;边界突刺例子(59s+00s)
1:40–3:00滑动窗口ZSet + 时间戳 + Lua 原子删/数/添
3:00–4:00令牌桶与工程平均速率;网关+应用双层
4:00–5:00收尾「选型看是否容忍突刺、要不要突发容量;原子性靠 Lua」

【关键数字】

参数经验值说明
常见用户限流10–100 次/分钟按业务
固定窗口键 TTL60s(窗口长度)与窗口对齐
滑窗 ZSet member每请求一个 UUID窗外自动删
滑窗内存单用户约 N×几十字节阈值低时很小
Lua 优势一次 RTT 完成判定避免竞态
网关限流全局/接口维度先于业务

【追问链】(三层)

L1|「为什么不用数据库计数?」 → 限流在最热路径上,DB 的连接与事务开销扛不住;Redis 内存原子计数延迟亚毫秒级,是共享计数的标准件。

L2|「ZSet 滑窗会不会内存无限涨?」 → 每次都删窗外 member,窗口内数量 ≤ 阈值。若攻击者狂刷,达到阈值后不再 ZADD,内存有上界。仍建议配 maxmemory 策略。

L3|「Lua 脚本报错或超时怎么办?限流会不会放行失控?」 → 脚本执行失败应 fail-close(当作超限)或本地降级限流,不能 fail-open 放任。同时监控脚本错误率;Redis 不可用时网关层仍应有限流兜底。

【评分标准】

档位答案特征
60 分能用 INCR+过期实现固定窗口
80 分指出边界突刺,给出 ZSet 滑窗 + Lua
95 分讲清原子性为何必须 Lua;令牌桶与窗口的语义差;多层限流与 fail-close

【关联题】

【自测】

  1. 固定窗口的边界突刺是什么?举例。 参考答案:分钟交界前后各用满阈值,导致极短时间内放行双倍流量。如 59s 与 00s 各 10 次。
  2. 滑动窗口为什么用 Lua? 参考答案:删旧、计数、添加必须原子,否则并发请求会互相插入导致超限。
  3. 用户量极大时,限流 key 有什么隐患? 参考答案:key 数量爆炸与大 ZSet。可按用户散列分桶、缩短 TTL、或对非核心维度降级为固定窗口。

40. 订单 30 分钟未支付要自动关单(延时任务实现) ​

【考察内容】延时任务实现是电商场景最高频设计题之一

【题目】需求:订单 30 分钟未支付自动取消,已支付的不取消。你准备用 Redis 实现延时队列(不想引入额外组件),怎么设计?和“定时扫描数据库”“MQ 延迟消息”比,各自的优劣是什么?

【参考答案】订单 30 分钟未支付自动关单:延时触发 + 幂等 + 可靠:

  1. 容量估算(步进):假设日订单 100 万,支付转化 70%,则约 30 万单/日 需要超时关单,平均约 3.5 单/s,高峰支付前创建订单峰值可能 500–2000 TPS,对应 30 分钟后有集中关闭波峰(若整点营销创建,波峰可更高)。
  2. 实现方案:① 延迟队列:RocketMQ 延迟消息 / RabbitMQ TTL+死信 / 时间轮;② 定时任务扫表:WHERE status=CREATED AND expire_at<now,批量关单(要分片与索引);③ Redis ZSet+轮询(score=到期时间戳,ZRANGEBYSCORE 取到期)——单 key 装不下本题量级:这里的 500–2000 TPS 是整点营销的脉冲上界(第 1 条的 30 万单/日摊算只有约 3.5 TPS,即便集中在 2 小时也才约 42 TPS),按它×1800s =90 万~360 万成员,只算 30% 未支付也有 27 万~108 万,正撞第 30 题「集合元素 >10 万即严重」的大 key 红线,且每秒轮询同一 key 会造成分片热点;必须按到期时间分片(如 expire_slot=ts%64 拆 64 个 ZSet)并给「多实例抢同一批」的抢锁消费(Lua ZREM 返回被取走的成员)。
  3. 选型:延迟消息实时性好、解耦;扫表实现简单、最终可靠,常作为兜底。但选型要先按题干前提分两支,别一上来就把 MQ 当首答:题干已排除额外组件(「不想引入 MQ」)→ 首答就是 Redis ZSet 分片+扫表兜底,MQ 只作「若能引入会更实时解耦」的对比与后续演进;若允许引入组件 → 两者可并存(消息主路径 + 扫表补偿)。
  4. 失败与降级:消息丢失 → 扫表兜底;消费失败重试+死信告警;关单必须幂等(唯一状态迁移);关单同时要回补库存/优惠券;用户刚好在关单瞬间支付 → 状态机校验,支付回调以订单状态为准,冲突则退款或人工。
  5. 监控:延迟关闭成功率、超时未关堆积量、回补库存对账差异。

【原理溯源】

  • 为什么不能用普通定时 Thread.sleep? 应用重启/发布线程就没了,延时任务丢失。分布式下每个实例各有各的定时器,还会重复触发。所以延时必须落在共享、可恢复的存储或消息系统上。
  • ZSet 延时队列的因果链: 写入时 score=触发时间戳 → 时间到后 ZRANGEBYSCORE key 0 now 取出 → 业务处理 → ZREM。Redis 保证按 score 有序,O(logN+M) 取到期元素。注意:取出与处理非事务,要「处理成功再删」或「先删再处理+失败回队」,配合幂等。
  • 为什么过期监听不可靠? key 的物理删除由惰性/定期触发,过期事件在真正删除时才发,可能延迟;Redis 重启/事件总线丢消息也会丢事件。它不是为可靠投递设计的,只适合「丢了也无所谓」的场景。
  • 为什么 MQ 延迟消息更适合生产? Broker 持久化、投递有重试与死信、消费位点可管理。RocketMQ 支持定时/延迟消息(新版本可任意时刻),RabbitMQ 用 TTL+死信模拟。代价是引入组件与延迟粒度限制。
  • 为什么扫表仍然要留着当兜底? 任何异步通道都可能丢消息、积压或故障。扫表以 DB 状态为真相(create_time < now-30min 且 status=待支付),是最笨也最不容易「漏单」的一层。双保险:MQ 管时效,扫表管正确。
  • 时间轮是什么? 把时间分成轮次格子,定时器挂到对应格,指针转动触发。O(1) 添加/取消,适合进程内海量短延时。但同样面临进程退出丢失,需配合持久化。

【选型判断树】

text
延时关单:
├─ 能否引入 MQ?
│   ├─ 能 → 延迟消息为主 + DB 扫表兜底
│   └─ 不能(题干即此前提:不想引入额外组件)→ Redis ZSet 分片 + Lua 抢占 + 扫表兜底,MQ 只作对比与后续演进
├─ 延迟精度要求?
│   ├─ 秒级足够 → 上述皆可
│   └─ 毫秒级且进程内 → 时间轮(需持久化)
├─ 丢了是否可接受?
│   └─ 不可接受(资金/库存)→ 必须以 DB 状态扫表兜底
└─ 已支付校验
    └─ 关单前再查一次支付状态,防已支付被关

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

时间任务内容
0:00–0:30定性「延时任务要可恢复、可去重;不能靠本地 sleep」
0:30–2:00Redis ZSetscore=到期时间戳;按 expire_slot=ts%64 分片(单 key 装不下本题量级);轮询取到期;多实例用 Lua ZREM 原子抢占再处理
2:00–3:00为何不用过期监听删除才发事件、可能丢
3:00–4:00MQ 与扫表MQ 可靠时效;扫表保正确
4:00–5:00收尾「按题干能否引入组件分两支:不能→Redis ZSet 分片+扫表兜底;能→延迟消息主路径+扫表补偿;两支都关单前复查支付状态」

【关键数字】

参数经验值说明
订单支付超时常见 15–30 分钟按品类
ZSet 轮询间隔1s与精度/DB 压力权衡
RocketMQ 延迟4.x:18 个固定档位;5.x:可指定任意到点时刻(默认秒级精度、最长 24h)按版本分两支答
扫表间隔1–5 分钟兜底
扫表 SQLstatus=待支付 and create_time < 阈值索引要建对
重复关单必须幂等支付成功竞态

【追问链】(三层)

L1|「ZSet 方案订单处理失败怎么办?」 → 保留在队列中重试(重试次数上限后进死信/人工);或先 ZREM 再处理,失败则重新 ZADD。必须与业务关单幂等配合。

L2|「为什么不能只靠 Redis 过期事件?」 → 事件与物理删除耦合,可能延迟或丢失;Redis 故障期间更无保障。订单关闭是资金相关状态机,不能建立在 best-effort 上。

L3|「用户在第 29 分 59 秒支付成功,关单任务已经扫到了订单怎么办?」 → 关单前再查一次支付状态(或用状态机 CAS:update order set status=已取消 where id=? and status=待支付)。已支付则放弃关单。支付回调与关单的竞态要用条件更新化解。

【评分标准】

档位答案特征
60 分能用 ZSet score=时间做延时队列
80 分讲清取出/删除/失败重试;对比扫表与 MQ
95 分指出过期监听不可靠;按题干前提选型(排除 MQ 时答 ZSet 分片+Lua 抢占+扫表兜底);处理支付竞态(CAS/复查)

【关联题】

  • 同一知识簇: 第 31 题(ZSet)、第 29 题(缓存策略选型,含 Write Behind 的丢失窗口)、第 110 题(幂等)
  • MQ 侧: 第 86 题(Kafka)可对比消息可靠投递思想(本章业务用 RocketMQ 更常见)

【自测】

  1. ZSet 延时队列中,为什么强调「处理成功再 ZREM」? 参考答案:单消费者时处理成功再删可重试(先删后处理若失败任务就丢了);但多实例并行消费必须反过来——用一段 Lua 先 ZRANGEBYSCORE 取到期成员、再 ZREM 删掉(整段原子执行,才能把被取走的成员返回给自己;单独 ZREM 只返回删除个数,拿不到成员)再处理,否则两个实例同时 ZRANGEBYSCORE 取到同一批就是重复消费。两种做法都以消费端幂等为前提。
  2. 过期监听适合什么场景? 参考答案:丢了影响很小的会话清理、缓存统计等,不适合订单关闭。
  3. 生产上如何防止「已支付订单被自动关闭」? 参考答案:关单前复查支付状态 + DB 条件更新(仅待支付可改为已取消)+ 支付回调与关单幂等。

41. 游戏积分榜要实时更新、秒级查 TopN(排行榜设计) ​

【考察内容】排行榜是 ZSet 应用最经典场景题

【题目】游戏要做实时积分排行榜:玩家分数随时变化,榜单要实时刷新,还要支持 Top100 秒级查询、分页查看、按名次查人。怎么用 Redis 实现?日榜和总榜分别怎么处理?

【参考答案】实时积分榜:ZSET 为主,热点与容量要打散:

  1. 容量估算(步进):假设玩家 500 万上榜,ZSET 每元素约 64–100B → 单榜内存约 320–500MB(5e6×64B≈320MB),属于大 key,可能阻塞节点。更新 QPS 若高峰 5 万,单 key ZADD 会打满单分片。TopN 查询(如 Top100)极频繁,读 QPS 可能 10 万级。
  2. 设计:① 分榜:按赛季/分区分片;日榜与总榜要分开设计:日榜按天建 key(rank:day:{yyyymmdd})+当日 TTL、跨天自然滚动不需清理;总榜独立 long-lived key 且写入与日榜同源(避免两套计数对不上);两者都从同一份明细聚合作业回填,榜的口径(含不含退款/去刷量)必须一致,客户端路由;② 写聚合:本地攒批后定时 ZADD,降低中心 ZSET 压力——攒批周期必须 ≤ 题干允许的刷新粒度:若「实时刷新」指秒级(≤1s),1–5s 的攒批就已违约,要缩到 100–200ms 或改逐条 ZADD(此时中心 ZSet 写压力=峰值更新量,只能靠按分片摊开扛);若业务能接受分钟级,则攒批 1s+TopN 缓存 1–5s 才成立。先声明刷新口径,再选攒批参数;③ 热点玩家更新可分段后再合并;④ TopN 缓存(每 1–5s 刷新 Top100 到另一个小 key)。
  3. 查询:ZREVRANK 用户名次、ZREVRANGE TopN;分页榜用范围命令。
  4. 失败与降级:Redis 挂 → 展示上次缓存榜或 DB 快照;更新延迟可最终一致;恶意刷分走风控;清理赛季数据用 UNLINK。
  5. 一致性:分数来源服务端计算,防客户端伪造;并列分数用时间戳/用户 id 打破平局保证稳定排序。

【原理溯源】

  • 为什么 ZSet 是排行榜标配? 三个原生能力正好命中:①按 score 排序(跳表);②ZINCRBY 原子加减分;③ZREVRANGE/ZRANK 高效取区间与名次。用 List/Hash 都要自己排序,O(N) 且难保原子。
  • 复杂度为什么能「秒级」? ZINCRBY 为 O(logN);ZREVRANGE top100 为 O(logN+100)。百万用户也是亚毫秒到毫秒级,与「全量排序再截取」有本质区别。
  • 日榜为什么要独立 key? 时间维度不同,排名不可混。rank:day:20260911 每天新建、当日 TTL,跨天自然滚动不需清理(要给次日零点前查榜留余量时,最多把 TTL 放到当日+1 天,别按 2 天配);总榜单独一把 key。避免「用一个 key 再过滤日期」的复杂查询。
  • 同分如何「先到先得」? ZSet 只按 score 排。把 score 编码成 分数*K + 时间因子,让同分者时间早者 score 更优。代价是 double 精度:53 位有效整数,K 过大或分数过大时低位时间被丢掉,排序错乱。必须评估量级,或拆两个字段用应用层二次排序。
  • 为什么要落库快照? Redis 是易失层。榜单若承载运营奖励,必须周期性(如每 5 分钟/每日终)把 TopN 或全量写入 DB,供审计与恢复。这是「缓存做加速,DB 做真相」。

【选型判断树】

text
排行榜:
├─ 用户规模
│   ├─ 万级以下 → ZSet 单 key 轻松
│   ├─ 百万级 → ZSet 仍可,注意大 key 访问模式(只取 TopN,不全量 ZRANGE)
│   └─ 亿级+每人都要名次 → 考虑分片/离线预计算,不要对全量频繁 ZRANK
├─ 榜单维度
│   └─ 日/周/总用不同 key;周期 key 可 TTL
├─ 同分规则
│   └─ 仅按分 → 默认;先到先得 → score 编码(注意 double 精度)
└─ 持久化
    └─ 周期快照落 DB + 从库/集群防丢

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

时间任务内容
0:00–0:30定性「ZSet:score=分数,原生排序+原子加减」
0:30–2:00核心命令ZINCRBY / ZREVRANGE / ZRANK;复杂度
2:00–3:00日榜总榜日榜按天拆 key+当日 TTL(跨天自然滚动);总榜 long-lived、与日榜同源
3:00–4:00同分与精度score 编码;double 53 位陷阱
4:00–5:00落地落库快照;大榜只取 TopN

【关键数字】

参数经验值说明
TopN常 100/200/500产品定义
ZREVRANGE top100O(logN+100)毫秒内
亿级 ZSet 内存约数 GBmember 数十 B
double 整数精度53 位 ≈ 9e15score 编码上限
日榜 TTL当日滚动(跨天自然失效);若要回看历史榜,另按 {date} 存快照 1–3 天榜本身不需清理;总榜不设 TTL,须与日榜同源
快照落库每 1–15 分钟或日终防丢

【追问链】(三层)

L1|「用 List 能做排行榜吗?」 → 能存有序数组但不能按 score 原子更新某人名次,任何分数变化都要重排,O(N) 且并发难控。必须 ZSet。

L2|「分数相同怎么排?」 → 按 member 的字典序(ZSet 排序是 score→member,确定的,不是插入序,也不是实现分支)。要「先到先得」就把时间编进 score,注意 double 精度不要爆。

L3|「玩家掉线时分数更新失败,榜不准了怎么办?」 → 分数变更应走可靠路径:先落 DB/消息,再更新 Redis;或以 DB 为源定期重放。榜是展示层,账目正确优先于实时。

【评分标准】

档位答案特征
60 分知道用 ZSet 和 ZREVRANGE
80 分讲清 ZINCRBY/ZRANK、日榜按天 key+TTL 与总榜 long-lived 同源、口径一致(含不含退款/去刷量)、落库
95 分同分编码与 double 精度陷阱;海量用户只取 TopN;以 DB 为账目真相

【关联题】

【自测】

  1. 实时加分用什么命令?为什么原子? 参考答案:ZINCRBY。Redis 单线程执行,命令天然原子。
  2. 为什么日榜要单独 key? 参考答案:时间维度隔离,避免混榜;可用 TTL 自动清理历史。
  3. score 编码时间戳时最大的坑? 参考答案:double 只有 53 位整数精度,编码后超过约 9e15 会丢精度导致乱序。

42. App 要“查看附近的人/门店”,按距离排序(LBS 实现) ​

【考察内容】LBS 场景实现

【题目】社交 App 要上线“附近的人”,本地生活 App 要“附近的门店”,都是“给我当前位置,返回按距离排序的附近对象”。数据量百万级、更新频繁,怎么用 Redis 实现?地理位置怎么存储和计算距离(两问都要答:存储见下;距离由服务端算——GEOSEARCH ... WITHDIST 或 GEODIST 返回米(内部按球面/Haversine 计算),GEOPOS 取回经纬度再自算也可以;geohash 前缀只能判邻近,不能当距离用)?

【参考答案】附近的人/门店:GEO + 范围检索 + 索引兜底:

  1. 容量估算(步进):假设城市门店 20 万、附近用户查询峰值 1 万 QPS。GEOSEARCH(或 GEORADIUS)复杂度与范围内元素相关;若 3km 内有 500 家店,单次返回可控。全城所有点放一个 key 会成大 key(20万×~60B ≈ 12MB+),需按城市/网格分 key。
  2. 方案:① Redis GEO(geohash 存储):GEOADD 更新坐标,GEOSEARCH 按距离排序取 TopN;② 数据量大或复杂筛选 → PostGIS/MySQL 空间索引/专用 LBS 服务,Redis 缓存热门网格结果;② 地理网格预计算:按 geohash 前缀分桶,只查邻近 9 个桶。
  3. 性能:结果可短 TTL 缓存;用户位置抖动导致缓存命中低,可网格量化(四舍五入到网格中心)。
  4. 失败与降级:Redis 挂 → 降级查 DB 空间索引+限流;无坐标 → 默认城市热门列表;隐私合规要脱敏与授权。
  5. 口径:先估点密度与 QPS,决定用纯 GEO 还是网格+缓存+DB。

【原理溯源】

  • 为什么不用「全量算距离再排序」? 百万点每次请求都要算距离,CPU 与延迟不可接受。GeoHash 先把二维邻近变成一维前缀邻近,用索引快速圈出候选,再精确算距离——典型的「粗筛 + 精滤」。
  • GeoHash 如何把「附近」编码出来? 经纬度各在区间上二分,交替取 bit,拼成字符串。精度越高(字符串越长)网格越小。同一网格内的点前缀相同,前缀相同 ≈ 距离近。边界上的邻居可能前缀不同,所以要查周围 8 个网格。
  • 为什么 GEO 底层是 ZSet? GeoHash 编码成 52 位整数作为 score,member 为元素。ZSet 的有序性支撑范围查询,现有集群/持久化能力直接复用。这也是 GEOADD 的成员其实是 ZSet member 的原因。
  • 为什么按城市拆 key? 单 key 塞下全国点位会变成超大 key:内存倾斜、单节点瓶颈、范围查询慢。按 geo:city:xxx 切分后,单 key 元素量可控,还能按业务路由到不同分片。
  • 位置高频更新怎么办? 「附近的人」类数据有时效性,但要说清 Redis 的 TTL 是 key 级:GEO 数据存在一个 ZSet 里,GEOADD 没有 EX/PX 参数,ZSet 成员也没有独立过期时间(Redis 7.4 的逐字段 TTL 只作用于 Hash)。所以不能「给每个用户设 TTL」,只能:① 用另一张 score=最后上报时间的 ZSet,定时 ZRANGEBYSCORE 捞出超时成员、回到 GEO key 里 ZREM;② 或每用户一个带 TTL 的独立 key,代价是不能再用一次 GEOSEARCH 扫全城。否则离线用户会长期占榜、污染结果。

【选型判断树】

text
LBS:
├─ 数据量与更新频率
│   ├─ 十万级、更新不高 → Redis GEO 单城 key
│   ├─ 百万级 → 按城市/网格分 key
│   └─ 千万+、复杂多边形/筛选 → ES/PostGIS 等空间索引
├─ 查询形态
│   ├─ 半径 TopN → GEORADIUS / GEOSEARCH COUNT
│   └─ 多条件过滤 → 先 GEO 粗筛再业务过滤,或换 ES
└─ 时效
    └─ 在线场景:score 记上报时间 + 定时清理陈旧坐标(成员没有逐条 TTL)

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

时间任务内容
0:00–0:30定性「Redis GEO:GeoHash 一维化 + ZSet 范围查」
0:30–2:00命令与结构GEOADD / GEOSEARCH;底层 ZSet score=geohash
2:00–3:10GeoHash 原理二分编码、前缀邻近、边界 8 格
3:10–4:20工程按城市分 key 防大 key;定时 ZRANGEBYSCORE+ZREM 清陈旧坐标
4:20–5:00扩展海量上 ES/PostGIS

【关键数字】

参数经验值说明
GeoHash 位数(Redis 内部 52 bit 的等效精度)约 11 位 → 米级52 bit=经纬各 26 bit
单 key 建议城市级,避免全国一把防大 key
在线时效5–30 分钟无上报即清理靠定时清理,GEO/ZSet 成员没有逐条 TTL
GEOSEARCH COUNT20–100产品页大小
边界问题查 9 宫格相邻前缀不同

【追问链】(三层)

L1|「GEO 的底层数据结构是什么?」 → ZSet。score 是 GeoHash 编码的 52 位整数,member 是元素。因此 GEO 命令的「圈候选」语义落在 ZSet 之上——但 ZSet 的 score 是 52 位 geohash,它的序不等于距离的序;真实距离由服务端按球面/Haversine 算(GEOSEARCH ... WITHDIST/GEODIST 返回米),geohash 前缀只能判邻近、不能当距离用。

L2|「GeoHash 有什么经典问题?」 → 边界:两个物理相邻的点可能分属不同前缀。解法是查询目标及其周围 8 个格子,合并结果再按真实距离排序。

L3|「全国门店都放一个 GEO key 行不行?」 → 不推荐。会形成超大 key,单节点瓶颈、内存倾斜、慢查询。应按城市/大区拆分,必要时再哈希分片。

【评分标准】

档位答案特征
60 分知道 GEOADD/GEORADIUS,并能说清距离由服务端算(GEOSEARCH ... WITHDIST/GEODIST 返回米)
80 分讲清 GeoHash 前缀思想与 ZSet 底层
95 分边界 8 格;按城市分 key 防大 key;知道何时该上 ES 等空间索引

【关联题】

【自测】

  1. GEO 底层是什么结构? 参考答案:ZSet,score 为 GeoHash。
  2. 为什么 GeoHash 要查周围网格? 参考答案:边界上物理相近的点前缀可能不同,需合并邻格避免漏点。
  3. 「附近门店」为什么建议按城市拆 key? 参考答案:避免单 key 过大导致内存倾斜与慢查询,并便于按区域路由。

43. 购物车刷新不丢、登录态多机共享(Redis 业务应用) ​

【考察内容】Redis 业务应用面

【题目】两个需求:①用户加购的商品,换设备刷新后购物车要还在;②用户登录后,请求被负载均衡到任意一台机器都要认识他。分别怎么用 Redis 实现?用什么数据结构?和 JWT 方案比怎么选?

【参考答案】购物车不丢 + 登录态共享:用 Redis 作为跨实例业务状态:

  1. 容量估算(步进):假设 500 万用户购物车,平均 8 个商品,hash 估算每车 500B–2KB → 内存约 2.5–10GB,需要集群分片。登录态 Token:DAU 500 万、每 session 1KB → 约 5GB,可设 TTL 减少。峰值读写可能数万 QPS,Redis 合适。
  2. 设计:① 购物车:hash cart:{uid},field=skuId,value=数量/选中状态;合并离线与登录后的车用服务端逻辑;② 登录态:JWT 可无状态,或 Redis 存 session/refresh token;③ 多机共享的验证码、防重 Token 也可放 Redis。
  3. 一致性与持久:购物车是用户资产级体验数据,可短中期持久:定期快照到 DB/异步落库;Redis 淘汰策略选对业务友好的(带 TTL 的仅过期车可清)。
  4. 失败与降级:Redis 挂 → 购物车降级为 DB/本地(可能丢最近修改),登录态优先切换备用集群;关键下单动作服务端再校验库存,不信仅缓存的状态。
  5. 口径:这类场景 Redis 存的是“会话/购物体验状态”,允许短暂不一致,但要做好内存规划与备份。

【原理溯源】

  • 购物车为什么必须「服务端持久」? 本地 Cookie/Storage 换设备即丢,且无法做服务端勾稽(凑单、限购)。放 Redis 后以 userId 为键,任意设备/实例读同一份数据。

  • 为什么用 Hash 而不是 String JSON? 「用户 → 多个 SKU → 数量」与 Hash 的 field/value 同构。HINCRBY 可对单个商品原子 ±1,避免「GET 整包 → 改 → SET」的并发覆盖与带宽放大。

  • Session 为什么要进 Redis? 应用扩容后 Session 若只在本机内存,负载均衡会把用户打到不认识他的机器(登录态丢失)。共享存储让所有实例看到同一 Session。

  • JWT 为何「无状态」? 用户信息+签名放在令牌里,服务端验签即可,不必查共享存储。好处是水平扩展简单;代价是签发后难主动失效(除非黑名单),且令牌体积大、不能存敏感明文。

  • Redis Session vs JWT 的本质取舍:

    维度Redis SessionJWT
    服务端状态有无(或仅黑名单)
    主动踢下线容易(删 key)难(需黑名单)
    多端/开放 API一般更自然
    依赖 Redis 可用性高低
  • 为什么购物车也常要「未登录合并」? 未登录加购在本地,登录后合并到服务端 cart:{userId}。合并策略要幂等(以 SKU 为 field 做 HSETNX/HINCRBY),防止重复提交翻倍。

【选型判断树】

text
登录态放哪:
├─ 传统 Web、需要服务端踢人/会话管理
│   └─ Redis Session
├─ 移动端 / 开放 API / 多语言客户端
│   └─ JWT + 短有效期 + refresh
├─ 需要强制下线的 JWT
│   └─ JWT + Redis 黑名单(token 版本号/jti)
└─ 购物车
    └─ 服务端 Hash(以 userId 为 key),未登录合并

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

时间任务内容
0:00–0:30定性「两个问题都是『状态放哪才能多机共享』」
0:30–1:40购物车Hash cart:user;HINCRBY 原子;对比 String JSON
1:40–3:00Session存 Redis 共享;TTL
3:00–4:10JWT 对比无状态 vs 可踢下线;移动端选择
4:10–5:00收尾「Web 会话用 Redis Session;跨端用 JWT+黑名单」

【关键数字】

参数经验值说明
Session TTL30 分钟–7 天滑动按安全策略
JWT access 过期15 分钟–2 小时短令牌
refresh 过期7–30 天换新 access
购物车 field 数通常 < 100大车可分页
购物车保留登录用户长期;可转 DBRedis + 落库

【追问链】(三层)

L1|「购物车用 String 存 JSON 有什么问题?」 → 无法原子改单商品数量;并发下互相覆盖;每次改都整包读写,带宽与延迟差。Hash 按 field 操作才是正确模型。

L2|「JWT 泄露了怎么办?」 → 短有效期 + refresh 轮换;敏感操作二次验证;对已泄露 jti 加黑名单。无法像 Session 那样「全局立刻失效」,这是无状态的代价。

L3|「Redis Session 挂了是不是所有人都掉线?」 → 是。需要 Redis 高可用(哨兵/集群);极端时可降级到本地会话或强制重新登录。可用性设计要包含这一依赖。

【评分标准】

档位答案特征
60 分购物车 Hash;Session 存 Redis
80 分讲清结构匹配与共享存储原因,并能对比 JWT
95 分主动踢下线、黑名单、未登录合并、Redis 可用性依赖

【关联题】

【自测】

  1. 为什么分布式环境不能把 Session 只放本机? 参考答案:负载均衡会把用户路由到无此 Session 的实例,登录态丢失。
  2. JWT 最大的工程痛点是什么? 参考答案:签发后难以主动失效,需要黑名单/版本号才能支持强制下线。
  3. 购物车加购为什么用 HINCRBY 而不是 HSET? 参考答案:HINCRBY 原子累加数量,支持「再加一件」;HSET 是覆盖为固定值。

44. 布隆过滤器会误判还不能删,生产上怎么用(误判与删除) ​

【考察内容】布隆过滤器的工程细节

【题目】你们用布隆过滤器挡缓存穿透,但它有两个天生缺陷:会误判(把不存在的说成存在)、不支持删除。生产环境里这两个问题分别怎么处理?误判会造成什么后果?有支持删除的变体吗?

【参考答案】布隆过滤器会误判、不能删:生产这样用:

  1. 容量估算(步进):布隆误判率公式与容量相关:预计 1 亿元素、误判率 1%,约需 ~958Mbit ≈ 120MB;若要 0.1% 则内存显著上升。插入容量越满,误判率越高,必须按 实际元素数 1.5–2 倍 预留。
  2. 用法:① 穿透防护:合法 ID 预加载,请求先过滤,误判只会放行少量不存在 ID,再由空值缓存挡住;② 去重场景(爬虫 URL):误判会漏抓(当成已存在),要评估业务能否接受。
  3. 误判与删除:标准布隆不能删;要删除可选 Counting Bloom(每格改计数器,按 4bit/格算是普通布隆(1bit/格)的 4 倍;用 8bit 计数则 8 倍)或 Cuckoo Filter(CF.DEL 支持真删除,且空间通常比 Counting Bloom 更省)。注意 Redis 的概率型结构里只有 CF.*(Cuckoo)支持 CF.DEL,BF.* 不提供删除;选型时把「是否要删」当成决定性的分叉点。
  4. 工程方案:Redis 的概率型结构(Redis 8 起 BF./CF. 已内置于 Redis Open Source,更早版本才要挂 RedisBloom/Redis Stack 模块);自建则分片布隆;定期重建(数据版本化,双 buffer 滚动切换)。
  5. 失败与降级:布隆异常 → 降级为空值缓存+限流;重建期间双布隆并行;监控误判率与填充率,填充过高提前扩。

【原理溯源】

  • 误判方向为什么「安全」? 标准布隆只有假阳性(不存在→可能判存在),没有假阴性(存在→不会判不存在)。假阳性后果 = 多一次 DB 查询;假阴性后果 = 合法请求被拒,业务错误。所以工程上把「不存在」方向当成可优化项,把「存在」方向当成正确性底线。
  • 误判率为何随数据增多而上升? 置 1 的位越来越多,随机 k 位全为 1 的概率上升。必须按目标 n 和 p 预计算 m、k;若实际数据超过设计容量,p 会迅速恶化——布隆过滤器对超容量非常敏感。
  • 为什么标准布隆不能删? 一位被多个 key 共享。清 0 可能抹掉其他 key 的存在证据 → 引入假阴性。这是正确性红线,所以宁可不可删。
  • 计数布隆的代价: 每位变计数器(如 4 bit),空间涨 4 倍+;计数回绕仍可能出错。用空间换可删。
  • 布谷鸟过滤器的机制差异: 每个元素有两个候选位置,插入时若冲突则「踢出」已有元素到其另一候选位。支持删除、空间往往更省、查询更快;但插入踢出有上限,接近满时可能插入失败,需扩容或重建。
  • 为什么生产仍要空值缓存? 布隆放行后 DB 可能仍无此数据(误判或真实不存在)。空值缓存把这次穿透也吸收掉,避免「误判导致的重复打 DB」。

【选型判断树】

text
过滤器选型:
├─ 数据几乎只增不删(商品库、用户库)
│   └─ 标准布隆 + 定期重建
├─ 需要删除(会下架、会注销)
│   ├─ 删除不频繁 → 计数布隆 或 定期重建
│   └─ 删除频繁且要更省空间 → 布谷鸟过滤器
├─ 数据量不确定/会长期超设计
│   └─ 容量会增长的布隆(Redis 8 起 BF.* 已内置;它是单机结构,跨机靠分片)或分层
└─ 任何情况
    └─ 空值缓存兜底误判

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

时间任务内容
0:00–0:30定性「两个缺陷:误判只在存在方向;标准版不可删」
0:30–1:40误判后果=多一次 DB;可接受;用 m/k 控制
1:40–3:00删除为何不能删;计数布隆;布谷鸟
3:00–4:10工程定期重建、可扩容/分片布隆(Redis 8 起 BF.* 内置,跨机靠分片)、空值兜底
4:10–5:00收尾「按增删模式选变体,用空值吃掉误判」

【关键数字】

参数经验值说明
误判率目标0.1%–1%再低空间陡增
m 估算m=-n·ln(p)/(ln2)²设计容量 n
k 估算k=(m/n)·ln2通常 5–10
重建周期小时–天级视删除频率
计数布隆空间≈ 标准的 4–8 倍4bit 计数
布谷鸟装载因子通常 < 80%–90%过高插入失败

【追问链】(三层)

L1|「误判会让用户看到错误商品吗?」 → 不会。误判只放行一次查询,DB 无数据则仍返回空。错误展示来自「缓存了错误值」,不是布隆误判。

L2|「数据删了布隆怎么办?」 → 标准布隆重建;计数布隆减计数;布谷鸟直接删除。生产常见是「每日全量重建 + 双过滤器原子切换」。

L3|「重建过滤器期间请求怎么办?」 → 新旧双过滤器并行:重建用新位数组,完成后原子替换指针;期间旧过滤器继续服务。避免「删了再建」的空窗导致全量穿透。

【评分标准】

档位答案特征
60 分知道误判和不可删
80 分说清误判方向与后果;给出计数/布谷鸟/重建
95 分讲清为何不能删(假阴性红线);容量设计与超容风险;双过滤器切换与空值兜底

【关联题】

【自测】

  1. 布隆过滤器误判的方向性是什么? 参考答案:只会把不存在误判为存在(假阳性),不会把存在误判为不存在。
  2. 为什么需要删除时优先考虑布谷鸟而不是标准布隆? 参考答案:标准布隆清位会引入假阴性;布谷鸟原生支持删除且空间往往更优。
  3. 超过设计容量会怎样? 参考答案:置位变满,误判率快速上升。必须按 n、p 预留容量或使用可扩容实现。

45. 同一份数据,放进程内还是放 Redis(缓存位置选型) ​

【考察内容】缓存选型判断力

【题目】同样是缓存,有的数据适合放应用进程内(Caffeine),有的必须放 Redis。给下面的数据分类并说明理由:商品详情、用户 Session、库存、排行榜、系统字典配置、热点新闻。各自放哪一层?为什么?

【参考答案】数据放进程内还是 Redis,看共享范围 × 一致性 × 失效成本:

  1. 容量估算(步进):假设热点数据集 2GB、读 QPS 10 万。若 50 个实例都放本地全量,内存 50×2GB=100GB,成本高但 RT 最好(<1ms);放 Redis 一份即可,但每次读增加网络往返(同机房 0.1–1ms 量级,含序列化与应用开销常到 1–2ms)且 Redis 要扛 10 万 QPS(需集群/多副本)。
  2. 选本地缓存:数据只读或极低变更、可接受秒级陈旧、热点集中(配置、字典、热点商品基础信息)、实例数×本地内存可控。
  3. 选 Redis:多实例必须看到同一份状态(购物车、session、分布式锁、排行榜)、数据量超过单机内存、需要集中限流与原子运算。
  4. 常见组合:本地缓存 + Redis 多级:本地挡 30%–70% 读,Redis 挡大部分剩余,DB 只回源少量;变更走失效广播。
  5. 失败与降级:本地缓存陈旧 → 短 TTL+版本号;Redis 挂 → 本地缓存延长 TTL 兜底展示,关键写直连 DB+限流;本地内存 OOM 风险要限额与淘汰。
  6. 口径:“谁能容忍不一致、谁来承担失效协调成本” 决定位置;状态共享优先 Redis,纯热点读优先本地。

【原理溯源】

  • 本地 vs 分布式的本质差异是「共享语义」和「一致性传播」:

    维度Caffeine 本地Redis 分布式
    延迟微秒级毫秒级(网络)
    作用域单实例集群全局
    容量受堆内存限制可水平扩展
    一致性多实例各自一份,易不一致全局一份,易管理
    故障随进程生灭需高可用
  • 为什么字典/配置适合本地? 几乎只读、变更极少、所有实例可以短暂不一致。本地缓存把最热路径的 Redis 网络 RTT 去掉;变更时广播失效或拉长本地 TTL+版本号。

  • 为什么 Session/库存/排行榜必须 Redis? 它们的正确性依赖「所有实例看到同一份数据」。放本地会出现:A 机已登录 B 机不认识;A 机扣了库存 B 机还可扣——直接正确性事故。

  • 为什么热点新闻常「本地+Redis」两级? 读 QPS 极高但一致要求低。本地挡 90%+ 流量,Redis 提供跨实例共享的中层,DB 只扛 miss。这是用「可容忍的秒级不一致」换延迟与 Redis 压力。

  • 多级缓存的一致性代价: 更新时要失效多层:先删 Redis,再广播各实例失效本地,或依赖短本地 TTL 自愈。层数越多,一致窗口越难控——所以不是所有数据都该上多级,只给极热且可容忍旧值的数据用。

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

【选型判断树】

text
缓存放哪层:
├─ 数据是否要求跨实例一致?(Session/库存/锁/计数)
│   └─ 是 → 必须 Redis(或 DB),禁止只放本地
├─ 是否极热且允许秒级旧值?(详情页/字典/热文)
│   └─ 是 → 本地 Caffeine(短 TTL) + 可选 Redis 层
├─ 数据量是否很大?
│   └─ 是 → Redis/DB,本地只缓存热点子集
└─ 变更频率
    └─ 高频变更 → 少用本地层;低频变更 → 本地+广播失效

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

时间任务内容
0:00–0:30定性「选型四维:共享性、延迟、容量、一致性」
0:30–2:20逐个分类字典/热文→本地;Session/库存/榜→Redis;详情可两级
2:20–3:30为什么共享语义决定能不能本地;热路径决定该不该本地
3:30–4:20多级代价失效传播、不一致窗口
4:20–5:00收尾「要共享上 Redis,要极致延迟且可旧才上本地」

【关键数字】

参数经验值说明
Caffeine 延迟< 1ms,通常微秒级无网络
Redis 延迟0.1–1ms 量级同机房
本地 TTL1s–5min越短越一致
本地容量建议堆的 5%–20%防 Full GC 压力
多级命中本地可挡 90%+ 热点流量极热场景
失效广播秒级或靠 TTL

【追问链】(三层)

L1|「Caffeine 和 HashMap 有什么区别?为什么要用它?」 → Caffeine 提供基于 W-TinyLFU 的高性能淘汰、过期、统计,内存可控。裸 HashMap 无淘汰会 OOM,也无 TTL。

L2|「本地缓存怎么失效?」 → ①短 TTL 自愈;②更新时消息广播各实例 invalidate;③版本号,读到旧版本则丢弃。权衡复杂度与一致要求。

L3|「为什么库存绝不能只放本地缓存?」 → 多实例各持旧库存会导致超卖——这是资金/履约级事故。库存的「全局唯一真相」必须在共享存储(Redis 预扣 + DB 事务)。

【评分标准】

档位答案特征
60 分能区分本地和 Redis 各自适用
80 分按共享性/延迟/一致性给六类数据分类并说明
95 分讲清多级缓存一致性代价;给出失效手段;强调库存类禁止纯本地

【关联题】

【自测】

  1. 系统字典配置放哪?为什么? 参考答案:本地 Caffeine 为主。只读、变更少、允许短暂不一致;可加广播失效。
  2. 用户 Session 放本地为什么不行? 参考答案:负载均衡到其他实例会丢失登录态,必须共享存储。
  3. 多级缓存最大的工程代价是什么? 参考答案:更新时的失效传播与一致窗口变大,需要广播或短 TTL。

46. 刚下单的用户刷新页面查不到订单(主从延迟) ​

【考察内容】读写分离的一致性问题

【题目】Redis 做了主从读写分离后出现新问题:用户刚加购物车,立刻刷新,购物车是空的——从库还没同步完。主从同步有延迟,怎么保证“刚写的马上能读到”?有哪些方案、各自代价是什么?

【参考答案】刚下单刷新查不到:主从/读写分离延迟问题:

  1. 容量估算(步进):假设写主库下单 1000 TPS,读打到从库。主从延迟常见 几十毫秒到几秒,极端(大事务/网络抖动)可达几十秒。用户刷新间隔往往 <1s,延迟 500ms 就会有明显“查不到”投诉。
  2. 方案:① 写后读主:加购成功后短时间窗口(1–5 秒,主库吃不住就缩到 1–2 秒)内,该用户的购物车查询强制走主库,或用 session 粘滞;② 关键查询路由:购物车、「我的」这类写完就要看的读走主库,商品列表、推荐位等只读流量走从库;③ 结果缓存写后失效:加购后主动删除该用户的购物车缓存并短暂绑定主库;④ 降低复制延迟——先分清是哪套主从:题干是 Redis 主从,Redis 没有并行复制开关、复制本身已是异步(没有「退回异步」这一档,WAIT 是显式等待命令),能做的是拆大 key、避免慢命令阻塞输出缓冲与复制积压、按 master_repl_offset 差值监控、必要时减少 replica 数或摘掉慢从库的读权重;若换成 MySQL 主从才是并行复制(replica_parallel_workers/按组提交;8.0.27 起默认就是 4,从库回放默认已并行,slave_parallel_workers 为废弃别名)、避免大事务、从库规格与网络。⑤ 各方案代价配齐:①写后读主=主库读压力上升、全员读主会退化为单点;②关键查询路由=路由逻辑复杂、易漏配;③写后缓存失效=引入缓存与 DB 的一致性成本、缓存挂时要有兜底;④降延迟=只能缩窗、消不掉竞态。
  3. 业务兜底:下单响应直接把订单号与状态返回前端,不依赖再次查从库;提示“支付/订单状态可能稍有延迟”。
  4. 失败与降级:主库压力大时不能全员读主 → 只对“写后 3 秒内 + 同用户”读主;延迟告警,超阈值自动切路由策略。
  5. 口径:这是复制延迟与路由策略问题,不是缓存 bug;用“写后读路径可控”换一致性体验。

【原理溯源】

  • 为什么「刚写的读不到」? 主从复制是异步的:主库执行写并返回客户端成功时,从库可能还没收到/执行该命令。因果链:异步复制 → 读从库可能落后 → 写后立刻读从 = 经典「读己之写」不一致。

  • 为什么不能默认「全部读主」? 那等于放弃读写分离的意义(读扩展)。要区分数据与路径:写后短窗口的同一用户会话读主,其余读从——这是「会话粘滞读主」的思想。

  • 半同步为什么缩不了这个窗口? 主库等至少一个从库收到并写入 relay log、回 ACK 后再返回(AFTER_SYNC 下不等回放),写成功的定义从「本地落盘」升级为「已被一个从库持久保存」。注意它保证的是不丢,不是从库已可读到——从库对外可见仍需回放;代价是写 RTT 增加一个网络往返,吞吐下降。

  • 为什么「写后写缓存」有补偿作用? 写路径若把最新值同时写入 Redis,读路径优先读缓存,则不依赖从库是否同步。缓存成为写读之间的「高速道」。代价是引入缓存一致性问题(删失败、过期)。

  • 和 MySQL 读写分离的同一性: 问题本质相同(异步复制 + 读从),解法同构但有分工:「读到旧值」只能靠路由(写后读主、会话粘滞、缓存中转)或干脆取消读写分离;半同步解决的是另一个问题(主库宕机丢已提交写),它 AFTER_SYNC 下不等从库回放,读从库照样可能是旧值——别拿它当本方案的解法;延迟检测摘除防的是「越慢越读」的正反馈。

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

【选型判断树】

text
写后读不到:
├─ 业务能否接受秒级旧?
│   ├─ 能 → 监控延迟 + 自然自愈(多数场景)
│   └─ 不能(订单页、支付结果、购物车)
│       ├─ 同用户短窗口 → 写后读主 / 会话内强制主库
│       ├─ 全局都要新 → 取消读写分离(统一读主);别用半同步解这题——半同步只保证主库不丢已提交写,AFTER_SYNC 下不等从库回放,读从库照样可能是旧值(见上面【原理溯源】)
│       └─ 可用缓存中转 → 写后写缓存,读缓存优先
└─ 运维
    └─ Redis 主从:监控 `master_repl_offset` 差值,超阈值摘读权重(`Seconds_Behind_Source` 是 MySQL 侧指标,别套到本题语境)

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

时间任务内容
0:00–0:30定性「根因是异步复制延迟,不是 bug」
0:30–2:00机制主写从读;复制滞后;读己之写问题
2:00–3:30方案读主窗口、写后缓存、延迟监控与摘除(半同步只用于「防丢数据」,不解决读旧值)
3:30–4:20取舍扩展 vs 一致;不是所有读都要强一致
4:20–5:00收尾「按会话/数据关键性分流读主读从」

【关键数字】

参数经验值说明
典型主从延迟常见几十毫秒~几秒;极端(大事务/网络抖动)可达几十秒告警阈值要按这个分布定,不能只按「异常才秒级」
写后读主窗口1–5 秒或会话内标记
半同步开销写延迟 +1 RTT吞吐下降
延迟告警> 1s 关注,> 5s 摘除按业务
缓存中转 TTL30s–5min写后可见性

【追问链】(三层)

L1|「怎么实现写后读主?」 → ①业务标记:本次请求/会话带「刚写」标志,DAO 路由主库;②中间件 hint(ShardingSphere 等);③按 userId 粘滞短时间。

L2|「主从延迟为什么会突然变大?」 → 大事务/大 key 同步、从库机器负载、网络抖动、RDB 重传。要监控 master_repl_offset 差值并告警。

L3|「Redis 集群模式下还有这个问题吗?」 → 有。Cluster 分片仍有主从,异步复制问题同构。业务上的「写后读主/缓存中转」策略依然适用。

【评分标准】

档位答案特征
60 分知道有主从延迟,建议读主
80 分讲清异步复制成因,给出读主窗口/写后缓存/延迟监控与摘除(能把半同步正确归位到「防丢数据」是加分)
95 分按数据关键性分流;写后缓存补偿;能联系 MySQL 读写分离的同类问题与读写路由代价

【关联题】

【自测】

  1. 「刚写的读不到」根因是什么? 参考答案:主从异步复制存在延迟,从库尚未应用该写。
  2. 写后读主的代价是什么? 参考答案:主库读压力上升,读写分离收益下降;只应短窗口/关键路径使用。
  3. 判断对错:上了读写分离就一定能提高一致性。 参考答案:错。读写分离提高的是读扩展,往往降低一致性(异步延迟)。

47. 大促前缓存要提前备好,重建时不能挤爆 DB(预热与重建风暴) ​

【考察内容】缓存生命周期管理

【题目】大促前,核心商品数据需要提前从数据库加载到缓存(预热),否则活动一开始就是缓存穿透。预热怎么做?如果缓存又过期了,多个请求同时发现缓存为空、同时回源重建数据库,怎么防止这种“重建风暴”?

【参考答案】缓存预热与重建风暴:大促前把热点数据提前装进缓存:

  1. 容量估算(步进):假设热点商品 10 万条,每条 2KB,共约 200MB,预热数据量本身不大;但若上线后 5 万 QPS 同时 miss,回源 DB 会形成风暴。预热目标:开赛前热点命中率 >95%,回源压到 <2500 QPS。若从零重建且不加限制,DB 可能在几秒内被打满。
  2. 预热方法:① 启动时批量从 DB/备份加载热点列表(按访问统计排序);② 大促前人工/任务预导;③ 灰度切流,实例先预热完成再接流量;④ 逻辑过期/互斥重建,避免热点同时 miss。
  3. 防重建风暴:同 key 只放行 1 个线程重建(互斥/singleflight),其余等待或读兜底;10–50 并发是接口维度的回源上限,不是同 key 的上限,两个闸门别混写;TTL 随机化;本地缓存先挡一层;重建任务分批限速,避免拖垮从库。
  4. 失败与降级:预热任务失败 → 不直接全量放流,保留限流并告警;预热数据与 DB 不一致 → 短 TTL + 主动失效;预热拖垮从库 → 限速加载、分批、错峰执行;开赛瞬间命中率仍低 → 临时只保核心接口。
  5. 验证:预热后压测 miss 路径;监控启动后命中率爬坡时间与 DB QPS 曲线。

【原理溯源】

  • 为什么大促必须预热? 活动流量是「已知陡增」。若冷启动,所有热点 key 第一次访问都 miss,瞬时回源 = 人造雪崩。术语口径:题干说的「缓存穿透」按第 23 题的定义其实是冷启动并发回源(击穿形态、雪崩量级)——穿透专指查本就不存在的数据,别把三个名字当同义词背。预热把 miss 成本提前、摊到低峰期。
  • 预热数据从哪来? 不能拍脑袋:①离线统计历史 TopN;②运营活动清单;③实时预测(加购、收藏)。预热对象必须是「即将到来的热点」,而不是全量。
  • 重建风暴的因果链: 热点 key 过期 → 第一秒 5 万请求同时 miss → 若无合并,5 万次查 DB → 连接池打满。与击穿同一问题,发生在「预热失效之后」尤其危险。
  • 互斥锁 / singleflight / 逻辑过期如何对症?
    • 互斥锁:跨进程,只放一个回源;
    • singleflight:进程内合并同 key 并发,适合单机热点;
    • 逻辑过期:取消失效瞬间,旧值顶住、异步重建。 三者可组合:进程内 singleflight + 跨进程 Redis 锁。
  • 为什么预热 TTL 也要随机? 同一批预热若 TTL 相同,会在同一时刻集体过期,把雪崩推迟到那一刻。base + random 仍是必做。
  • 为什么预热要分批执行? 一次性从 DB 拉全量热点会把预热本身变成 DB 尖峰。按批次+限速,避免「为了防雪崩先打出一个雪崩」。

【选型判断树】

text
预热与重建:
├─ 活动前
│   ├─ 确定热点清单 → 分批预热写 Redis
│   ├─ TTL = base + random
│   └─ 预热任务本身限速,防打挂 DB
├─ 活动中 key 过期
│   ├─ 单机并发 → singleflight
│   ├─ 多机并发 → Redis 互斥锁
│   └─ 可容忍旧值 → 逻辑过期
└─ 兜底
    └─ DB 限流 + 降级;本地缓存

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

时间任务内容
0:00–0:30定性「预热把 miss 摊到低峰;重建风暴是过期瞬间的并发回源」
0:30–2:00预热怎么做热点来源、分批限速、TTL 随机
2:00–3:30防重建风暴锁 / singleflight / 逻辑过期
3:30–4:20组合与监控进程内合并 + 跨进程锁;回源 QPS 告警
4:20–5:00收尾「生命周期:预热—运行—过期重建—降级,每段都要有主」

【关键数字】

参数经验值说明
预热提前量30 分钟–2 小时视数据量
预热批次1000–10000 key/批,间隔可控防 DB 尖峰
预热 TTL活动时长 + buffer + random覆盖活动
锁 TTL3–10s覆盖一次回源
singleflight同 key 仅 1 次回源Go 常用
回源告警回源 QPS 突增及时发现风暴

【追问链】(三层)

L1|「预热的数据源不准怎么办?」 → 预热只是降低 miss 率,不是 100% 命中。运行时仍要靠击穿/穿透防护兜底;可滚动更新热点清单。

L2|「singleflight 和 Redis 锁的区别?」 → singleflight 合并本进程内同 key 并发;Redis 锁解决跨进程。多实例部署两者常叠加。

L3|「预热写 Redis 挂了一半,活动开始会怎样?」 → 半冷启动:部分热点 miss,回源压力集中。应预热完成校验(抽样命中率)、失败重跑,并准备降级开关。

【评分标准】

档位答案特征
60 分知道提前加载热点、用锁防并发回源
80 分讲清热点来源、分批、TTL 随机;锁/逻辑过期/singleflight
95 分意识到预热自身可能打挂 DB;组合进程内+跨进程合并;有校验与降级

【关联题】

【自测】

  1. 为什么预热 TTL 也要加随机? 参考答案:避免同批 key 同时过期,造成二次雪崩。
  2. singleflight 解决什么范围的问题? 参考答案:单进程内相同 key 的并发回源合并;跨进程仍需分布式锁。
  3. 预热任务如何避免自己打挂 DB? 参考答案:分批、限速、低峰执行,必要时从离线宽表而非在线库读取。

48. Redis 内存涨到 90%,写入开始报错(内存治理) ​

【考察内容】容量治理与运维

【题目】监控显示 Redis 内存使用率持续上涨,已到 90%,部分写入开始报 OOM 错误。业务不能停,你怎么处理?先做什么、后做什么?淘汰策略、扩容、大 key 治理、数据分层怎么排优先级?

【参考答案】Redis 内存涨到 90%、写入报错:扩容与治理并行:

  1. 容量估算(步进):实例 8G,90% = 7.2G。假设日增 500MB 且无淘汰/无 TTL,不足 2 天就满。需立即判断:是真业务增长,还是过期未删/无 TTL/大 key/碎片。
  2. 紧急处理:① 开/确认 maxmemory-policy(缓存常用 allkeys-lru);② 清理过期与临时 key、UNLINK 大 key;③ 业务侧临时降级(关非必要缓存);④ 只读或限制写入保护实例——用这一步之前要先对题干「业务不能停」表个态:若「不能停」不允许拒写,就不能拿只读保实例,改按 key 前缀只停低价值写入(日志/统计类回写)、核心写走条件更新+排队,同时紧急扩内存/加从库分摊读;若允许短时只读,则只读是最快止血,但要给出时长上限与恢复判据(内存回落到安全线、evicted/slowlog 归零)。两种口径都得写,别让「保护实例」变成隐性停服务。
  3. 根因治理:补齐 TTL;拆分大 key;数据分级:热数据 Redis,冷数据 DB/SSD 存储;监控碎片率;无界增长结构(list 无 trim)要加上限。
  4. 扩容:优先水平 Cluster 分片(在线扩分片、迁移 slot,客户端需支持拓扑刷新);垂直升配只当短期止血,单机内存有上限且大内存 fork/COW 成本更高,不是正解。
  5. 失败与降级:内存满且 noeviction → 写报错,应用要捕获并降级走 DB/本地缓存;扩容过程搬迁抖动 → 低峰执行+限流;淘汰导致命中率下降 → DB 限流保护。

【原理溯源】

  • 为什么 90% 就开始出事? maxmemory 逼近后:若 noeviction,写失败;若开淘汰,则可能误踢热点。同时 RDB/AOF 重写、主从全量同步需要额外内存,高水位下极易雪上加霜。经验上应把 maxmemory 设为物理内存的 50%–75%,给 fork/COW/复制留白。
  • 为什么先分「垃圾」还是「真多」? 处置完全不同:垃圾(无 TTL、已过期未删、错误 key 前缀)应清理;真多(业务数据合法增长)应扩容或分层。误判会导致「清完又满」或「盲目扩容」。
  • 为什么清理必须 SCAN 分批? KEYS 会阻塞单线程;大批量 DEL 大 key 同样阻塞。SCAN + UNLINK 分批是安全清理的标准动作。
  • 为什么淘汰策略是应急而不是根治? 淘汰牺牲缓存命中,DB 压力上升,可能引发二级故障。它买时间,不买容量。根治仍是扩容、拆大 key、缩 TTL、冷热分层。
  • Cluster 扩容为什么能不中断? 新节点加入 → 迁移部分 slot → 客户端感知拓扑更新。单个 slot 迁移期间该槽短暂不可用,但对全局是渐进的。这是水平扩容量的正解,而不是加内存条(单机有上限且 fork 成本高)。
  • 冷热分层的经济性: Redis 内存贵。把低频数据放更便宜的介质(SSD/磁盘库/对象存储),Redis 只留热数据。用「命中率换成本」。

【选型判断树】

text
内存 90%+:
├─ 1. 应急止血
│   ├─ 确认 maxmemory-policy(缓存→allkeys-lru/lfu)
│   ├─ 拒绝非核心写入 / 降级
│   └─ 开始 SCAN 清理明显垃圾 key
├─ 2. 定性
│   ├─ 大 key 主导 → 拆分/UNLINK(第 30 题)
│   ├─ 僵尸/无 TTL 堆积 → 清理 + 规范写入
│   └─ 合法增长 → 3
└─ 3. 根治
    ├─ 数据量可预期 → Cluster 加分片
    ├─ 冷数据多 → 缩 TTL / 下沉
    └─ 监控阈值 80%/90% 常态化

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

时间任务内容
0:00–0:30定性「先止血再定性再根治;区分垃圾堆积与容量不足」
0:30–1:40应急淘汰策略、限写、分批清理
1:40–3:00定性原因大 key / 僵尸 key / 真增长
3:00–4:20根治扩容、分层、TTL 规范
4:20–5:00收尾「淘汰买时间,扩容与分层买容量」

【关键数字】

参数经验值说明
maxmemory 建议物理内存 50%–75%留 fork/复制
预警/告警80% / 90%可调
清理批次SCAN count 100–1000配 UNLINK
大 keyString>10KB,集合>5000治理
slot 迁移在线渐进Cluster 扩容
淘汰策略缓存 allkeys-lru/lfu业务数据禁止

【追问链】(三层)

L1|「能不能直接 FLUSHALL?」 → 绝对不能。会清掉全部数据,事故扩大。只清理确认无用的 key 前缀,且分批。

L2|「Cluster 扩容会不会中断服务?」 → 新节点迁 slot 时,迁移中的槽短暂停顿,整体可控。客户端要支持拓扑刷新(MOVED/ASK)。

L3|「如何防止再次冲到 90%?」 → ①容量规划与增长预测;②写入规范(必 TTL、限大小);③大 key 周期扫描;④分层与缩 TTL;⑤阈值告警与自动扩容预案。

【评分标准】

档位答案特征
60 分知道开淘汰、清 key
80 分先定性再处置;给出 SCAN/UNLINK、Cluster 扩容
95 分讲清 maxmemory 留白与 fork;淘汰只是应急;有防复发体系

【关联题】

【自测】

  1. maxmemory 为什么不要设成物理内存 100%? 参考答案:要为 fork/COW、复制缓冲、持久化重写留内存,否则易被 OOM killer 或阻塞。
  2. 清理大量 key 为什么不能用 KEYS? 参考答案:KEYS 阻塞单线程;应用 SCAN 游标分批。
  3. 淘汰策略能根治内存不足吗? 参考答案:不能,只是牺牲命中率买时间。根治靠扩容、拆分、分层与 TTL 规范。

49. 怎么主动发现缓存和数据库悄悄不一致了(一致性监控) ​

【考察内容】一致性保障体系的完整性

【题目】缓存一致性方案做得再好,线上仍可能出现“缓存和数据库悄悄不一致”(历史 bug、补偿失败)。如何设计一套主动发现不一致的监控?定时对账怎么做?发现不一致后怎么修复?

【参考答案】主动发现缓存与 DB 不一致:抽样对比 + 变更链路审计:

  1. 容量估算(步进):全量对比不现实(读 5 万 QPS 的商品逐条对 DB 会打挂库)。务实做法:按 0.1%–1% 抽样 或只对比“近 N 分钟变更过”的 key。假设变更 1000 key/分钟,抽 1% = 10 次/分钟额外读,对 DB 几乎无压力;资金/价格这类强一致字段把比例提到 5%–10%,也才 50–100 次/分钟,仍在预算内。
  2. 手段:① 定时对账任务:对比热点/资金相关字段;② 监听 binlog 与缓存删除事件是否闭环(删除失败率);③ 双读 diff:灰度少量请求同时读缓存与 DB,只打日志不给用户;④ 业务埋点:下单/改价后检查缓存版本或主动失效确认。
  3. 指标体系:不一致率、平均不一致时长、删除失败队列堆积、binlog 消费延迟、抽样差异 top key。
  4. 失败与降级:发现批量不一致 → 触发缓存批量失效/重建(限速执行);对账任务本身要错峰限流,避免反向伤害 DB;强一致业务以 DB 为准并告警人工介入;对账任务挂了要有备用定时与告警,不能静默失效。
  5. 口径:一致性监控不是“全量实时”,而是高价值字段、变更窗口、可闭环的删除链路;先保证资金与交易类,再覆盖展示类。

【原理溯源】

  • 为什么「方案做得好」仍要监控? 分布式下补偿可能失败、代码可能漏分支、运维可能误操作。一致性方案降低概率,监控把残余概率变成可发现事件。没有监控的一致性是「希望它一致」。

  • 对账为什么是抽样而不是总全量? 全量比对 DB 与 Redis 成本高(遍历 key、查 DB),可能本身打挂系统。抽样+热点优先+低峰执行,在发现能力与成本间平衡。

  • binlog 比对为什么一石二鸟? 每一次 DB 变更都应对应一次缓存删除/更新。订阅 binlog 后:既可校验「删没删」,也可在缺失时补删。它以 DB 为真相源,覆盖应用漏发删除的场景。

  • 为什么「删除失败」是最危险信号? 成功删缓存,下一次读会回填新值;删除失败则旧值长期驻留。所以删除失败率是领先指标,应在发生时就告警,而不是等对账发现结果。

  • 版本号为何优于裸值比对? 裸值可能因序列化格式、字段顺序、默认值不等价而误报。版本单调递增,比对简单可靠:缓存版本 < DB 版本即不一致。

  • 修复原则为什么是「以 DB 为准」? 缓存是衍生数据,DB 是系统记录。修复动作应是「删缓存让其重建」或「写入 DB 的新值」,绝不能反过来用缓存覆盖 DB(除非缓存是唯一写入口且有版本证明)。

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

【选型判断树】

text
一致性监控:
├─ 写路径可观测吗?
│   └─ 先打点:更新 DB / 删缓存 成功失败率(最低成本)
├─ 是否有 Canal/binlog?
│   ├─ 有 → 事件驱动比对+补删(推荐主通道)
│   └─ 无 → 定时抽样对账
├─ key 是否巨大?
│   └─ 热点优先抽样,冷数据低频
└─ 发现不一致
    └─ 删缓存 / 回填 DB 值;短 TTL;修 bug

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

时间任务内容
0:00–0:30定性「方案降概率,监控抓残余;DB 是真相源」
0:30–2:00发现手段删除失败打点、抽样对账、binlog 比对、版本号
2:00–3:20为什么这样设计全量太贵;删除失败是领先指标
3:20–4:20修复以 DB 为准,删或回填;TTL 自愈
4:20–5:00收尾「一致性 = 正确方案 + 补偿 + 监控对账」

【关键数字】

参数经验值说明
抽样比例每轮变更 key 的 0.1%–1%(资金类 5%–10%)基准是每分钟变更 key 数(第 1 条例:1000/分钟×1%=10 次/分钟),换成按天累计要重算
对账时段低峰(凌晨)减少业务影响
热点对账频率分钟–小时级价格类更勤
删除失败告警> 0 即关注,突增告警领先指标
binlog 延迟秒内比对通道
不一致修复 SLA分钟级(缓存删后自愈)加 TTL

【追问链】(三层)

L1|「怎么发现不一致?」 → ①删缓存失败监控;②抽样对比 DB 与缓存(含版本);③binlog 驱动校验;④用户反馈只作最后一道。

L2|「全量对账会不会压力大?」 → 会。应抽样、分片、低峰执行;热点 key 提高频率,冷 key 降低。必要时只比对「最近变更的 key」。

L3|「对账发现不一致,以谁为准?为什么不能用缓存修 DB?」 → 以 DB 为准。缓存是副本,可能被错误回填;除非架构上缓存是唯一写入点且带权威版本,否则禁止缓存反向覆盖 DB。

【评分标准】

档位答案特征
60 分知道要对账、用户反馈不算数
80 分给出抽样对账与 binlog;修复以 DB 为准
95 分删除失败作为领先指标;版本号比对;成本控制(抽样/热点优先);与写路径监控闭环

【关联题】

【自测】

  1. 最容易导致缓存不一致的失败点是什么? 参考答案:更新 DB 成功后删除缓存失败。应单独监控删除失败率。
  2. 为什么对账要抽样? 参考答案:全量遍历与比对成本过高,可能影响业务;抽样+热点优先更经济。
  3. 发现不一致后的标准修复动作? 参考答案:以 DB 为准删除缓存(或回填新值),依赖短 TTL 与下次读自愈;同时修根因。

50. 所有业务共用一套缓存组件,内置三防(通用缓存组件设计) ​

【考察内容】通用缓存组件设计(缓存三防的工程落地)

【题目】公司要求封装一个通用缓存组件给所有业务方用(屏蔽 Redis 细节),并且必须内置防穿透、防击穿、防雪崩能力,业务方不用自己处理。你如何设计这个组件的 API 和内部机制?不同业务对一致性要求不同,怎么兼顾?

【参考答案】通用缓存组件内置“三防”(穿透/击穿/雪崩),让业务少踩坑:

  1. 容量估算(步进):组件服务数百接口,假设总读 20 万 QPS,命中率 95% → 回源 1 万 QPS;组件自身要保证:单 key 热点不打爆、回源有并发上限(接口维度 10–50 并发;同一 key 只放 1 个 loader——两个闸门别混写,否则 1 万个 key 同时过期时「每 key 50 并发」等于没有闸门)。组件网关自身 QPS 预算要覆盖峰值 2 倍以上。
  2. 三防内置:① 穿透:可选空值缓存 + 布隆过滤器插件,TTL 与误判策略可配置;② 击穿:热点互斥(分布式锁/单飞)+ 逻辑过期模式,重建失败返回兜底;③ 雪崩:TTL 加随机、集群探活、回源限流与熔断降级钩子(业务可注入 fallback)。
  3. 统一能力:命名规范与序列化;指标(命中率、回源 RT、重建次数、三防触发次数)自动上报;防误用(禁止大 value、必须 TTL、禁止 KEYS);热更开关与灰度。API 与策略(题干第一、二问):对外只给 get(key, loader, policy) 与 put/invalidate,业务不裸操作 Redis;「一致性要求不同」靠 policy 分档而不是组件内一刀切——强一致档不缓存或只缓存版本号、常规档删缓存+TTL、只读档容忍快照,由业务按数据性质选档。
  4. 失败与降级:组件故障不影响主流程——降级直连 DB(业务限流开关);熔断器打开时返回业务 fallback;灰度发布组件,保留旧版本回滚;布隆/本地缓存异常时自动退回空值+限流路径。
  5. 口径:好组件把“正确性默认项”做进框架,而不是每个业务自己手写一遍还容易写错;业务只关心“读这个 key”,三防是平台责任。

【原理溯源】

  • 为什么要封装组件而不是让业务各写各的? 三防逻辑容易写漏、写错、不一致。组件把「正确默认」产品化:业务只提供 key 与 loader,防护、监控、降级由平台统一。这是「用平台约束换全局正确性」。

  • get(key, loader) 为什么是核心抽象? 它把「读缓存 + miss 回源 + 回填」收成一次调用,业务不再手写 if-miss。loader 内是 DB 查询;组件在 loader 外包一层锁/空值/逻辑过期。API 面小,内部策略可换。

  • 三防如何在组件内分层?

    • 穿透:loader 返回 null 时写空值缓存(短 TTL);可选布隆前置。
    • 击穿:同 key 并发 miss 时,Redis 锁或进程内 singleflight,只跑一个 loader。
    • 雪崩:写入 TTL = base + random;热点统计自动延长 TTL。
  • 一致性要求不同时如何兼顾? 配置化而不是一刀切:

    模式适用行为
    强一致偏好价格、库存展示短 TTL、优先删缓存、写后删失败重试更激进
    最终一致详情、资讯逻辑过期、可稍长 TTL
    只读配置字典本地层 + 广播失效

    通过 CachePolicy 注解/配置切换策略,而不是分叉多套组件。

  • 为什么必须有监控与降级? 组件是全局咽喉:命中率、回源 QPS、loader 耗时、删除失败率必须可见。Redis 故障时自动降级为限流直连 DB,避免「缓存挂了业务全挂」。没有降级的组件是单点放大器。

  • 如何保证业务方真的走组件? ①代码规范与 CR;②包装官方客户端,业务依赖里不暴露裸 Jedis/Lettuce;③Redis 网络 ACL 限制来源。技术手段比口头约定可靠。

【选型判断树】

text
组件设计决策:
├─ API
│   └─ get(key, loader, policy) + put/delete;禁止业务裸操作 Redis
├─ 防穿透
│   ├─ 默认空值缓存
│   └─ 大流量不存在查询 → 打开布隆开关
├─ 防击穿
│   ├─ 强一致 → 互斥锁回源
│   └─ 高性能 → 逻辑过期 / singleflight
├─ 防雪崩
│   └─ TTL+random 必开;热点自动加长
├─ 一致性
│   └─ 写路径统一先更库后删缓存;失败重试+binlog 可插拔
└─ 运行时
    └─ 埋点、限流、降级开关、动态配置

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

时间任务内容
0:00–0:30定性「把三防和一致性做成平台默认,业务只写 loader」
0:30–2:00API 与三防get(key,loader);空值/锁/TTL 随机
2:00–3:10一致性分级policy 配置;统一先更库后删
3:10–4:10可观测与降级命中率、回源、失败率;故障直连 DB
4:10–5:00落地包装客户端;动态开关;灰度

【关键数字】

参数经验值说明
空值 TTL30s–5min组件默认可配
回源锁 TTL3–10s防击穿
TTL 随机±10%–30% 或 +0–300s防雪崩
命中率目标核心接口 > 95%低于则报警
删除失败重试3–5 次 + 异步队列一致性
降级Redis 错误率超阈值自动开保 DB

【追问链】(三层)

L1|「业务方绕过组件直接用 Redis 怎么办?」 → 依赖管理上不暴露底层客户端;网络/ACL 限制;CR 与扫描禁止裸用。最好从架构上让裸客户端不可得。

L2|「不同业务一致性要求冲突,组件会不会越改越复杂?」 → 用策略模式(policy)而不是 if 业务名。核心只提供几种标准档位;特殊业务自定义扩展点,而不是改内核。

L3|「组件自身会不会成为故障放大器?」 → 会,所以必须:超时与熔断、降级直连 DB、线程池隔离、动态关闭重逻辑(锁/布隆)、全量可观测。组件升级要灰度。

【评分标准】

档位答案特征
60 分能说出封装 get + 三防手段
80 分loader 抽象、三防内置机制、写路径一致性
95 分策略化多档一致性;监控降级;防绕过;把组件当产品设计(可观测、灰度、可关)

【关联题】

【自测】

  1. get(key, loader) 中 loader 返回 null 时组件应做什么? 参考答案:写入空值缓存(短 TTL),防止穿透;可选布隆前置。
  2. 组件如何防击穿? 参考答案:同 key 并发 miss 时用 Redis 锁或 singleflight,保证只有一个 loader 回源。
  3. Redis 整体故障时组件该怎么办? 参考答案:熔断降级为限流直连 DB(或返回降级数据),避免缓存层把业务拖死。

持续学习,持续积累。