feat: add ThumbUP 项目 — 70 questions across 5 subtopics (cache-protection, two-level-cache, lua-bucketing-sync, heavykeeper-topk, distributed-lock)
Deploy Examination / deploy (push) Successful in 20s
Deploy Examination / deploy (push) Successful in 20s
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"topic": "two-level-cache",
|
||||
"type": "code_reading",
|
||||
"schema_version": "1.0.0",
|
||||
"generated": "2026-09-09T21:42:00+08:00",
|
||||
"questions": [
|
||||
{
|
||||
"id": "cr-001",
|
||||
"type": "code_reading",
|
||||
"difficulty": 3,
|
||||
"tags": [
|
||||
"two-level-cache",
|
||||
"CacheManager",
|
||||
"get-flow"
|
||||
],
|
||||
"question": "阅读以下 CacheManager.get() 方法的简化实现,回答后续子问题。",
|
||||
"code": "java\npublic Object get(String key, Supplier<Object> dataLoader) {\n // Step 1: 查询 L1 Caffeine 缓存\n Object value = caffeineCache.getIfPresent(key);\n if (value != null) {\n return value;\n }\n \n // Step 2: 查询空值缓存\n if (nullCache.containsKey(key)) {\n return null;\n }\n \n // Step 3: 加锁 double-check\n Object lock = lockMap.computeIfAbsent(key, k -> new Object());\n synchronized (lock) {\n // double-check L1\n value = caffeineCache.getIfPresent(key);\n if (value != null) {\n return value;\n }\n // double-check 空值缓存\n if (nullCache.containsKey(key)) {\n return null;\n }\n \n // Step 4: 查询 Redis (L2)\n value = redisTemplate.opsForValue().get(key);\n \n if (value == null) {\n // Step 5: 写入空值缓存防止穿透\n nullCache.put(key, Boolean.TRUE);\n } else {\n // Step 6: HeavyKeeper 记录并判断是否提升到 L1\n heavyKeeper.add(key);\n if (heavyKeeper.isHotKey(key)) {\n caffeineCache.put(key, value);\n }\n }\n return value;\n }\n}",
|
||||
"sub_questions": [
|
||||
{
|
||||
"index": 1,
|
||||
"type": "single_choice",
|
||||
"question": "为什么在 Step 3 中需要先对空值缓存做 double-check?",
|
||||
"options": {
|
||||
"A": "防止空值缓存过期导致重复穿透",
|
||||
"B": "防止多个线程同时查到 Redis 为空后重复写入空值缓存",
|
||||
"C": "空值缓存的写入不是线程安全的",
|
||||
"D": "空值缓存可能被其他线程删除"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "在加锁之前,线程 A 可能已经查过空值缓存为 false,然后进入 synchronized 块。此时线程 B 也完成了 Step 2 检查并等待锁。如果线程 A 发现 Redis 为空并写入了空值缓存,线程 B 获得锁后不重新检查空值缓存,就会再次查 Redis。double-check 确保每个线程在持有锁后重新验证,避免重复穿透。"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"type": "short_answer",
|
||||
"question": "如果 HeavyKeeper 的 isHotKey(key) 始终返回 false,会对系统产生什么影响?请从缓存命中率和 Redis 压力两个角度分析。",
|
||||
"answer": "如果 HeavyKeeper 始终判定所有 Key 都不是热 Key,则 Caffeine(L1)永远不会被主动填充数据。所有请求都必须穿透到 Redis(L2)获取数据。短期来看:缓存命中率急剧下降,Redis QPS 显著上升,系统延迟增加。长期来看:Caffeine 只能通过被动加载(如 get(key, loader) 的 loading cache 模式)或预热填充,失去了热 Key 主动提升的价值。在高并发场景下,Redis 可能成为瓶颈甚至宕机。",
|
||||
"explanation": "",
|
||||
"keywords": [
|
||||
"Caffeine",
|
||||
"Redis",
|
||||
"命中率",
|
||||
"Redis压力",
|
||||
"热Key"
|
||||
]
|
||||
}
|
||||
],
|
||||
"explanation": "该代码展示了 CacheManager.get() 的核心流程:L1 查询 → 空值缓存检查 → 加锁 double-check → Redis 查询 → 空值缓存写入 / HeavyKeeper 记录与热 Key 提升。",
|
||||
"source": null,
|
||||
"related": [],
|
||||
"language": "java"
|
||||
},
|
||||
{
|
||||
"id": "cr-002",
|
||||
"type": "code_reading",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"HeavyKeeper",
|
||||
"add",
|
||||
"bucket-operation"
|
||||
],
|
||||
"question": "阅读以下 HeavyKeeper.add() 方法的核心逻辑,回答后续子问题。",
|
||||
"code": "java\npublic void add(String key) {\n long fingerprint = fingerprint(key);\n for (int i = 0; i < depth; i++) {\n int bucketIdx = hash(key, i) % width;\n Bucket bucket = buckets[bucketIdx];\n \n // 分支 1: 桶为空\n if (bucket.fingerprint == 0) {\n bucket.fingerprint = fingerprint;\n bucket.count = 1;\n return;\n }\n \n // 分支 2: fingerprint 匹配\n if (bucket.fingerprint == fingerprint) {\n bucket.count++;\n if (bucket.count > maxCount) {\n bucket.count = maxCount;\n }\n return;\n }\n \n // 分支 3: 桶冲突,概率衰减竞争\n if (ThreadLocalRandom.current().nextDouble() < decay) {\n bucket.count--;\n if (bucket.count <= 0) {\n bucket.fingerprint = fingerprint;\n bucket.count = 1;\n }\n }\n }\n}",
|
||||
"sub_questions": [
|
||||
{
|
||||
"index": 1,
|
||||
"type": "single_choice",
|
||||
"question": "在分支 3(桶冲突)中,decay=0.92 的作用是什么?",
|
||||
"options": {
|
||||
"A": "92% 的概率直接替换桶中的 fingerprint",
|
||||
"B": "以 92% 的概率对桶中已有 count 执行减 1 操作",
|
||||
"C": "将桶中 count 乘以 0.92 进行衰减",
|
||||
"D": "92% 的概率跳过该桶的处理"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "在分支 3 中,每次遇到桶冲突时,有 decay(0.92 = 92%)的概率对桶中已有的 count 执行减 1 操作。如果衰减后 count ≤ 0,则替换为当前 Key 的 fingerprint。这意味着冷 Key 的 count 会逐渐被衰减至 0 并被替换,而热 Key 由于分支 2 的 count++ 频率更高,能保持较高的 count 值,从而不被淘汰。decay 越大,冷 Key 被淘汰的速度越快。"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"type": "short_answer",
|
||||
"question": "如果 width=100000, depth=5,理论上一个 Key 最多占用几个桶?最坏情况下会占用多少个桶(考虑 fingerprint 碰撞)?请解释原因。",
|
||||
"answer": "理论上每个 Key 通过 5 个哈希函数映射到 5 个桶,正常情况下最多占用 5 个桶。但如果其他 Key 的 fingerprint 与当前 Key 相同(fingerprint 碰撞),则分支 2 会被触发,那些桶实际上也被当前 Key 的 fingerprint 「占用」。然而在实际中,64-bit fingerprint 碰撞概率极低。因此一个 Key 最多占用 depth=5 个桶。这是 HeavyKeeper 内存效率的关键——每个 Key 只用 5 个桶位置来维护热度信息,而不是像 Count-Min Sketch 那样需要更多空间。",
|
||||
"explanation": "",
|
||||
"keywords": [
|
||||
"fingerprint",
|
||||
"depth",
|
||||
"桶",
|
||||
"碰撞",
|
||||
"Count-Min Sketch"
|
||||
]
|
||||
}
|
||||
],
|
||||
"explanation": "HeavyKeeper 的 add() 方法体现了三个分支的设计:空桶直接写入、fingerprint 匹配累加、冲突概率衰减竞争。配合 fading() 的周期性减半,实现了高效的热 Key 探测。",
|
||||
"source": null,
|
||||
"related": [],
|
||||
"language": "java"
|
||||
},
|
||||
{
|
||||
"id": "cr-003",
|
||||
"type": "code_reading",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"two-level-cache",
|
||||
"update",
|
||||
"putIfPresent",
|
||||
"inconsistency"
|
||||
],
|
||||
"question": "阅读以下二级缓存更新逻辑的简化实现,回答后续子问题。",
|
||||
"code": "java\npublic boolean update(String key, Object newValue) {\n // Step 1: 先更新 Redis (L2)\n Boolean redisResult = redisTemplate.opsForValue().setIfAbsent(key, newValue);\n if (redisResult == null || !redisResult) {\n return false; // Key 不存在,更新失败\n }\n \n // Step 2: 失效 L1 Caffeine 缓存\n caffeineCache.invalidate(key);\n \n // Step 3: 通知其他实例失效 L1\n notificationService.publishInvalidation(key);\n \n // Step 4: 更新空值缓存\n nullCache.invalidate(key);\n \n return true;\n}\n\npublic boolean delete(String key) {\n // Step 1: 先删除 Redis\n Long count = redisTemplate.delete(key);\n \n // Step 2: 失效 L1\n caffeineCache.invalidate(key);\n \n // Step 3: 通知其他实例\n notificationService.publishInvalidation(key);\n \n // Step 4: 写入空值缓存\n nullCache.put(key, Boolean.TRUE);\n \n return count != null && count > 0;\n}",
|
||||
"sub_questions": [
|
||||
{
|
||||
"index": 1,
|
||||
"type": "single_choice",
|
||||
"question": "在 delete() 方法中,Step 2(失效 L1)和 Step 1(删除 Redis)之间存在一个时间窗口。在这个窗口内,如果有新的读请求到达同一实例,会发生什么?",
|
||||
"options": {
|
||||
"A": "读请求在 L1 命中旧数据并返回",
|
||||
"B": "读请求在 L1 未命中,去 Redis 查询也被删除,返回 null",
|
||||
"C": "读请求触发 L1 重新从 Redis 加载,但 Redis 已删除,所以返回 null",
|
||||
"D": "读请求被阻塞等待 delete 完成"
|
||||
},
|
||||
"answer": "A",
|
||||
"explanation": "delete() 中 Redis 删除(Step 1)和 Caffeine 失效(Step 2)之间存在时间窗口。在这个窗口内,该实例的 Caffeine 中仍然持有旧数据。由于同一实例内的读请求不会经过分布式锁,直接从 Caffeine 读取,因此会命中旧数据并返回。这是一个典型的 L1/L2 不一致窗口,但由于删除操作顺序是「先删 Redis 再删 L1」(而非相反),不一致的影响相对较小——最多读到即将过期的旧数据,不会读到脏数据。"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"type": "short_answer",
|
||||
"question": "update() 方法中使用了 setIfAbsent 而非直接 set。请分析:如果两个线程同时调用 update() 更新同一个 Key 的不同值,最终结果会怎样?这种设计有什么利弊?",
|
||||
"answer": "由于 setIfAbsent 只在 Key 不存在时写入,两个线程同时调用时:第一个线程成功写入,第二个线程 setIfAbsent 返回 false 导致更新失败。最终结果是只保留第一个线程的值。利:防止了并发更新时的覆盖问题,保证了写入的原子性,是一种乐观锁思路。弊:后续的合法更新会被误拒绝,导致数据丢失。如果业务要求后写入者胜出(last-write-wins),应该使用 set 而非 setIfAbsent,或在 Redis 端使用 Lua 脚本做 CAS 操作。",
|
||||
"explanation": "",
|
||||
"keywords": [
|
||||
"setIfAbsent",
|
||||
"并发",
|
||||
"乐观锁",
|
||||
"last-write-wins",
|
||||
"CAS"
|
||||
]
|
||||
}
|
||||
],
|
||||
"explanation": "该代码展示了二级缓存更新/删除的标准流程:先操作 Redis,再失效本地缓存,最后通知其他实例。操作顺序(先 L2 后 L1)决定了不一致窗口的方向和影响。",
|
||||
"source": null,
|
||||
"related": [],
|
||||
"language": "java"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"slug": "two-level-cache",
|
||||
"name": "Caffeine + Redis 二级缓存",
|
||||
"description": "CacheManager 二级缓存架构、HeavyKeeper 热Key探测、putIfPresent 写一致性",
|
||||
"tags": [
|
||||
"CacheManager",
|
||||
"HeavyKeeper",
|
||||
"L1-L2",
|
||||
"decay",
|
||||
"fading",
|
||||
"get-flow",
|
||||
"inconsistency",
|
||||
"parameters",
|
||||
"two-level-cache",
|
||||
"write-consistency"
|
||||
],
|
||||
"difficulty_range": [
|
||||
3,
|
||||
5
|
||||
],
|
||||
"schema_version": "1.0.0",
|
||||
"updated": "2026-09-09",
|
||||
"question_files": [
|
||||
"single_choice",
|
||||
"code_reading",
|
||||
"short_answer"
|
||||
],
|
||||
"stats": {
|
||||
"total": 14,
|
||||
"by_type": {
|
||||
"single_choice": 8,
|
||||
"code_reading": 3,
|
||||
"short_answer": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"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种方案不再加分。"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"topic": "two-level-cache",
|
||||
"type": "single_choice",
|
||||
"schema_version": "1.0.0",
|
||||
"generated": "2026-09-09T21:42:00+08:00",
|
||||
"questions": [
|
||||
{
|
||||
"id": "sc-001",
|
||||
"type": "single_choice",
|
||||
"difficulty": 2,
|
||||
"tags": [
|
||||
"two-level-cache",
|
||||
"CacheManager",
|
||||
"architecture"
|
||||
],
|
||||
"question": "在 Caffeine + Redis 二级缓存架构中,CacheManager.get() 的第一步查询目标是什么?",
|
||||
"options": {
|
||||
"A": "直接查询 Redis(L2 缓存)",
|
||||
"B": "查询 Caffeine 本地缓存(L1 缓存)",
|
||||
"C": "查询空值缓存(null cache)",
|
||||
"D": "加锁后执行 double-check 查询 Redis"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "CacheManager.get() 的完整流程为:查 L1(Caffeine)→ 查空值缓存 → 加锁 double-check → 查 Redis → HeavyKeeper 记录 → 热 Key 提升 L1。第一步始终是查询本地 Caffeine 缓存,命中则直接返回,未命中才进入后续流程。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "sc-002",
|
||||
"type": "single_choice",
|
||||
"difficulty": 3,
|
||||
"tags": [
|
||||
"two-level-cache",
|
||||
"null-cache",
|
||||
"lock"
|
||||
],
|
||||
"question": "CacheManager.get() 流程中,空值缓存(null cache)的作用是什么?",
|
||||
"options": {
|
||||
"A": "缓存 Redis 中存储的空值对象,防止穿透",
|
||||
"B": "记录已查询过但 Redis 中不存在的 Key,避免重复穿透到 Redis",
|
||||
"C": "作为 L1 和 L2 之间的一致性校验缓存",
|
||||
"D": "存储 HeavyKeeper 探测到的热 Key 列表"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "空值缓存用于记录已确认在 Redis 中不存在的 Key,后续相同 Key 的查询命中空值缓存后直接返回,不再穿透到 Redis,有效防止缓存穿透。注意这里缓存的不是 Redis 中的空值对象,而是对 Key 不存在这一事实的标记。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "sc-003",
|
||||
"type": "single_choice",
|
||||
"difficulty": 2,
|
||||
"tags": [
|
||||
"Caffeine",
|
||||
"W-TinyLFU",
|
||||
"eviction"
|
||||
],
|
||||
"question": "Caffeine 使用的 W-TinyLFU 淘汰策略中,「W」代表什么含义?",
|
||||
"options": {
|
||||
"A": "Write-through(写穿透)",
|
||||
"B": "Window(窗口)",
|
||||
"C": "Weighted(加权)",
|
||||
"D": "Waiting(等待)"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "W-TinyLFU 中的 W 代表 Window,即窗口缓存。W-TinyLFU 将缓存分为 Window(窗口)和 Main(主)两个区域。新数据先进入 Window 区,通过准入策略(admission filter)决定是否提升到 Main 区。Main 区内部再分为 TinyLFU 的 probation 和 protected 两个子区。这种分层设计兼顾了频率统计精度和内存效率。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "sc-004",
|
||||
"type": "single_choice",
|
||||
"difficulty": 3,
|
||||
"tags": [
|
||||
"HeavyKeeper",
|
||||
"hot-key",
|
||||
"detection"
|
||||
],
|
||||
"question": "HeavyKeeper 热 Key 探测算法的 add() 方法在遇到桶冲突时,执行的是什么操作?",
|
||||
"options": {
|
||||
"A": "直接覆盖桶中原有的 fingerprint 和 count",
|
||||
"B": "累加已有 fingerprint 对应桶的 count",
|
||||
"C": "以概率 decay 对已有 count 进行衰减,若衰减后 count < minCount 则替换为新 fingerprint,否则保留原 fingerprint",
|
||||
"D": "将冲突 Key 放入溢出队列,异步处理"
|
||||
},
|
||||
"answer": "C",
|
||||
"explanation": "HeavyKeeper 的 add() 有三个分支:1) 桶为空时直接写入 fingerprint 和 count=1;2) fingerprint 匹配时直接 count++;3) 桶冲突时以概率 decay 对已有 count 衰减,衰减后若 count < minCount(阈值为 10)则替换为当前 fingerprint 和 count=1,否则保留原 fingerprint。这一设计利用概率衰减自然淘汰冷 Key,保留热 Key。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "sc-005",
|
||||
"type": "single_choice",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"HeavyKeeper",
|
||||
"fading",
|
||||
"decay"
|
||||
],
|
||||
"question": "HeavyKeeper 的 fading() 方法每 20 秒执行一次,其核心操作是什么?",
|
||||
"options": {
|
||||
"A": "清空所有桶的 count,重新统计",
|
||||
"B": "将所有桶的 count 右移一位(等效于除以 2,减半衰减)",
|
||||
"C": "只对 count > minCount 的桶执行衰减",
|
||||
"D": "删除所有 fingerprint 为空的桶"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "fading() 每 20 秒触发一次,对 HeavyKeeper 的所有 bucket 的 count 执行右移一位操作,即 count >>= 1,等效于将所有计数值减半。这实现了时间维度的指数衰减,使 HeavyKeeper 能够反映 Key 的近期热度而非累积热度。配合 add() 中的冲突衰减(decay=0.92),形成双重衰减机制。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "sc-006",
|
||||
"type": "single_choice",
|
||||
"difficulty": 3,
|
||||
"tags": [
|
||||
"two-level-cache",
|
||||
"write-consistency",
|
||||
"putIfPresent"
|
||||
],
|
||||
"question": "在二级缓存架构中,更新数据时使用 putIfPresent 写入 Redis 的主要目的是什么?",
|
||||
"options": {
|
||||
"A": "提高写入性能,减少 Redis 操作次数",
|
||||
"B": "保证只有在 Key 已存在时才更新,防止误覆盖其他线程新写入的数据",
|
||||
"C": "确保 Redis 和 Caffeine 的写入顺序一致",
|
||||
"D": "自动触发 L1 缓存的失效通知"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "putIfPresent 是 Redis 的条件写入命令,只有当 Key 已存在时才会执行更新操作并返回 OK,否则返回 nil。在二级缓存更新场景中,这可以防止并发更新时的误覆盖问题——如果另一个线程已经删除并重建了该 Key,当前线程的写入不会覆盖新数据。这是一种乐观锁思路的写一致性保障。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "sc-007",
|
||||
"type": "single_choice",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"two-level-cache",
|
||||
"inconsistency",
|
||||
"L1-L2"
|
||||
],
|
||||
"question": "在 Caffeine + Redis 二级缓存中,以下哪种场景最容易导致 L1 和 L2 之间的数据不一致?",
|
||||
"options": {
|
||||
"A": "L1 缓存容量设置过小导致频繁淘汰",
|
||||
"B": "多个应用实例同时更新 Redis 中的同一 Key,但各实例的 Caffeine 失效通知未及时送达",
|
||||
"C": "Caffeine 的 expireAfterWrite 设置过长",
|
||||
"D": "Redis 采用主从架构导致读写延迟"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "多实例场景下,当一个实例更新了 Redis 数据后,需要通过消息广播(如 Redis Pub/Sub、RocketMQ 等)通知其他实例失效本地 Caffeine 缓存。如果通知延迟或丢失,其他实例的 Caffeine 中仍持有旧数据,而 Redis 已经是新数据,造成 L1/L2 不一致。这是分布式二级缓存最经典的一致性问题。A 和 C 影响的是缓存命中率,D 影响的是主从一致性而非 L1/L2 一致性。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "sc-008",
|
||||
"type": "single_choice",
|
||||
"difficulty": 5,
|
||||
"tags": [
|
||||
"HeavyKeeper",
|
||||
"parameters",
|
||||
"configuration"
|
||||
],
|
||||
"question": "HeavyKeeper 当前配置为 k=100, width=100000, depth=5, decay=0.92, minCount=10。当一个新 Key 被 add() 时,该 Key 的 fingerprint 需要经过几个哈希函数映射到几个桶中进行检测?",
|
||||
"options": {
|
||||
"A": "1 个(随机选一个桶)",
|
||||
"B": "5 个(depth 个桶)",
|
||||
"C": "100 个(k 个桶)",
|
||||
"D": "100000 个(width 个桶)"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "HeavyKeeper 中,depth 决定了每个 Key 需要检测的桶数量。对于每个被 add 的 Key,先计算其 fingerprint,然后通过 depth(此处为 5)个不同的哈希函数映射到 width 个桶中的 5 个位置。在这 5 个桶中,按 add() 的三分支逻辑处理。depth 越大,对热 Key 的识别精度越高,但内存开销也相应增加。k 是返回的 Top-K 热 Key 数量,width 是桶数组大小,decay 和 minCount 控制衰减和淘汰。",
|
||||
"source": null,
|
||||
"related": []
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user