{ "topic": "cache-read-write-strategy", "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": [ "Cache-Aside", "Read-Through", "Write-Through", "Write-Behind" ], "question": "分别简述 Cache-Aside、Read-Through、Write-Through、Write-Behind 四种缓存读写策略在「读」和「写」上的核心差别。", "answer": "Cache-Aside:读时 miss 后应用自行查库并回填缓存;写时先写数据库再删缓存。Read-Through:把 miss→查库→回填封装进缓存层,应用只读缓存。Write-Through:写缓存的同时同步写数据库,强一致但慢。Write-Behind/Write-Back:写请求先写缓存即返回成功,DB 由后台异步批量刷写,吞吐高但有丢失与乱序风险。", "keywords": [ "Cache-Aside", "Read-Through", "Write-Through", "Write-Behind", "写库", "删缓存", "异步" ], "scoring_rubric": "正确描述 Cache-Aside 读写流程得 30 分;Read-Through 封装 miss 回填得 15 分;Write-Through 同步写库得 15 分;Write-Behind 异步刷库及其丢失风险得 40 分。", "explanation": "四种策略可从「谁负责回填」「何时写库」两个维度区分:Cache-Aside 由应用回填、先写库再删缓存;Read-Through 由缓存层回填;Write-Through 同步写库;Write-Back 异步写库,牺牲一致性换取吞吐。", "source": null, "related": [] }, { "id": "sa-002", "type": "short_answer", "difficulty": 3, "tags": [ "Write-Behind", "Write-Back", "数据安全" ], "question": "Write-Behind 策略采用「先写缓存、后台异步落库」的方式,带来了哪些风险?为什么交易、余额、库存等数据不能用它?", "answer": "风险有三:①丢失——缓存进程在落库前宕机则数据丢失;②乱序——异步并发落库可能导致写库顺序与业务发生顺序不一致;③外部读旧值——直接读 DB 的一方可能读到旧值。由于存在这些风险只能保证最终一致,余额、库存、交易等要求强一致、绝不丢数据,所以不可用。", "keywords": [ "丢失", "乱序", "读旧值", "最终一致", "强一致" ], "scoring_rubric": "答出丢失风险得 30 分;乱序得 20 分;外部读旧值得 20 分;说明交易/余额不可用及原因得 30 分。", "explanation": "Write-Behind 的性价比来自「写进缓存即返回」,代价是数据可能丢失、乱序、外部读到旧值,只适合可容忍最终一致与丢失的数据(计数、会话、中间态)。", "source": null, "related": [] }, { "id": "sa-003", "type": "short_answer", "difficulty": 3, "tags": [ "多级缓存", "L2", "Caffeine" ], "question": "给出经典多级缓存的结构(L1/L2/DB),并说明各自的职责与读路径。", "answer": "经典结构为 L1 本地进程内缓存(Caffeine)→ L2 Redis(共享)→ DB。L1 负责当前进程热点 key 的低延迟读取,零网络开销;L2 负责跨节点共享数据;DB 为真相源兜底。读路径逐层 miss 逐层回填:本地命中最快,Redis 命中次之(毫秒级),DB 兜底。", "keywords": [ "L1", "Caffeine", "L2", "Redis", "DB", "逐层回填", "miss" ], "scoring_rubric": "结构写对 L1/L2/DB 得 40 分;职责描述正确得 30 分;读路径逐层回填得 30 分。", "explanation": "多级缓存的目的在于利用本地缓存的极低延迟承接热点读,Redis 承接跨节点共享,DB 兜底。读路径逐层 miss 逐层回填,命中最快的本地缓存优先。", "source": null, "related": [] }, { "id": "sa-004", "type": "short_answer", "difficulty": 4, "tags": [ "多级缓存", "一致性" ], "question": "为什么多级缓存是实现时最大的痛点是「一致性」?多级缓存的写路径应当如何处理?", "answer": "同一数据在 L1 本地、L2 Redis、多个节点上同时存在副本,更新时若任一副本残留旧值就会读到不一致数据,且跨层跨节点难以实时同步。因此写路径应以数据库为真相源:先写 DB,再逐层失效,即删除 Redis 并广播清理所有节点的本地缓存;必要时配合短 TTL / 消息兜底。", "keywords": [ "一致性", "副本", "先写DB", "逐层失效", "广播", "TTL" ], "scoring_rubric": "解释一致性的成因为何(多副本)得 30 分;写路径先写 DB 得 20 分;逐层失效(删 Redis、广播本地)得 40 分;补充短 TTL 兜底得 10 分。", "explanation": "多级缓存把一致性数据复制到多个位置换来读性能,代价是跨层一致性。写路径必须统一以数据库为真相源,写后逐层失效并通过广播清理各节点本地副本,必要时加短 TTL 兜底。", "source": null, "related": [] }, { "id": "sa-005", "type": "short_answer", "difficulty": 4, "tags": [ "Cache-Aside", "双删", "延时" ], "question": "说明 Cache-Aside「先写库再删缓存」的竞态窗口中有哪两个旧值来源,并简述「双删+延时」优化的做法与原理。", "answer": "两个来源:来源1为读请求在删缓存前命中缓存旧值(窗口很短通常无需处理);来源2(严重)为读线程在库中读到旧值后回填缓存,且可能发生在删缓存之后,导致旧值永久残留。双删做法:写库后先删缓存,延时约 50ms 后再删一次,目的是清掉第一次删除后刚回填的旧值;延时需覆盖并发读回填的窗口。", "keywords": [ "删缓存前命中旧值", "回填旧值", "双删", "延时", "50ms" ], "scoring_rubric": "正确指出两种来源各得 25 分;说明双删流程得 25 分;说明延时目的与原理得 25 分。", "explanation": "竞态的核心在于第二次删除发生在回填之前,双删+延时让第二次删除覆盖回填窗口,清理刚被写回的旧值。它只能把残留概率降得很低,版本号回填才能根治。", "source": null, "related": [] }, { "id": "sa-006", "type": "short_answer", "difficulty": 4, "tags": [ "版本号回填", "双删", "竞态" ], "question": "为什么说「版本号回填」能根治 Cache-Aside 竞态窗口中的「旧值回填」问题?它与双删相比有何优劣?", "answer": "版本号回填在缓存里同时存业务值和版本号,写库时携带新版本号,读线程回填前先把缓存当前版本号与将回填数据的版本号比较,旧版本直接丢弃。这样读线程永远不会把旧版本覆盖进去,从根上杜绝旧值残留。双删只能概率性把第一次删后回填的旧值二次清掉,无法保证绝对干净且会放大读 miss 窗口;版本号代价是需要维护版本并原子比较替换。", "keywords": [ "版本号", "回填前比较", "丢弃旧版本", "根治" ], "scoring_rubric": "说清缓存带版本号并比较得 40 分;说明旧版本丢弃原理(根治)得 30 分;对比双删的优劣得 30 分。", "explanation": "版本号方案把一致性判定下沉到回填动作本身,因此能从根上避免旧值被写回;双删只能概率性清理,且延时等待放大读 miss 窗口。两者可组合使用。", "source": null, "related": [] }, { "id": "sa-007", "type": "short_answer", "difficulty": 3, "tags": [ "CacheLoader", "refreshAfterWrite", "Caffeine", "Read-Through" ], "question": "Caffeine 的 CacheLoader 与 refreshAfterWrite 各自的作用是什么?如何配合防止缓存击穿?", "answer": "CacheLoader 在缓存 miss 时自动回调加载数据并回填,把 Cache- 封装成 Read-Through 的本地版;refreshAfterWrite 在条目标写入指定时长后异步刷新,避免缓存到期瞬间大量请求同时穿透到数据库。两者配合可在单条失效前提前异步刷新热点数据,显著降低击穿风险。", "keywords": [ "CacheLoader", "回填", "refreshAfterWrite", "异步刷新", "击穿" ], "scoring_rubric": "CacheLoader 的 miss 自动加载得 30 分;refreshAfterWrite 的异步刷新得 40 分;结合避免击穿得 30 分。", "explanation": "CacheLoader 负责把旁路加载封装成缓存内自动加载;refreshAfterWrite 通过在到期前异步刷新,把原本集中在某一时刻的回填错开,从而缓解或避免击穿。", "source": null, "related": [] }, { "id": "sa-008", "type": "short_answer", "difficulty": 2, "tags": [ "Caffeine", "TinyLFU", "多级缓存" ], "question": "说明为什么多级缓存的 L1 层通常首选 Caffeine 而非 Redis,以及 Caffeine 相比 Redis 的局限。", "answer": "Caffeine 是 Java 进程内存宿主缓存,毫纳秒级读取、读无锁,作为 L1 能提供最低延迟扛住超高 QPS,其 TinyLFU 淘汰算法对热点负载友好;Redis 是分布式共享缓存,需网络往返。局限在于 Caffeine 只是单机副本:不跨节点共享、重启即丢失、无法持久化/主从,多节点之间与本节点缓存一致性需额外机制维护。", "keywords": [ "本地进程内", "低延迟", "不共享", "重启丢失", "TinyLFU" ], "scoring_rubric": "Caffeine 的本地低延迟/高吞吐优点得 30 分;TinyLFU 与共享共识得 20 分;单机副本、不共享、无法持久的局限得 50 分。", "explanation": "L1 追求的是极低延迟,Caffeine 本地化天然满足;Redis 的价值在于共享与持久,二者定位不同、互补组成 L1+L2。", "source": null, "related": [] }, { "id": "sa-009", "type": "short_answer", "difficulty": 3, "tags": [ "多级缓存", "选择" ], "question": "结合读多写少、容忍秒级延迟的标准,说明什么时候应该使用多级缓存,什么时候不应该。", "answer": "应使用:读频率高、数据更新少或可容忍秒级落后的热点数据,如商品详情、榜单、静态字典、可容忍稍慢的配置。应避免:强一致实时数据如库存、余额、订单、实时价格,以及写密集或请求量本身不大的场景,因为多级缓存的主要收益是读速、代价是一致性,写密集/强一致场景得不偿失。", "keywords": [ "读多写少", "可容忍秒级落后", "热点", "强一致", "库存余额" ], "scoring_rubric": "写应用场景(读多写少/热点/容忍稍慢)得 50 分;写不应用场景(强一致/写密集化/价格真实时)得 50 分。", "explanation": "多级缓存的价值前提是读远多于写、热点集中、能容忍少量秒级落后;对一致性敏感或写密集的数据,多副本不一致的代价超过收益。", "source": null, "related": [] }, { "id": "sa-010", "type": "short_answer", "difficulty": 3, "tags": [ "多级缓存", "Cache-Aside", "Read-Through", "Write-Behind" ], "question": "客户只知道缓存读写,想让缓存层自动加载与异步落库时,会选择怎样的组合?这种主链路模式的代价有哪些?", "answer": "会采用 Read-Through + Write-Behind 组合:客户端只读写缓存,缓存 miss 时由缓存层自动查库回填(Read-Through),写则由后台异步批量刷库(Write-Behind)。代价:写操作可能丢失、可能乱序、外部直接读库的一方会读到旧值;而且交易、余额这类强一致数据绝不合适进入这类缓存主链路。", "keywords": [ "Read-Through", "Write-Behind", "异步", "丢失", "乱序", "读旧值" ], "scoring_rubric": "正确组合(Read-Through + Write-Behind)得 40 分;说明客户端只需读写缓存得 15 分;答出三种代价之一得 15 分(共 45 分)。", "explanation": "Read-Through + Write-Behind 让应用只读写缓存、把命中回填与落库都交给缓存层,代价是丢失、乱序、外部读旧值,属于「最终一致且容忍丢写」的组合。", "source": null, "related": [] } ] }