Files
examination/topics/thumbup/cache-protection/code_reading.json
T

151 lines
13 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"topic": "cache-protection",
"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": [
"cache-protection",
"bloom-filter"
],
"question": "阅读以下 BloomFilterManager 代码片段,回答后续问题:",
"code": "@Component\npublic class BloomFilterManager {\n private BloomFilter<String> thumbBloomFilter;\n private static final long EXPECTED_INSERTIONS = 1_000_000L;\n private static double FPP = 0.01;\n\n @PostConstruct\n public void init() {\n thumbBloomFilter = BloomFilter.create(\n Funnels.stringFunnel(StandardCharsets.UTF_8),\n EXPECTED_INSERTIONS, FPP);\n }\n\n public void add(Long userId, Long blogId) {\n thumbBloomFilter.put(userId + \":\" + blogId);\n }\n\n public boolean mightContain(Long userId, Long blogId) {\n return thumbBloomFilter.mightContain(userId + \":\" + blogId);\n }\n}",
"language": "java",
"sub_questions": [
{
"sub_id": "cr-001-1",
"type": "single_choice",
"question": "thumbBloomFilter 没有标注 @PostConstruct 以外的任何并发控制注解。在多线程环境下直接调用 add() 和 mightContain(),以下哪种说法正确?",
"options": {
"A": "Guava BloomFilter 内部使用了锁,线程安全,无需额外保护",
"B": "Guava BloomFilter 不是线程安全的,但位数组的原子性写入在实践中不会导致严重问题",
"C": "必须在 add() 和 mightContain() 方法上加 synchronized,否则会出现数据竞争",
"D": "BloomFilter 在多线程环境下会直接抛出 ConcurrentModificationException"
},
"answer": "B",
"explanation": "Guava BloomFilter 不是线程安全的(官方文档明确说明)。但由于 BloomFilter 的底层是一个 bitset,put 和 mightContain 操作都是对单个 bit 的读写,在大多数 JVM 实现中对 long 数组元素的读写是原子的(64 位 JVM),实践中通常不会导致严重问题(可能出现少量误判率上升)。但这不等于它是「线程安全的」——理论上仍存在可见性问题。选项 A 错误,Guava 没有内置锁;选项 C 虽然安全但不是必须的;选项 D 错误,BloomFilter 不会抛此异常。",
"index": 1
},
{
"sub_id": "cr-001-2",
"type": "short_answer",
"question": "init() 方法在 @PostConstruct 中执行,用 Funnels.stringFunnel 创建了布隆过滤器。如果需要支持不同用户维度的独立布隆过滤器(例如按年份隔离),你会如何修改这个设计?简要说明方案。",
"answer": "将 thumbBloomFilter 从单个实例改为 Map<String, BloomFilter<String>>,以维度标识(如年份)为 key,每个 BloomFilter 独立维护 expectedInsertions 和 FPP。也可以改为懒加载模式,在首次访问某维度时动态创建对应的 BloomFilter 实例。",
"keywords": [
"Map",
"多实例",
"按维度隔离",
"懒加载",
"dynamic creation"
],
"scoring_rubric": "评分标准:提出使用 Map 或类似容器存储多个 BloomFilter 实例(2分);说明维度标识(如年份)作为 key 的设计(1分);提及懒加载或动态创建策略(1分);答案结构清晰、表述准确(1分)",
"index": 2,
"explanation": "init() 方法在 @PostConstruct 中执行,用 Funnels.stringFunnel 创建了布隆过滤器。如果需要支持不同用户维度的独立布隆过滤器(例如按年份隔离),你会如何修改这个设计?简要说明方案。"
}
],
"source": null,
"related": [],
"explanation": "阅读以下 BloomFilterManager 代码片段,回答后续问题:"
},
{
"id": "cr-002",
"type": "code_reading",
"difficulty": 4,
"tags": [
"cache-protection",
"mutex-lock",
"null-cache"
],
"question": "阅读以下 CacheManager.get() 方法的缓存穿透与击穿防护部分,回答后续问题:",
"code": "private final ConcurrentHashMap<String, Object> lockMap = new ConcurrentHashMap<>();\nprivate static final Object NULL_PLACEHOLDER = new Object();\nprivate static final long NULL_CACHE_TTL_SECONDS = 30;\nprivate final Cache<String, Object> nullValueCache = Caffeine.newBuilder()\n .maximumSize(10000)\n .expireAfterWrite(NULL_CACHE_TTL_SECONDS, TimeUnit.SECONDS)\n .build();\n\npublic Object get(String hashKey, String key) {\n String compositeKey = this.buildCacheKey(hashKey, key);\n // 1. 查本地缓存\n Object value = localCache.getIfPresent(compositeKey);\n if (value != null) { hotKeyDetector.add(key, 1); return value; }\n // 2. 查空值短缓存\n Object nullMarker = nullValueCache.getIfPresent(compositeKey);\n if (nullMarker != null) { return null; }\n // 3. 加互斥锁\n Object lock = lockMap.computeIfAbsent(compositeKey, k -> new Object());\n synchronized (lock) {\n // double-check\n value = localCache.getIfPresent(compositeKey);\n if (value != null) { hotKeyDetector.add(key, 1); return value; }\n // 4. 查 Redis\n Object redisValue = redisTemplate.opsForHash().get(hashKey, key);\n if (redisValue == null) {\n nullValueCache.put(compositeKey, NULL_PLACEHOLDER);\n return null;\n }\n AddResult addResult = hotKeyDetector.add(key, 1);\n if (addResult.isHotKey()) { localCache.put(compositeKey, redisValue); }\n return redisValue;\n }\n}",
"language": "java",
"sub_questions": [
{
"sub_id": "cr-002-1",
"type": "single_choice",
"question": "当 Redis 返回的 redisValue 为 null 时,代码执行了 nullValueCache.put(compositeKey, NULL_PLACEHOLDER)。但此时并没有对 nullValueCache 做 double-check。这意味着在极端并发场景下可能出现什么问题?",
"options": {
"A": "多个线程同时 put 相同的 NULL_PLACEHOLDER,导致 nullValueCache 中出现重复条目",
"B": "不存在问题,因为 Caffeine 是线程安全的,且 NULL_PLACEHOLDER 是常量",
"C": "可能导致短时间内多个线程同时穿透到 Redis 查询同一个不存在的 key",
"D": "会导致 nullValueCache 的 TTL 被重置,延长空值缓存时间"
},
"answer": "C",
"explanation": "在加锁之前,线程已经检查了 nullValueCache(步骤 2)。但在步骤 4(锁内)发现 Redis 返回 null 后才 put 空值。如果多个线程几乎同时进入 synchronized 块(对不同 compositeKey),它们会在各自的锁释放后才将 NULL_PLACEHOLDER 写入 nullValueCache。但对于同一个 compositeKey,由于 synchronized 锁的存在,只有一个线程能进入锁内执行 put,后续线程会在 double-check 中命中本地缓存或重新从 Redis 读取(此时仍为 null)再 put。所以对同一个 key,最多有两次无效的 Redis 查询(第一个进入锁的 + double-check 命中本地缓存前的一个)。选项 A 不对,Caffeine put 不会产生「重复条目」;选项 B 虽然 Caffeine 线程安全,但 put 在锁内,不影响并发穿透的可能性;选项 D 不成立,TTL 是基于首次写入时间的。",
"index": 1
},
{
"sub_id": "cr-002-2",
"type": "single_choice",
"question": "代码中步骤 2 在加锁之前检查了 nullValueCache,但步骤 4 中又在锁内将 null 值写入 nullValueCache。以下哪种设计改进可以减少对 Redis 的无效查询?",
"options": {
"A": "将 nullValueCache 的检查移到 synchronized 块内部",
"B": "将 nullValueCache.put 移到 synchronized 块外部,紧跟 Redis 查询返回 null 之后",
"C": "在步骤 2 和步骤 4 之间增加一层对 nullValueCache 的二次检查(在锁内、Redis 查询前)",
"D": "删除步骤 2 的 nullValueCache 检查,全部依赖锁内的逻辑"
},
"answer": "C",
"explanation": "当前流程中,线程 A 在锁内查 Redis 得到 null 并 put nullValueCache 后释放锁。但如果线程 B 在线程 A 获得锁之前已经通过了步骤 2 的 nullValueCache 检查(此时 nullValueCache 尚无数据),线程 B 会在锁上等待。线程 A 完成后,线程 B 获得锁,double-check 本地缓存未命中(因为 null 值未放入本地缓存),然后查 Redis(仍为 null)。如果在 synchronized 块内、Redis 查询之前再做一次 nullValueCache 检查,线程 B 就能在锁内直接返回 null,避免重复查询 Redis。选项 A 会让所有线程串行化查 nullValueCache,降低性能;选项 B 将 put 移到锁外并不能减少 Redis 查询;选项 D 去掉前置检查会增加所有请求的延迟。",
"index": 2
}
],
"source": null,
"related": [],
"explanation": "阅读以下 CacheManager.get() 方法的缓存穿透与击穿防护部分,回答后续问题:"
},
{
"id": "cr-003",
"type": "code_reading",
"difficulty": 4,
"tags": [
"cache-protection",
"bloom-filter",
"null-cache"
],
"question": "阅读以下 ThumbServiceImpl.hasThumb() 方法,结合 BloomFilterManager 和 CacheManager 的上下文,回答后续问题:",
"code": "public Boolean hasThumb(Long blogId, Long userId) {\n if (!bloomFilterManager.mightContain(userId, blogId)) {\n return false; // 布隆过滤器拦截\n }\n Object thumbIdObj = cacheManager.get(\n ThumbConstant.USER_THUMB_KEY_PREFIX + userId, blogId.toString());\n if (thumbIdObj == null) return false;\n Long thumbId = ((Number) thumbIdObj).longValue();\n return !thumbId.equals(ThumbConstant.UN_THUMB_CONSTANT);\n}",
"language": "java",
"sub_questions": [
{
"sub_id": "cr-003-1",
"type": "single_choice",
"question": "假设布隆过滤器的误判率为 1%(FPP=0.01),且有 90% 的 blogId 确实存在点赞记录。在一次查询中,布隆过滤器返回 true 但该 blogId 实际不存在点赞记录的概率约为多少?",
"options": {
"A": "1%",
"B": "0.1%",
"C": "10%",
"D": "无法确定,取决于具体的哈希函数"
},
"answer": "B",
"explanation": "FPP=0.01 意味着在元素不存在时,mightContain 返回 true 的概率为 1%。实际不存在点赞记录的 blogId 占比为 10%(1-90%)。因此,查询一个不存在的 blogId 且被布隆过滤器误判为存在的联合概率 = 10% × 1% = 0.1%。换言之,所有查询中约有 0.1% 会因为布隆过滤器误判而穿透到 CacheManager 层。选项 A 是 FPP 本身,不是联合概率;选项 C 是不存在记录的比例;选项 D 虽然理论上有影响,但在实践中 0.1% 是合理的估算。",
"index": 1
},
{
"sub_id": "cr-003-2",
"type": "short_answer",
"question": "分析 hasThumb() 方法中的防御层次:布隆过滤器拦截了什么?CacheManager 又分别用哪些机制防护?各层之间是否存在冗余或遗漏?",
"answer": "布隆过滤器拦截了「一定不存在」的 key,避免其进入缓存查询链路。CacheManager 用三层防护:(1) 本地缓存命中则直接返回,避免查 Redis;(2) nullValueCache 缓存空值 30 秒,防止对不存在数据的反复 Redis 查询(防穿透);(3) synchronized 互斥锁 + double-check,防止热点 key 过期时的缓存击穿。两层之间存在少量重叠:布隆过滤器的 false positive 会漏过少量不存在的 key,但 nullValueCache 会接住它们,形成互补而非冗余。潜在遗漏:布隆过滤器的误判数据没有回写到布隆过滤器中(即只读不写),且 nullValueCache 的 TTL 较短,30 秒后同一不存在的 key 仍可能再次穿透。",
"keywords": [
"布隆过滤器前置拦截",
"本地缓存",
"nullValueCache 空值缓存",
"synchronized 互斥锁",
"double-check",
"互补关系",
"false positive 漏网"
],
"scoring_rubric": "评分标准:准确说明布隆过滤器拦截「一定不存在」的 key(1分);列出 CacheManager 的三层防护机制(本地缓存、空值缓存、互斥锁)(2分);分析各层之间的互补/重叠关系(1分);指出潜在遗漏或改进点(1分)",
"index": 2,
"explanation": "分析 hasThumb() 方法中的防御层次:布隆过滤器拦截了什么?CacheManager 又分别用哪些机制防护?各层之间是否存在冗余或遗漏?"
}
],
"source": null,
"related": [],
"explanation": "阅读以下 ThumbServiceImpl.hasThumb() 方法,结合 BloomFilterManager 和 CacheManager 的上下文,回答后续问题:"
}
]
}