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

245 lines
13 KiB
JSON

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