276 lines
17 KiB
JSON
276 lines
17 KiB
JSON
{
|
||
"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": []
|
||
}
|
||
]
|
||
}
|