Files
examination/topics/architecture/cache-system/cache-architecture/code_reading.json
T
2026-09-05 12:03:21 +08:00

276 lines
17 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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": []
}
]
}