78 lines
5.8 KiB
JSON
78 lines
5.8 KiB
JSON
|
|
{
|
|||
|
|
"topic": "two-level-cache",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"schema_version": "1.0.0",
|
|||
|
|
"generated": "2026-09-09T21:42:00+08:00",
|
|||
|
|
"questions": [
|
|||
|
|
{
|
|||
|
|
"id": "sa-001",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 3,
|
|||
|
|
"tags": [
|
|||
|
|
"two-level-cache",
|
|||
|
|
"CacheManager",
|
|||
|
|
"get-flow"
|
|||
|
|
],
|
|||
|
|
"question": "请完整描述 CacheManager.get() 的查询流程,说明每一步的目的。",
|
|||
|
|
"answer": "CacheManager.get() 的完整流程如下:\n1. 查询 L1(Caffeine 本地缓存):命中则直接返回,这是最快的路径,零网络开销。\n2. 查询空值缓存(null cache):如果 Key 被标记为空值缓存,说明之前确认 Redis 中不存在该 Key,直接返回 null,防止缓存穿透。\n3. 加锁并 double-check:获取 Key 级别的锁后,重新检查 L1 和空值缓存,防止并发请求重复穿透(防止惊群效应)。\n4. 查询 Redis(L2 缓存):从 Redis 获取数据。\n5. 写入空值缓存或 HeavyKeeper 记录:Redis 返回 null 则写入空值缓存防止穿透;Redis 有数据则通过 HeavyKeeper.add() 记录访问频次。\n6. 热 Key 提升 L1:如果 HeavyKeeper.isHotKey() 返回 true,将数据写入 Caffeine,后续相同 Key 的请求可在 L1 直接命中。",
|
|||
|
|
"explanation": "",
|
|||
|
|
"keywords": [
|
|||
|
|
"L1",
|
|||
|
|
"Caffeine",
|
|||
|
|
"空值缓存",
|
|||
|
|
"double-check",
|
|||
|
|
"Redis",
|
|||
|
|
"HeavyKeeper",
|
|||
|
|
"热Key提升"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "满分10分。描述完整6步流程得8分,每少一步扣1.5分。每步有正确的目的说明额外加0.5分,最多加2分。出现概念性错误(如将空值缓存描述为缓存 Redis 空值对象)扣2分。"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-002",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 4,
|
|||
|
|
"tags": [
|
|||
|
|
"HeavyKeeper",
|
|||
|
|
"fading",
|
|||
|
|
"decay",
|
|||
|
|
"parameters"
|
|||
|
|
],
|
|||
|
|
"question": "请解释 HeavyKeeper 的双重衰减机制(add() 中的冲突衰减和 fading() 的周期衰减)是如何协同工作的,以及为什么要同时使用两种衰减。",
|
|||
|
|
"answer": "HeavyKeeper 的双重衰减机制包括:\n\n1. add() 中的冲突衰减(per-operation decay):当新 Key 的 fingerprint 与桶中已有 Key 冲突时,以概率 decay(0.92)对已有 count 执行 -1 操作。这是一种细粒度的、事件驱动的衰减,每次冲突都可能淘汰冷 Key。\n\n2. fading() 的周期衰减(periodic decay):每 20 秒对所有桶的 count 执行右移一位(除以 2)。这是一种粗粒度的、时间驱动的衰减,确保热度统计反映的是近期行为而非历史累积。\n\n协同工作原理:\n- 如果只有冲突衰减:冷 Key 不会被冲突到的桶永远不会衰减,导致历史热 Key 永远占据桶位。\n- 如果只有周期衰减:两个冷 Key 之间不会互相淘汰,桶位被低频 Key 长期占用。\n- 两者结合:冲突衰减在局部竞争中快速淘汰冷 Key,周期衰减在全局范围内将所有 Key 的热度重置为半衰期模型。冲突衰减处理 Key 之间的横向竞争,fading() 处理时间维度的纵向衰减。这种设计让 HeavyKeeper 既能快速感知热度变化,又能防止历史数据干扰。",
|
|||
|
|
"explanation": "",
|
|||
|
|
"keywords": [
|
|||
|
|
"冲突衰减",
|
|||
|
|
"周期衰减",
|
|||
|
|
"decay",
|
|||
|
|
"fading",
|
|||
|
|
"right-shift",
|
|||
|
|
"半衰期"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "满分10分。正确描述两种衰减各2分(共4分)。正确分析只有一种衰减时的缺陷各1.5分(共3分)。正确说明协同工作原理2分。表述清晰、逻辑连贯1分。出现概念性错误(如将右移描述为除以4)扣2分。"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-003",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 5,
|
|||
|
|
"tags": [
|
|||
|
|
"two-level-cache",
|
|||
|
|
"inconsistency",
|
|||
|
|
"L1-L2",
|
|||
|
|
"write-consistency"
|
|||
|
|
],
|
|||
|
|
"question": "在分布式环境下,Caffeine + Redis 二级缓存面临哪些数据不一致风险?请分别从「写后读」和「并发写」两个场景分析,并给出至少两种缓解方案。",
|
|||
|
|
"answer": "一、写后读不一致风险:\n当一个实例更新/删除 Redis 后,需要通知其他实例失效本地 Caffeine。在通知发出到其他实例收到并执行失效之间,存在时间窗口。在这个窗口内,其他实例的 Caffeine 仍持有旧数据,读请求会返回过期值。\n\n二、并发写不一致风险:\n多个实例同时更新同一 Key 时,由于操作顺序不可控(实例 A 先删 L1 后写 Redis,实例 B 可能在 A 写 Redis 之前读到旧 L1 并缓存),可能导致最终 L1 和 L2 数据不一致。如果使用 putIfPresent 还可能导致合法更新被拒绝。\n\n缓解方案:\n1. 延迟双删:更新 Redis 后先失效 L1,sleep 一段时间后再次失效 L1,确保窗口期的脏数据被清理。\n2. 消息队列通知:使用可靠的 MQ(如 RocketMQ)替代 Redis Pub/Sub 发送失效通知,保证通知不丢失。\n3. 版本号/CAS:在 Redis 中为每个 Key 维护版本号,读取时校验版本号,版本不一致则重新加载。\n4. 短 TTL 兜底:L1 的 expireAfterWrite 设置较短(如几秒),即使通知丢失,脏数据也会很快自然过期。\n5. 订阅 Redis 消费 binlog:使用 Canal 等工具监听 Redis 的数据变更事件,实现最终一致。",
|
|||
|
|
"explanation": "",
|
|||
|
|
"keywords": [
|
|||
|
|
"写后读",
|
|||
|
|
"并发写",
|
|||
|
|
"延迟双删",
|
|||
|
|
"消息队列",
|
|||
|
|
"版本号",
|
|||
|
|
"TTL",
|
|||
|
|
"Canal"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "满分10分。正确描述写后读不一致场景2分。正确描述并发写不一致场景2分。给出至少两种有效缓解方案,每种1.5分(最多6分)。方案描述含糊或有概念错误扣1分。超过5种方案不再加分。"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|