{ "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": [] } ] }