feat: add cache-system topic with 105 questions (穿透/击穿/雪崩, 读写策略, 架构选型)
Deploy Examination / deploy (push) Successful in 5s

This commit is contained in:
2026-09-05 12:03:21 +08:00
parent 2785c25dc9
commit 0015449dcf
16 changed files with 2834 additions and 0 deletions
@@ -0,0 +1,275 @@
{
"topic": "cache-architecture",
"type": "code_reading",
"schema_version": "1.0.0",
"generated": "2026-09-05T00:00:00+08:00",
"questions": [
{
"id": "cr-001",
"type": "code_reading",
"difficulty": 2,
"tags": [
"单机缓存",
"多进程共享",
"Redis取舍",
"Caffeine",
"组件选型"
],
"question": "阅读以下函数,它根据运行环境信息判断该场景是否需要引入独立的 Redis 作为缓存层,回答子问题。",
"code": "func decide(topology Topology) string {\n if !topology.multiProcess {\n return \"L1(LocalMap/Caffeine)\" // 单进程缓存\n }\n if topology.needShared &&\n !topology.needExternalStore &&\n topology.capacityOK {\n return \"L1(LocalMap/Caffeine)\"\n }\n return \"Redis\"\n}\n\n// ①: single-process checker, 缓存可重启重查, 无 AOF/INCR 需求\nfmt.Println(decide(topology{})) // ①\n// ②: 一台机器跑 3 个进程, 各持本地副本会不一致\nfmt.Println(decide(topology{multiProcess: true})) // ②\n// ③: 单进程, 但业务依赖 INCR 原子计数与 AOF 持久化\nfmt.Println(decide(topology{multiProcess: false})) // ③",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "调用①传入的空结构体 topology 中 multiProcess 默认 false,输出是什么?",
"options": {
"A": "Redis",
"B": "L1(LocalMap/Caffeine)",
"C": "DB",
"D": "编译错误,结构体不能直接传给函数"
},
"answer": "B",
"explanation": "multiProcess 默认 false,直接命中第一个分支返回 L1(LocalMap/Caffeine)。单进程、缓存可重启重查、无特殊需求时不需要 Redis。"
},
{
"index": 2,
"type": "single_choice",
"question": "调用② 传入 multiProcess=true 但 needSharedMemory=false,代码却已指向 Redis,其根本原因是?",
"options": {
"A": "缓存需要跨进程共享,各进程持本地副本必然不一致",
"B": "因为数据量大本地内存扛不住",
"C": "因为业务需要 AOF 持久化",
"D": "因为将来必然扩展为多机集群"
},
"answer": "A",
"explanation": "一台机器跑多个进程,各进程持有本地缓存的各自副本,读到同一 key 的不同版本,必然不一致。此时本地 Caffeine/Map 不适用,需要共享缓存层(如 Redis)。核心误判是『把单机当成单进程』。"
},
{
"index": 3,
"type": "short_answer",
"question": "调用③ 传入 multiProcess=false,但业务依赖 Redis 的 INCR 原子计数与 AOF 持久化,decide 会返回什么?说明理由。",
"answer": "Redis(命中最终兜底分支)。",
"explanation": "multiProcess=false 不进第一个分支;needSharedMemory 默认 false 使第二个分支的条件不成立,最终返回 Redis。虽是单进程,但需要 Redis 的原子计数/持久化能力,本地缓存提供不了,故仍需 Redis。印证『判断三问』:取决于是否被共享、是否依赖 Redis 特性、数据量是否本地扛得住,而非单机还是多机。"
}
],
"explanation": "『单机』≠『单进程』。是否需要 Redis 取决于三问:①缓存是否需被多进程/多服务共享;②是否依赖 Redis 能力(AOF、分布式锁、INCR、ZSET、PubSub);③数据量本地内存是否扛不住。三者皆否则由本地 Caffeine/Map 充当足够;任一为是则需独立 Redis 作为共享/持久化层。",
"source": null,
"related": []
},
{
"id": "cr-002",
"type": "code_reading",
"difficulty": 4,
"tags": [
"Write-Behind",
"回写式缓存",
"AOF",
"幂等",
"异步批量落库",
"Redis持久化"
],
"question": "以下伪代码实现 Write-Behind(回写式/懒更新)缓存:客户端统一只写缓存,由缓存层异步批量落库。回答子问题。",
"code": "type BatchWriter struct {\n db DB\n buf map[string]*Entry // key -> 待写条目\n backup *BackupLog // 兜底日志(确保崩溃不丢写)\n}\n\nfunc (w *BatchWriter) Write(key, value string, now int64) {\n e := &Entry{Key: key, Value: value, Version: now}\n w.pending[key] = e\n w.backup.Append(e) // 先记兜底日志\n}\n\nfunc (w *BatchWriter) flush() {\n batch := w.drainPending()\n for _, e := range batch {\n // 幂等: 已落库的行比该条目新则跳过, 防乱序覆盖\n if e.Version < GetRowVersion(e.Key) {\n continue\n }\n db.Upsert(e.Key, e.Value, e.Version)\n w.backup.Delete(e.Key) // 落库成功即清兜底\n }\n}\n\nfunc (w *BatchWriter) recover() {\n for _, e := range w.backup.DrainAll() { // 崩溃后调备份重放\n db.Upsert(e.Key, e.Value, e.Version)\n }\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "short_answer",
"question": "Write-Behind 中为何要引入 BackupLog 并在 flush 里对 version 做幂等判断?",
"answer": "BackupLog 保证缓存崩溃/重启前的未落库写不丢失;version 幂等判断防止乱序/旧写覆盖新写。",
"explanation": "Write-Behind 三大边界之一『不丢写』与『不乱序』:缓存没有 AOF+兜底日志时崩溃即丢写,所以兜底日志是必要成分;异步批量落库时序不确定,必须带时间戳/版本号,旧版本直接跳过,防止旧写覆盖新写。"
},
{
"index": 2,
"type": "single_choice",
"question": "下列哪类业务因为最终一致与滞后可见性,绝不应该放进该 Write-Behind 链路?",
"options": {
"A": "高频可丢的用户浏览历史",
"B": "余额、库存、订单金额等强一致交易类数据",
"C": "可容忍几秒延迟的统计报表",
"D": "热点读多写少的商品详情"
},
"answer": "B",
"explanation": "Write-Behind 是最终一致:落库前直查 DB 的外部系统读到滞后数据,一旦写丢失后果严重,交易/余额/库存绝不进此链路,应回 DB 同步写。"
},
{
"index": 3,
"type": "single_choice",
"question": "若缓存层既未开 AOF 也没有 BackupLog,flush 前 power 断点会怎样?",
"options": {
"A": "不会丢,Redis 有默认持久化",
"B": "未落库写入随进程重启丢失,DB 丢失这笔写",
"C": "自动用 RDB 恢复到最新",
"D": "客户端感知延迟但数据仍在"
},
"answer": "B",
"explanation": "未开 AOF 又无兜底日志时,进程重启无法恢复缓存中未落库的 key,这批写给 DB 就永久丢失了。这就是『把缓存当唯一真源会丢写』的经典反面教训。"
}
],
"explanation": "Write-Behind(回写式缓存):客户端不碰 DB 统一走缓存,由缓存层异步批量落库。工程代价三条铁律:①不丢写——缓存必须 AOF + 兜底日志,绝不把缓存当唯一真源;②不乱序——带版本号/时间戳防旧写覆盖;③外部读旧值——只适配内部可容忍一致性的读,交易/存量/库存绝不进入。",
"source": null,
"related": []
},
{
"id": "cr-003",
"type": "code_reading",
"difficulty": 3,
"tags": [
"多级缓存",
"写失效广播",
"本地缓存",
"TTL",
"版本号"
],
"question": "以下 Go 代码是多级缓存(L1 本地 Caffeine + L2 Redis)下写入时通过失效广播维护一致性的示意。回答子问题。",
"code": "func Write(key string, val []byte, ver int64) {\n redis.Set(key, val, ver) // ① L2 更新并记录版本\n l1.InvalidateLocal(key) // ② 本进程 L1 失效\n mq.Publish(\"cache.invalid\", key) // ③ 广播给所有进程\n}\n\nfunc OnInvalid(key string) {\n l1.InvalidateLocal(key) // 收到广播的进程清本地副本\n}\n\nfunc Read(key string) []byte {\n if v, ok := l1.Get(key); ok {\n // 本地版本足够新则直接返回\n if v.ver >= Version() { return v.val }\n return l1.Put(key, redis.Get(key)) // 落后则回落 L2\n }\n return redis.Get(key)\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "short_answer",
"question": "Write() 中①②③三步的目的分别是什么?顺序能否随意调换?",
"answer": "①写 L2 让共享真值源更新;②失效本进程 L1 不读旧;③广播通知其它进程失效各自 L1 副本。顺序不可随意调换,广播必须等 L2 收完后发出,否则存在让其它进程读到旧值的窗口。",
"explanation": "多级缓存副本不一致是核心难点。①②③保证本进程读到最新、其他进程也会被通知清除旧本地值。若先广播后写 L2,会有窗口其他进程读到 L2 旧疑似。顺序有意义。"
},
{
"index": 2,
"type": "single_choice",
"question": "Read() 中 L1 命中后比对版本号目的是什么?",
"options": {
"A": "让 L1 永远返回本地值",
"B": "通过版本号判定本地副本是否过期,避免读到旧值",
"C": "让 L2 不再需要",
"D": "只是多花一次 Redis 请求"
},
"answer": "B",
"explanation": "即使有失效广播,仍有广播丢失/竞态可能留下 L1 旧副本,读时再用版本比对:本地版本满足最新则直接用,落后则回落 L2,兜底判定过期。"
},
{
"index": 3,
"type": "single_choice",
"question": "广播消息偶发丢失时,哪种本地策略能额外兜底防止长期读到旧值?",
"options": {
"A": "增本地缓存短 TTL 并配合逻辑过期",
"B": "把 TTL 设成无限大",
"C": "不做任何处理",
"D": "只用版本号即可完全防丢失"
},
"answer": "A",
"explanation": "主动广播不是绝对可靠(MQ 可能丢消息),所以我还要本地缓存设短 TTL + 逻辑过期兜底:广播丢失时因 TTL 到期强制回源,避免长期读到旧值。二者叠加使用双保险。"
}
],
"explanation": "多级缓存一致性三道防线:①写时主动失效+广播;②本地短 TTL+逻辑过期到期强制回源;③版本号/时间戳判定过期兜底读取。三者配合可把 L1 与 L2 不一致窗口压到最低。",
"source": null,
"related": []
},
{
"id": "cr-004",
"type": "code_reading",
"difficulty": 2,
"tags": [
"RDB",
"AOF",
"Redis持久化",
"配置选择",
"高可用"
],
"question": "以下是 Redis 持久化配置片段,结合读多写少、可接受重启丢几秒写的场景,回答子问题。",
"code": "# redis.conf 关键配置\n# ---- RDB 快照条件 ----\nsave 900 1 # 900 秒内 >=1 次写则快照\nsave 300 10\nsave 60 10000\n\n# ---- AOF 只改动日志 ----\nappendonly yes\nappendfsync everysec\n\naof-use-rdb-preamble no",
"language": "redis",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "读多写少、可接受重启丢最近几秒写入的场景,推荐哪组持久化?",
"options": {
"A": "只开 RDB 并把 appendonly 设为 no",
"B": "关闭持久化",
"C": "开 AOF 且 appendfsync everysec 再辅以周期 RDB",
"D": "开 AOF 且 appendfsync always"
},
"answer": "C",
"explanation": "读多写少、可容忍秒级丢失、希望尽量可恢复,推荐『AOF(everysec)+周期 RDB』:每个周期最多丢约 1 秒写,RDB 加速冷启动。appendalways 接近不丢但性能损耗大,强语义场景才用。"
},
{
"index": 2,
"type": "short_answer",
"question": "本配置开启 appendfsync everysec 且保留 save 300 10,此刻断电最坏丢多少写?为什么?",
"answer": "最多约 1 秒内的写入。",
"explanation": "appendfsync everysec 表示每秒刷盘一次,断电时最多丢失最后一秒 buffer 内的写。RDB 的 save 规则只影响快照频率与恢复速度,不决定断电丢多少。"
},
{
"index": 3,
"type": "single_choice",
"question": "同时开启 AOF 与 RDB 时,Redis 加载启动为何优先用 AOF 而非 RDB?",
"options": {
"A": "AOF 文件读取速度更快",
"B": "AOF 逐条记录写命令,能恢复 RDB 快照点之后的写,数据最全",
"C": "RDB 是二进制不可读",
"D": "开 appendonly 会自动禁用 RDB"
},
"answer": "B",
"explanation": "RDB 只是某时间点的快照,快照之后到 AOF 之间的写不会进入;AOF 逐条记录写命令,只要开启就是恢复全量,所以优先用 AOF。混合头可加速载入,正文仍以 AOF 为准。"
}
],
"explanation": "持久化选型:写不多、可容忍秒级丢失→AOF(everysec)+周期 RDB;几乎不可丢→allways 换性能;极便宜最快恢复→只 RDB。重启以 AOF 优先重放最新命令。",
"source": null,
"related": []
},
{
"id": "cr-005",
"type": "code_reading",
"difficulty": 5,
"tags": [
"多级缓存",
"降级",
"熔断",
"缓存雪崩",
"过期随机抖动",
"高可用"
],
"question": "以下 Go 伪代码实现了一个带降级与熔断的多级缓存读取函数,阅读代码找出其核心缺陷并回答子问题。",
"code": "func Get(key string) ([]byte, error) {\n if breaker.IsOpen(key) {\n return nil, errCircuitOpen // 熔断直接返回\n }\n if v, ok := l1.Get(key); ok { return v }\n if v, ok := l2.Get(key); ok { l1.Put(key, v); return v }\n v, err := db.Load(key) // L1/L2 都未命中回源 DB\n if err != nil { return nil, err }\n ttl := 3600 // 固定 TTL, 无抖动\n l2.Set(key, v, ttl)\n l1.Set(key, v, ttl)\n return v, nil\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "代码用固定 ttl=3600 且无互斥/逻辑过期,当热点 key 恰好同时过期且大量请求涌入会发生什么?",
"options": {
"A": "所有请求一起穿透到 DB(缓存雪崩击穿),DB 可能被打垮",
"B": "没有影响,L1 已把流量挡住",
"C": "Redis 会自动去重请求",
"D": "熔断会立刻返回降级结果"
},
"answer": "A",
"explanation": "固定 TTL 使所有 key 在同一时刻统一过期,热点 key 过期瞬间大量并发一并穿透到 DB,这就是典型雪崩/击穿。正确做法是热点 key 加互斥(singleflight)或逻辑过期,且 TTL 加随机抖动分散过期。"
},
{
"index": 2,
"type": "single_choice",
"question": "关于熔断分支 return nil, errCircuitOpen,下列哪项判断正确?",
"options": {
"A": "熔断后不返回任何降级数据,只返回错误,属于不够优雅的降级",
"B": "熔断逻辑完全没有用",
"C": "熔断永远不会发生因为 DB 不失败",
"D": "已经是完整可靠的降级方案"
},
"answer": "A",
"explanation": "合理降级应返回可用的兜底(如 stale 旧缓存值或本地默认值)并告警,而不是直接抛错让上层蒙 撞。此处熔断返回 nil+err,是降级不彻底的表现。"
},
{
"index": 3,
"type": "short_answer",
"question": "针对固定 TTL 雪崩、热点key击穿、熔断无降级数据三个问题,分别给出改进要点。",
"answer": "① TTL 加随机抖动(如 3600+rand(0,300)) 防固定时间集体过期;② 热点 key 加互斥锁/singleflight 或逻辑过期,避免并发重复回源;③ 熔断时返回本地旧缓存 stale 值或默认值并告警 + 逐级恢复,保障降级可用。",
"explanation": "本题三处反例对应三套治理:固定 TTL→TTL 随机抖动/逻辑过期;无互斥→击穿加单飞;裸熔断→降级兜底值+告警。综合保护 DB 与缓存,避免整体雪崩。"
}
],
"explanation": "多级缓存可靠性设计:防雪崩(随机 TTL 抖动)、防击穿(互斥锁/singleflight/逻辑过期)、降级(本地兜底值+熔断计数+告警)。本题集中在三个反例对应的修复方案。",
"source": null,
"related": []
}
]
}
@@ -0,0 +1,180 @@
{
"topic": "cache-architecture",
"type": "fill_blank",
"schema_version": "1.0.0",
"generated": "09-2026-09-05T00:00:00+08:00",
"questions": [
{
"id": "fb-001",
"type": "fill_blank",
"difficulty": 1,
"tags": [
"单机缓存"
],
"question": "判断单机应用是否需要引入 Redis 的核心依据不是'是否单机',而是缓存是否需要被______共享。",
"answer": [
"多进程/多个服务"
],
"answer_rule": "any",
"explanation": "决定要不要 Redis 的核心判断是'缓存是否需要被多进程/多能力共享'。单进程应用可用本地缓存;一旦多进程共享缓存,就必须引入 Redis 以保证一致性。常见误判是把'单机'当'单进程'。",
"source": null,
"related": []
},
{
"id": "fb-002",
"type": "fill_blank",
"difficulty": 2,
"tags": [
"多进程共享"
],
"question": "一台机器上多个进程需要共享缓存时,若各进程持本地副本会彼此不一致,此时应引入______作为独立的共享缓存层。",
"answer": [
"Redis"
],
"answer_rule": "any",
"explanation": "多个业务进程同时访问缓存时,进程内本地缓存各持副本必然不一致,应引入 Redis 这类独立进程的缓存服务作为共享层,保证所有进程看到同一份数据。",
"source": null,
"related": []
},
{
"id": "fb-003",
"type": "fill_blank",
"difficulty": 2,
"tags": [
"Redis持久化",
"AOF"
],
"question": "在进程重启后缓存不能丢失,且依赖原子自增(INCR)能力的场景下,应选择的缓存组件是______,并结合______持久化保证重启不丢。",
"answer": [
"Redis",
"AOF"
],
"answer_rule": "ordered",
"explanation": "需要跨进程共享、进程重启不丢且依赖原子计数时,应选 Redis。Redis 通过 AOF(追加日志)或 RDB(快照)持久化;其中 AOF 基于操作日志、重放后数据恢复更完整,符合'重启不丢'的诉求。",
"source": null,
"related": []
},
{
"id": "fb-004",
"type": "fill_blank",
"difficulty": 3,
"tags": [
"Write-Behind",
"回写式缓存"
],
"question": "客户端不直接访问 DB,统一读写缓存层,由缓存层异步批量写回 DB,这种架构被称为 Read-Through + ______(回写式缓存 / 懒更新缓存)。",
"answer": [
"Write-Behind"
],
"answer_rule": "any",
"explanation": "Write-Behind 也叫回写式缓存或懒更新缓存:写操作只更新缓存,由缓存层异步批量同步到 DB。它带来了异步落库、可批量合并写的优势,但也有丢写风险和外部读到旧值的代价。",
"source": null,
"related": []
},
{
"id": "fb-005",
"type": "fill_blank",
"difficulty": 3,
"tags": [
"Write-Behind"
],
"question": "Write-Behind 架构下为保证不丢写,缓存必须开启______持久化,并配套兜底日志/队列;为保证批量落库不乱序,需为数据附带______或时间戳。",
"answer": [
"AOF",
"版本号"
],
"answer_rule": "ordered",
"explanation": "Write-Behind 丢写风险高,缓存必须持久化(AOF)并有兜底日志/队列;批量落库时要带版本号或时间戳,防止旧数据覆盖新数据造成乱序。绝不能把缓存当作唯一真相源。",
"source": null,
"related": []
},
{
"id": "fb-006",
"type": "fill_blank",
"difficulty": 2,
"tags": [
"多级缓存"
],
"question": "多级缓存(L1 本地 + L2 Redis)副本不一致的常见解法之一是:本地缓存设短 TTL 结合______过期做兜底,或写时通过 MQ ______通知各节点清本地缓存。",
"answer": [
"逻辑",
"广播"
],
"answer_rule": "ordered",
"explanation": "L1 本地与 L2 Redis 副本不一致是核心难点。缓解手段包括:本地短 TTL + 逻辑过期兜底;主动失效 + MQ 广播通知所有节点清本地缓存;版本号/时间戳判定过期。",
"source": null,
"related": []
},
{
"id": "fb-007",
"type": "fill_blank",
"difficulty": 2,
"tags": [
"Caffeine",
"TinyLFU"
],
"question": "Caffeine 是进程内 JVM 内存缓存,采用______淘汰策略,单次访问纳秒级、无网络开销,适合做 L1 ______热点缓存。",
"answer": [
"TinyLFU",
"极热/高"
],
"answer_rule": "ordered",
"explanation": "Caffeine 使用 TinyLFU 淘汰策略,能更精确地识别高频热数据;作为进程内内存虽然纳秒级访问,但重启即失、无法跨进程共享,因此适合做 L1 极热/高访问层,与 Redis(L2 共享)搭配成多级缓存。",
"source": null,
"related": []
},
{
"id": "fb-008",
"type": "fill_blank",
"difficulty": 3,
"tags": [
"限流",
"熔断"
],
"question": "在缓存降级设计中,当 Redis 抖动不可用时,为防止流量全部回源打爆 DB,应引入本地缓存兜底、配合______、______等保护手段。",
"answer": [
"限流",
"熔断"
],
"answer_rule": "ordered",
"explanation": "Redis 不可用时应降级而不是无限回源。常用手段包括:本地缓存兜底、限流(限制回源速率)、熔断(快速失败保护后端)、以及返回降级数据。共同目标都是保护 DB 和后端服务免于过载。",
"source": null,
"related": []
},
{
"id": "fb-009",
"type": "fill_blank",
"difficulty": 4,
"tags": [
"哨兵",
"Redis Cluster"
],
"question": "Redis 高可用的两种组成方式:由______负责主从复制的故障检测与自动切换,由______负责数据分片以实现横向扩容。",
"answer": [
"哨兵/Sentinel",
"Redis Cluster"
],
"answer_rule": "ordered",
"explanation": "哨兵(Sentinel)部署在主从复制之上,监控、故障检测并自动提升换来主从切换,保障高可用;Redis Cluster 按 slot 分片,把数据分发到不同分片实现水平扩展。两者职责互补。",
"source": null,
"related": []
},
{
"id": "fb-010",
"type": "fill_blank",
"difficulty": 4,
"tags": [
"RDB"
],
"question": "Redis 提供两种主流持久化方式:______以周期性快照落盘,______则记录每一次写命令,二者可以同时开启。",
"answer": [
"RDB",
"AOF"
],
"answer_rule": "ordered",
"explanation": "RDB(快照)按周期把内存数据整份序列化写入磁盘,恢复快但可能丢失最后一次快照后的数据;AOF 记录每次写命令的追加日志,可重放到最近状态,数据更安全。两者不相斥,生产上可同时开启。",
"source": null,
"related": []
}
]
}
@@ -0,0 +1,56 @@
{
"slug": "cache-architecture",
"name": "缓存架构与组件选型",
"description": "单机是否需 Redis、客户端只读写缓存(Write-Behind)、多级缓存一致性、组件选型与降级高可用",
"tags": [
"AOF",
"Caffeine",
"RDB",
"Redis Cluster",
"Redis取舍",
"Redis持久化",
"TTL",
"TinyLFU",
"Write-Behind",
"写失效广播",
"单机缓存",
"哨兵",
"回写式缓存",
"多级缓存",
"多进程共享",
"幂等",
"异步批量落库",
"本地缓存",
"熔断",
"版本号",
"组件选型",
"缓存降级",
"缓存雪崩",
"过期随机抖动",
"配置选择",
"降级",
"限流",
"高可用"
],
"difficulty_range": [
1,
5
],
"schema_version": "1.0.0",
"updated": "2026-09-05",
"question_files": [
"single_choice",
"fill_blank",
"short_answer",
"code_reading"
],
"stats": {
"total": 35,
"by_type": {
"single_choice": 10,
"fill_blank": 10,
"short_answer": 10,
"code_reading": 5
}
}
}
@@ -0,0 +1,235 @@
{
"topic": "cache-architecture",
"type": "short_answer",
"schema_version": "1.0.0",
"generated": "09-2026-09-05T00:00:00+08:00",
"questions": [
{
"id": "sa-001",
"type": "short_answer",
"difficulty": 2,
"tags": [
"单机缓存"
],
"question": "简述判断单机环境是否需要引入 Redis 的'判断三问',并说明每问回答'是'意味着什么。",
"answer": "判断三问:①缓存是否需要被多进程/多服务共享?②是否依赖 Redis 某项能力(AOF 持久化、分布式锁、原子计数 INCR、榜单 ZSET、PubSub 等)?③数据量是否大得本地进程内内存扛不住、或将来必然扩展为多机集群?只要任一回答'是',就应当引入 Redis;只有当三问全部为'否'时,进程内本地缓存(如 Caffeine)才足够。",
"keywords": [
"多进程共享",
"Redis能力",
"数据量",
"本地缓存",
"Caffeine"
],
"scoring_rubric": "答出三问各得 20 分(共 60 分);正确说明三问全否才选本地缓存得 20 分;能指出'单机≠单进程'这一易错点得 20 分。",
"explanation": "决定是否用 Redis 的核心是缓存是否需要被多进程/多能力共享,而不是机器是否单机。三问中任一命中就说明需要 Redis 的跨进程共享或特性能力;全否则本地缓存足够,加 Redis 反而引入网络与一致性麻烦。",
"source": null,
"related": []
},
{
"id": "sa-002",
"type": "short_answer",
"difficulty": 3,
"tags": [
"Write-Behind",
"回写式缓存"
],
"question": "请说明 Write-Behind(回写式/懒更新缓存)架构的运作方式,以及使用它需要注意哪几类代价或边界。",
"answer": "运作方式:客户端完全不直连 DB,统一读写缓存层;写操作只更新缓存并返回,由缓存层异步批量把写入同步回 DB(Read-Through + Write-Behind)。注意事项:①不丢写——缓存必须开启持久化(AOF)并配套兜底日志/队列,绝不可把缓存当唯一真源;②不乱序——批量落库需带时间戳/版本号防止旧覆盖新;③外部读到旧值——直查 DB 的外部/报表系统会看到滞后数据,因此只适合内部读、可容忍一致的数据,而交易/余额/库存等强一致数据绝不可进入该链路。",
"keywords": [
"异步落库",
"持久化",
"AOF",
"版本号",
"旧值",
"强一致"
],
"scoring_rubric": "正确描述异步落库机制得 30 分;说出缓存需持久化兜底不丢写得 25 分;说出版本号/时间戳防乱序得 20 分;指出外部读旧值风险并排除强一致数据得 25 分。",
"explanation": "Write-Behind 的优势是写异步化、可批量合并、降低 DB 写压力;代价是丢写风险、乱序风险和读滞后。本题考察对整个架构边界(可靠性、一致性、适用数据)的把握,是必考点。",
"source": null,
"related": []
},
{
"id": "sa-003",
"type": "short_answer",
"difficulty": 3,
"tags": [
"多级缓存"
],
"question": "多级缓存(L1 本地 + L2 Redis)副本不一致是核心难点,请给出至少三种缓解不一致的治理手段。",
"answer": "①本地缓存设置短 TTL + 逻辑过期兜底——即使没收到失效通知,本地数据也会在较短时间内自然过期回源,缩小不一致窗口;②主动失效 + 广播——写入时通过 MQ 发送失效事件,通知所有节点清除本地缓存副本;③版本号/时间戳判定过期——缓存项携带版本号或写入时间戳,读时对比 L2 判定本地副本是否已过期。",
"keywords": [
"短TTL",
"逻辑过期",
"广播",
"MQ",
"版本号",
"失效"
],
"scoring_rubric": "每种治理手段计约 33 分(共约 100 分)。答出短 TTL/逻辑过期得 33 分;答出 MQ 广播主动失效得 33 分;答出版本号/时间戳判过期得 34 分。",
"explanation": "多级缓存的根本矛盾是同一数据多个副本。思路分两条线:缩短过期窗口(短 TTL + 逻辑过期),和主动同步失效(广播 + 版本判定)。三者属于缓存一致性的经典治理手段。",
"source": null,
"related": []
},
{
"id": "sa-004",
"type": "short_answer",
"difficulty": 2,
"tags": [
"Caffeine",
"TinyLFU",
"多级缓存"
],
"question": "对比 Caffeine 与 Redis 两种缓存组件,说明各自适用场景,在多级缓存中各自适合承担哪一层。",
"answer": "Caffeine 是进程内 JVM 内存、TinyLFU 淘汰、纳秒级无网络单机访问,适合做 L1 极热/高访问层;但重启即失、无法跨进程共享。Redis 是独立进程、可跨机器,毫秒网络 I/O、支持持久化、分布式锁/原子计数/榜单/PubSub,适合做 L2 共享与兜底层。多级缓存中 Caffeine 放 L1 承接极端热点、Redis 放 L2 跨进程共享,两者组合兼顾极热性能与一致性。",
"keywords": [
"Caffeine",
"TinyLFU",
"L1",
"Redis",
"L2",
"多级缓存"
],
"scoring_rubric": "Caffeine 特点与 L1 定位各得 20 分;Redis 特点与 L2 定位各得 20 分;正确说明二者组合成多级缓存得 20 分。",
"explanation": "两者的本质差异是'进程内内存' vs '独立共享服务'。多级缓存正是发挥二者长处:L1 用 Caffeine 承接极热访问减网络开销,L2 用 Redis 保证跨进程一致与可靠性。",
"source": null,
"related": []
},
{
"id": "sa-005",
"type": "short_answer",
"difficulty": 4,
"tags": [
"缓存降级",
"限流",
"熔断"
],
"question": "在缓存架构的可靠性设计中,请说明限流、熔断、降级三种手段各自的职责,以及它们如何共同保护 DB 和后端服务。",
"answer": "限流:限制单位时间回源/请求的数量,防止突发流量瞬间打满 DB 连接或线程池;熔断:当后端(DB/缓存)连续失败达到阈值时快速失败而非继续请求,让其有喘息恢复时间;降级:当 Redis 等组件不可用时,返回兜底数据(如本地缓存副本、默认值、静态页)而不是无限回源。三者配合:Redis 抖动时先本地兜底降级 + 限流控制回源 + 熔断保护 DB,避免全量打爆后端。",
"keywords": [
"限流",
"熔断",
"降级",
"兜底",
"保护DB"
],
"scoring_rubric": "正确说明限流职责得 25 分;熔断职责得 25 分;降级兜底职责得 30 分;能整合说明三者协作保护 DB 得 20 分。",
"explanation": "可靠性设计不止是缓存本身高可用,更重要的是对下游的保护。限流控制流量、熔断快速失败、降级提供兜底,三者共同避免缓存故障演变为 DB 过载故障。",
"source": null,
"related": []
},
{
"id": "sa-006",
"type": "short_answer",
"difficulty": 3,
"tags": [
"Redis持久化",
"RDB",
"AOF"
],
"question": "请对比 Redis 的 RDB 快照与 AOF 日志两种持久化方式,说明各自在数据安全性与恢复方式上的特点,并给出选型建议。",
"answer": "RDB 按周期把内存数据整份快照序列化落盘,文件紧凑、恢复快,但两次快照之间的写入可能丢失,安全性较弱。AOF 记录每次写命令的追加日志,可重放到最近状态,数据更安全,但文件较大、在重写(bgrewriteaof)影响恢复速度/性能。选型:若对安全性要求高(如缓存作为准真源、写后异步落库)选 AOF 或两者同时开启、仅接受少量丢失选 RDB;一般高要求生产推荐同时开启 AOF 以保数据。",
"keywords": [
"RDB",
"AOF",
"快照",
"重放",
"持久化",
"选型"
],
"scoring_rubric": "正确说明 RDB 快照特点与丢失窗口得 30 分;正确说明 AOF 追加日志与重放得 30 分;给出符合场景的选型建议得 40 分。",
"explanation": "RDB 是周期性快照(紧凑、恢复快、可能丢最后窗口数据),AOF 是命令日志(数据更安全、可重放)。持久化选型属于缓存组件选型的核心点,尤其结合 Write-Behind 的落库需求更强调 AOF。",
"source": null,
"related": []
},
{
"id": "sa-007",
"type": "short_answer",
"difficulty": 2,
"tags": [
"单机缓存",
"多进程共享"
],
"question": "为什么说'单机'不等于'单进程',并基于此说明一台单机部署在手机多进程时为何仍需 Redis。",
"answer": "'单机'描述的是部署物理维度(一台机器),而'单进程'描述的是运行进程维度(应用进程个数)。决定是否需要 Redis 的是缓存是否被多进程/多能力共享:即使在同一台机器上运行多个业务进程,每个进程持本地副本会各自维护一份缓存而产生数据不一致;此时仍需要引入 Redis 作为跨进程的共享缓存层,保证所有进程读到同一份数据。因此'单机用本地缓存即可'的常见误判,本质是把'单机'误当成了'单进程'。",
"keywords": [
"单机",
"单进程",
"多进程",
"共享",
"不一致"
],
"scoring_rubric": "正确解释单机与单进程的维度区别得 40 分;指出多进程各自持副本不一致得 30 分;正确结论是仍需 Redis 做共享层得 30 分。",
"explanation": "该题是缓存选型中最常见的误判点。判断依据是'缓存是否需要被共享',与机器台数无关。只要多进程共享就需 Redis。",
"source": null,
"related": []
},
{
"id": "sa-008",
"type": "short_answer",
"difficulty": 4,
"tags": [
"哨兵",
"Redis Cluster"
],
"question": "区分哨兵(Sentinel)与 Redis Cluster 在体系结构中的职责,并说明二者是否可以同时使用及其各自解决的问题。",
"answer": "哨兵负责高可用:部署在主从复制之上,监控主从状态,在主库故障时自动发起投票并提升上手从库为主库,实现自动接管,不负责数据分片;Redis Cluster 负责横向扩展:按 hash slot(16384 个槽)把键分布到多个分片节点,支持大容量与更高吞吐,具有分片能力。两者职责不同可同时使用——在 Cluster 之上叠加基于哨兵的监控/运维切换,或对不同的独立 Redis 实例分别用哨兵保障。哨兵解决'主库挂了怎么办',Cluster 解决'单实例容量/吞吐不够怎么扩展'。",
"keywords": [
"哨兵",
"Sentinel",
"集群",
"Cluster",
"分片",
"自动切换"
],
"scoring_rubric": "正确说明哨兵负责故障检测与自动切换得 35 分;正确说明 Cluster 负责分片/横向扩容得 35 分;说明两个职责互补、可同时使用得 30 分。",
"explanation": "这是选型中易混淆的一对:哨兵是'高可用'组件(故障切换),Cluster 是'分片扩展'组件(横向扩容)。职责不同,可搭配使用。",
"source": null,
"related": []
},
{
"id": "sa-009",
"type": "short_answer",
"difficulty": 3,
"tags": [
"Write-Behind",
"回写式缓存"
],
"question": "Write-Behind 架构中,为什么'绝不能把缓存当作唯一真源'?请从一两个典型故障说明后果与应采取的兜底措施。",
"answer": "因为 Write-Behind 下数据先写缓存、异步落库,若缓存作为唯一真源而该缓存未持久化或崩溃,则尚未同步到 DB 的写入会直接丢失;同时如果只有一套副本,一旦重启清空则无法恢复任何待落库数据。典型故障:缓存进程崩溃且未开 AOF,队列中积压的上万笔写入全部丢失;或落库逻辑持续失败导致缓存与 DB 长期分歧。兜底措施:缓存开启持久化(AOF)+ 独立的可靠的落库日志/消息队列兜底,保证每比写入都被可靠记录并可重放;配合版本号防乱序、System对账重试。",
"keywords": [
"丢写",
"崩溃",
"持久化",
"AOF",
"消息队列",
"兜底"
],
"scoring_rubric": "指出缓存作为唯一真源会有崩溃丢写风险得 30 分;给出具体的故障场景(崩溃/清空/重放)得 30 分;提出持久化 + 队列兜底/对账措施得 40 分。",
"explanation": "Write-Behind 的可靠性核心是不把缓存当唯一真相源:必须要有持久化 + 兜底队列,把写入先可靠记下来再异步同步,避免缓存崩溃丢真源。",
"source": null,
"related": []
},
{
"id": "sa-010",
"type": "short_answer",
"difficulty": 5,
"tags": [
"多级缓存",
"Caffeine"
],
"question": "设计一个多级缓存的一致性读写如何做具体过程:热点数据写入时,如何保证 L1 本地、L2 Redis 与 DB 三方的新旧一致?",
"answer": "可采用的完整流程:写时先更新 DB,写成功后向 L2 Redis 写新值并发出失效事件/版本号;通过 MQ 把版本号/失效通知直接发给所有节点的 L1 本地缓存,各节点收到后删除对应本地 key(主动失效);本地 key 同时设置短 TTL + 逻辑过期作为兜底,即使广播丢失也会在短 TTL / 逻辑过期时触发校验回源,读时携带版本号判断过期;对外暴露时返回已确认一致的值。排列组合:更新 DB → 更新或失效 Redis → 广播失效本地 → 本地短 TTL 逻辑过期兜底 + 版本号判定,最终保证 DB 为真源、L2 与 L1 为可作废副本,缩短不一致窗口。",
"keywords": [
"RIO广播",
"失效",
"短TTL",
"版本号",
"DB真源"
],
"scoring_rubric": "答出 DB 为真源、写库先行得 20 分;答出 L2 更新/失效再加版本号得 20 分;答出 MQ 广播失效各节点 L1 得 25 分;答出短 TTL + 逻辑过期兜底得 15 分;答出版本号校验过期得 20 分。",
"explanation": "多级缓存一致性的核心是“DB 真源 + 主动失效 + 过期兜底”。题目考察对读写路径和一致性窗口最小化的全套设计理解,属于本子主题较难的综合题。",
"source": null,
"related": []
}
]
}
@@ -0,0 +1,204 @@
{
"topic": "cache-architecture",
"type": "single_choice",
"schema_version": "1.0.0",
"generated": "09-2026-09-05T00:00:00+08:00",
"questions": [
{
"id": "sc-001",
"type": "single_choice",
"difficulty": 1,
"tags": [
"单机缓存"
],
"question": "一台机器上运行着多个独立的业务进程,各进程需要共享同一份热点数据缓存。此时关于是否需要引入 Redis,下列说法正确的是?",
"options": {
"A": "因为部署在一台机器上,用进程内 Caffeine 即可,无需 Redis",
"B": "只要数据能被重启后重新加载,就绝不该用 Redis",
"C": "缓存需要跨进程共享,进程内本地缓存会各自持有副本导致不一致,应引入 Redis",
"D": "单机场景下 Redis 会因为网络开销绝对劣于本地缓存"
},
"answer": "C",
"explanation": "决定是否用 Redis 的关键不是'单机'而是'缓存是否需要被多进程/多能力共享'。多进程部署时每个进程各持一份本地缓存的副本会彼此不一致,因此需要引入 Redis 作为共享缓存层。单机不等于单进程,A 把'单机'误当'单进程'。B 忽略了跨进程共享的需求。D 中 Redis 在网络开销上高于本地缓存,但在跨进程共享场景下是必要的,不能说'绝对劣于'。",
"source": null,
"related": []
},
{
"id": "sc-002",
"type": "single_choice",
"difficulty": 2,
"tags": [
"单机缓存"
],
"question": "一个单进程应用,缓存数据可以随时重启后从 DB 重新加载,也不依赖任何 Redis 特性。下列最合理的做法是?",
"options": {
"A": "接入 Redis,保证缓存能被其他进程共享",
"B": "直接使用进程内本地缓存(如 Caffeine),不加 Redis",
"C": "必须同时使用本地缓存和 Redis 两层缓存",
"D": "放弃缓存,所有请求直连 DB"
},
"answer": "B",
"explanation": "判断三问:①缓存是否被多进程共享?否;②依赖 Redis 某项能力(可持久化、分布式锁、原子计数等)?否;③数据量本地扛不住?否。三条都'否',说明本地缓存足够,加 Redis 反而引入一层网络与一致性麻烦,属于多余。因此直接用进程内本地缓存即可。",
"source": null,
"related": []
},
{
"id": "sc-003",
"type": "single_choice",
"difficulty": 3,
"tags": [
"多进程共享",
"Redis持久化"
],
"question": "某系统需要:缓存跨多个进程共享、进程重启后缓存不丢失、支持分布式锁与原子自增。针对该需求,下列说法正确的是?",
"options": {
"A": "用进程内 Caffeine 即可满足全部需求",
"B": "必须引入 Redis,因为它支持共享、AOF 持久化、分布式锁和原子计数",
"C": "只要本地缓存设置足够大的容量就能解决",
"D": "跨进程共享和持久化互斥,无法同时满足"
},
"answer": "B",
"explanation": "该需求命中判断三问中的①②:需要跨进程共享、且依赖 Redis 特性。Redis 独立于进程、支持 AOF/RDB 持久化,原生支持分布式锁(SETNX/Redlock)、原子计数(INCR)、榜单(ZSET)、PubSub,因此必须引入 Redis。Caffeine 是进程内内存,无法跨进程共享、无法在进程崩溃后保留数据,故 A、C 错误。共享与持久化并不互斥,D 错误。",
"source": null,
"related": []
},
{
"id": "sc-004",
"type": "single_choice",
"difficulty": 2,
"tags": [
"多级缓存"
],
"question": "在多级缓存架构中,客户端完全不直接访问 DB,统一通过缓存层读写,并由缓存层异步地把写入批量同步回 DB。这种架构被称为?",
"options": {
"A": "Cache-Aside(旁路缓存)",
"B": "Read-Through + Write-Behind(回写式缓存)",
"C": "Cache-Aside + 同步写库",
"D": "直写缓存(Write-Through)"
},
"answer": "B",
"explanation": "Read-Through 指程序只读缓存,缓存缺失时由缓存层负责回源加载;Write-Behind(回写式/懒更新缓存)指写操作只写入缓存,由缓存层异步批量写回 DB。两者结合即为'客户端只读写缓存+异步落库'的架构。Write-Through 是同步写入库,与题目'异步批量'不符。",
"source": null,
"related": []
},
{
"id": "sc-005",
"type": "single_choice",
"difficulty": 3,
"tags": [
"Write-Behind",
"回写式缓存"
],
"question": "采用 Write-Behind 回写式缓存架构时,最终哪类数据绝不应进入该链路?",
"options": {
"A": "可容忍短暂滞后的报表统计数据",
"B": "仅供内部读、允许弱一致的数据",
"C": "交易、余额、库存等强一致且不可丢失的业务数据",
"D": "热点读多写少的页面配置数据"
},
"answer": "C",
"explanation": "Write-Behind 存在两个天然代价:外部直查 DB 的系统会读到滞后旧值,以及若缓存崩溃且未持久化可能丢写。因此该架构只适合内部读、可容忍一致性的数据;而交易、余额、库存等强一致、不可丢失的核心业务数据绝不能进入此链路,必须走强一致方案。",
"source": null,
"related": []
},
{
"id": "sc-006",
"type": "single_choice",
"difficulty": 4,
"tags": [
"多级缓存"
],
"question": "针对多级缓存(L1 本地 + L2 Redis)副本不一致的问题,下列哪项方案不能从根本上缓解不一致?",
"options": {
"A": "本地缓存设置较短 TTL 并配合逻辑过期兜底",
"B": "写入时通过 MQ 广播失效事件,通知所有节点清除本地缓存",
"C": "为缓存条目附带版本号/时间戳,读时判定过期",
"D": "把本地缓存 TTL 无限拉长以减少回源次数"
},
"answer": "D",
"explanation": "加剧不一致的做法是把本地 TTL 无限拉长——本地副本长时间不过期,L2 已更新时本地仍返回旧值,反而放大不一致。正确治理是:短 TTL+逻辑过期兜底、主动失效并广播、版本号/时间戳判定过期。A、B、C 都是缓解一致性的正确手段。",
"source": null,
"related": []
},
{
"id": "sc-007",
"type": "single_choice",
"difficulty": 3,
"tags": [
"Caffeine",
"TinyLFU"
],
"question": "关于 Caffeine 与 Redis 的对比,下列说法正确的是?",
"options": {
"A": "Caffeine 是独立进程可跨机器共享,访问无需网络",
"B": "Redis 是进程内 JVM 内存,重启后即失",
"C": "Caffeine 采用 TinyLFU 淘汰策略,纳秒级访问、适合做 L1 极热点缓存,但重启即失",
"D": "Redis 只能做本地内存,不支持持久化和分布式锁"
},
"answer": "C",
"explanation": "Caffeine 是进程内 JVM 内存,采用 TinyLFU 淘汰,单元访问纳秒级、无网络开销,适合做 L1 极热点层,但重启即失、无法跨进程共享。Redis 是独立进程、可跨机器,毫秒级网络 I/O,支持持久化、分布式锁/原子计数/榜单/PubSub,是 L2 共享层。A、B、D 的描述正好反了。",
"source": null,
"related": []
},
{
"id": "sc-008",
"type": "single_choice",
"difficulty": 2,
"tags": [
"多级缓存"
],
"question": "若业务场景为'读多写少、数据不强一致、希望提升极端热点访问性能',最合适的缓存方案是?",
"options": {
"A": "放弃缓存,全部走 DB",
"B": "引入多级缓存:进程内 Caffeine 作 L1 极热层,Redis 作 L2 共享层",
"C": "只用 DB 并加大连接池压力",
"D": "直接把 DB 当作缓存使用"
},
"answer": "B",
"explanation": "读多写少、不强一致、极端热点适合多级缓存:本地 Caffeine 承担 L1 极热高频访问(纳秒级,无网络),Redis 作为 L2 跨进程共享与兜底层。该组合在热点场景显著降低回源。该场景不需要强一致,无需回 DB。",
"source": null,
"related": []
},
{
"id": "sc-009",
"type": "single_choice",
"difficulty": 4,
"tags": [
"缓存降级",
"限流",
"熔断"
],
"question": "关于缓存架构的可靠性设计,下列说法错误的是?",
"options": {
"A": "Redis 可通过主从复制 + 哨兵表决实现高可用",
"B": "限流、熔断、降级主要用于保护 DB 和后端服务免于过载",
"C": "必要的 TTL 加上随机抖动可以有效防止缓存雪崩",
"D": "当 Redis 抖动或不可用时,应把流量无限回源 DB,不做任何降级"
},
"answer": "D",
"explanation": "可靠性设计的另一面恰恰是降级与兜底:当 Redis 不可用时,直接全量回源 DB 可能瞬间打爆 DB,应通过本地缓存兜底、限流/熔断、返回降级数据等方式保护后端。A、B、C 均是正确描述,D 忽略了降级策略,是错误的说法。",
"source": null,
"related": []
},
{
"id": "sc-010",
"type": "single_choice",
"difficulty": 5,
"tags": [
"Redis Cluster",
"哨兵"
],
"question": "关于 Redis 组成的选型,下列说法正确的是?",
"options": {
"A": "哨兵(Sentinel)负责数据分片扩容,用于水平扩展写容量",
"B": "Redis Cluster 负责故障检测与主从自动切换,不具备分片能力",
"C": "哨兵用于主从复制的高可用表决与故障切换,Cluster 用于多分片横向扩展",
"D": "RDB 与 AOF 只能二选一,不能同时开启"
},
"answer": "C",
"explanation": "哨兵(Sentinel)用于对主从复制架构做监控、故障检测、自动提升与切换(顾向选举),保障高可用;Redis Cluster 提供的是数据分片(slot),用于横向扩展容量与吞吐。二者职责不同。A、B 将两者职责对调了。RDB 与 AOF 可以同时开启,故 D 错误。",
"source": null,
"related": []
}
]
}