{ "topic": "cache-protection", "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": [ "cache-protection", "bloom-filter" ], "question": "BloomFilterManager 中 EXPECTED_INSERTIONS 设为 1,000,000,FPP 设为 0.01。这意味着当实际插入量远超 1,000,000 时,最可能出现什么后果?", "options": { "A": "布隆过滤器自动扩容,无任何影响", "B": "false positive rate(误判率)显著上升,导致更多不存在的 key 被错误放行", "C": "布隆过滤器抛出 IllegalStateException 异常", "D": "insert 操作被静默丢弃,不影响查询结果" }, "answer": "B", "explanation": "Guava BloomFilter 创建后容量固定,不会自动扩容。当实际插入量远超 expectedInsertions 时,位数组的每个 bit 被置 1 的比例升高,查询时误判率(false positive rate)会远高于预期的 1%(0.01),导致大量不存在的 key 也能通过 mightContain 检查,削弱布隆过滤器对缓存穿透的拦截效果。选项 A 错误,BloomFilter 没有扩容机制;选项 C 错误,不会抛异常;选项 D 错误,put 操作不会被丢弃。", "source": null, "related": [] }, { "id": "sc-002", "type": "single_choice", "difficulty": 2, "tags": [ "cache-protection", "bloom-filter" ], "question": "关于 BloomFilter.mightContain() 方法的返回值语义,下列哪项描述是正确的?", "options": { "A": "返回 true 表示该元素一定存在", "B": "返回 false 表示该元素一定不存在", "C": "返回 true 表示该元素一定不存在", "D": "无论返回 true 还是 false 都是不确定的" }, "answer": "B", "explanation": "布隆过滤器的核心特性:mightContain 返回 false 则元素一定不存在(无 false negative),返回 true 则元素可能存在也可能不存在(有 false positive)。因此 ThumbServiceImpl.hasThumb() 中判断 !bloomFilterManager.mightContain(userId, blogId) 为 true 时直接返回 false,是安全的。选项 A 错误,true 不是确定性结论;选项 C 错误,true 表示可能存在;选项 D 错误,false 是确定性结论。", "source": null, "related": [] }, { "id": "sc-003", "type": "single_choice", "difficulty": 2, "tags": [ "cache-protection", "null-cache" ], "question": "CacheManager 中 NULL_CACHE_TTL_SECONDS 设为 30 秒。这个设计的核心目的是什么?", "options": { "A": "防止缓存雪崩,让空值在 30 秒后自然失效", "B": "防止缓存穿透,避免同一个不存在的 key 反复穿透到 Redis", "C": "防止缓存击穿,保护热点 key 的本地缓存", "D": "防止内存溢出,限制 nullValueCache 的最大条目数" }, "answer": "B", "explanation": "当 Redis 中某个 key 不存在时,nullValueCache 会在 30 秒内缓存 NULL_PLACEHOLDER,后续相同 key 的查询直接在空值短缓存层返回 null,不再穿透到 Redis。这是典型的「缓存穿透防护」手段——通过缓存空值来拦截对不存在数据的反复查询。选项 A 描述的是缓存雪崩场景(大量 key 同时过期);选项 C 是击穿场景(热点 key 过期);选项 D 虽然 maximumSize 确实能防止内存溢出,但不是 TTL 设置的直接目的。", "source": null, "related": [] }, { "id": "sc-004", "type": "single_choice", "difficulty": 3, "tags": [ "cache-protection", "mutex-lock" ], "question": "CacheManager.get() 方法中使用了 synchronized(lock) 加互斥锁。结合代码分析,这种互斥锁设计主要解决的是什么问题?", "options": { "A": "防止缓存穿透:拦截不存在的 key 访问 Redis", "B": "防止缓存击穿:避免热点 key 过期瞬间大量请求同时打到 Redis", "C": "防止缓存雪崩:保证同一时刻只有一个线程写入缓存", "D": "防止数据库连接池耗尽:限制并发数据库查询数" }, "answer": "B", "explanation": "这是经典的「互斥锁防击穿」模式:当本地缓存未命中时,先用 computeIfAbsent 在 lockMap 中为该 key 创建一把锁对象,然后 synchronized 加锁。锁内再次 double-check 本地缓存(防止其他线程已经完成加载)。如果仍未命中则查 Redis。这样即使热点 key 同时过期,也只有一个线程能穿透到 Redis,其余线程在锁上等待,避免了「击穿」导致的 Redis/数据库压力骤增。选项 A 是 nullValueCache 的职责;选项 C 场景不对;选项 D 的目标对象不对。", "source": null, "related": [] }, { "id": "sc-005", "type": "single_choice", "difficulty": 3, "tags": [ "cache-protection", "mutex-lock" ], "question": "CacheManager.get() 中在 synchronized 块内部先做了一次 localCache.getIfPresent(compositeKey) 检查(double-check)。这一步的作用是?", "options": { "A": "减少锁竞争时间,因为 synchronized 块内可以更快获取本地缓存", "B": "防止线程在等待锁期间,另一个线程已完成加载,导致重复查 Redis", "C": "防止 nullValueCache 被多个线程并发写入导致数据不一致", "D": "防止 hotKeyDetector 被多次 add 导致热点误判" }, "answer": "B", "explanation": "这是经典的 double-check locking 模式:线程 A 持锁加载数据完成后释放锁,线程 B(或 C、D…)获得锁后,如果不做 double-check 就直接查 Redis,会导致同一个 key 被重复加载。double-check 在锁内再次检查本地缓存,如果已被前一个线程加载好,直接返回,避免重复查询。选项 A 描述的是减少持锁时间,但不是 double-check 的核心目的;选项 C 不涉及 nullValueCache 的并发写入问题;选项 D 虽然有关联但不是主要目的。", "source": null, "related": [] }, { "id": "sc-006", "type": "single_choice", "difficulty": 3, "tags": [ "cache-protection", "null-cache" ], "question": "CacheManager 中 nullValueCache 的 maximumSize 设为 10000。如果攻击者用大量不存在的 key 发起请求,以下哪种情况最可能发生?", "options": { "A": "nullValueCache 持续增长直到 OOM,因为不存在的 key 总是不同", "B": "Caffeine 使用 W-TinyLFU 策略自动淘汰最不常用的空值条目,内存不会无限增长", "C": "新写入的空值条目直接覆盖最旧的空值条目,类似于 LRU", "D": "nullValueCache 会抛出 IllegalStateException 并阻止后续写入" }, "answer": "B", "explanation": "Caffeine 的 maximumSize 采用 W-TinyLFU(Window Tiny Least Frequently Used)淘汰策略。当条目数接近 10000 时,会自动淘汰访问频率最低的条目,腾出空间给新条目。因此即使攻击者用大量不存在的不同 key 来请求,nullValueCache 的内存也不会无限增长,旧的低频空值条目会被淘汰。选项 A 错误,Caffeine 不会 OOM;选项 C 描述的是简单 FIFO/LRU,不是 Caffeine 的策略;选项 D 错误,Caffeine 不会抛异常。", "source": null, "related": [] }, { "id": "sc-007", "type": "single_choice", "difficulty": 4, "tags": [ "cache-protection", "bloom-filter" ], "question": "ThumbServiceImpl.hasThumb() 中布隆过滤器与 CacheManager 的协作关系是:先用布隆过滤器判断 key 是否可能存在,再用 CacheManager 查缓存。如果把这两层的顺序反转(先查缓存,再查布隆过滤器),以下哪种说法最准确?", "options": { "A": "功能完全等价,因为两层都是拦截,顺序不影响最终结果", "B": "布隆过滤器层失去意义,因为 CacheManager 本身已有空值缓存和互斥锁来防穿透", "C": "仍然有效,但布隆过滤器的拦截时机后移,无法在第一时间过滤不存在的 key,增加了对空值缓存的无效访问", "D": "会导致功能错误,因为 CacheManager 返回的 null 无法被布隆过滤器识别" }, "answer": "C", "explanation": "原设计中布隆过滤器作为最前置的「粗筛」,能在 O(1) 时间内快速排除大量不存在的 key,减少对 CacheManager/Redis 的无效访问。如果反转顺序,所有请求(包括不存在的 key)都会先走 CacheManager 的多层查询链路(本地缓存 → nullValueCache → 锁 + Redis),只有在 CacheManager 返回非 null 时才走布隆过滤器。此时布隆过滤器的前置拦截价值大打折扣。选项 A 错误,两层职责不同不能简单等价;选项 B 说法过于绝对,布隆过滤器仍有价值;选项 D 不成立,null 本身就是一个合法的返回值。", "source": null, "related": [] }, { "id": "sc-008", "type": "single_choice", "difficulty": 4, "tags": [ "cache-protection", "mutex-lock" ], "question": "CacheManager 的 lockMap 使用 ConcurrentHashMap,且锁对象通过 computeIfAbsent 创建。以下哪种场景可能导致该互斥锁机制失效?", "options": { "A": "多个 JVM 实例部署时,每个实例的 lockMap 独立,不同实例的线程无法互斥", "B": "Redis 集群模式下,不同 slot 的 key 命中不同 Redis 节点,导致锁失效", "C": "Caffeine 本地缓存被驱逐时,lockMap 中对应的锁对象也被清除", "D": "synchronized 关键字在高并发下会导致 CPU 100%,锁机制自动降级为无锁模式" }, "answer": "A", "explanation": "lockMap 是 JVM 进程内的 ConcurrentHashMap,锁对象仅在当前 JVM 实例内有效。在多实例部署场景下,实例 A 的线程持有 lockMap 中的锁不会阻塞实例 B 的线程,因此多个实例可能同时穿透到 Redis。这是单机互斥锁的固有局限,通常需要配合分布式锁(如 Redis SETNX)来解决。选项 B 描述的是 Redis 分片问题,与锁无关;选项 C 不成立,lockMap 和 localCache 是独立的数据结构;选项 D 荒谬,synchronized 不会自动降级。", "source": null, "related": [] } ] }