151 lines
13 KiB
JSON
151 lines
13 KiB
JSON
{
|
||
"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 的上下文,回答后续问题:"
|
||
}
|
||
]
|
||
} |