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,151 @@
|
||||
{
|
||||
"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 的上下文,回答后续问题:"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"slug": "cache-protection",
|
||||
"name": "缓存穿透与击穿防护",
|
||||
"description": "布隆过滤器、空值短缓存、互斥锁防击穿的三重防护机制",
|
||||
"tags": [
|
||||
"bloom-filter",
|
||||
"cache-protection",
|
||||
"mutex-lock",
|
||||
"null-cache"
|
||||
],
|
||||
"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,84 @@
|
||||
{
|
||||
"topic": "cache-protection",
|
||||
"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": [
|
||||
"cache-protection",
|
||||
"null-cache",
|
||||
"bloom-filter"
|
||||
],
|
||||
"question": "缓存穿透(Cache Penetration)是指查询一个数据库中也不存在的数据,导致请求每次都打到数据库。请结合提供的代码,说明这套系统中有哪些机制可以防止缓存穿透,并分析各机制的优缺点。",
|
||||
"answer": "系统中有两层防穿透机制:\n1. **布隆过滤器(BloomFilterManager)**:在最前置层快速判断 key 是否可能存在,false 则直接返回,减少对缓存层的无效访问。优点:O(1) 时间复杂度,内存占用极低。缺点:存在 false positive(误判),且不支持删除操作,容量固定不能动态扩容。\n2. **空值短缓存(nullValueCache)**:对 Redis 返回 null 的 key,缓存 NULL_PLACEHOLDER 30 秒。优点:实现简单,有效拦截同一 key 的短时间重复穿透。缺点:TTL 过长会浪费内存,过短则防穿透效果有限;不适用于攻击者使用大量随机 key 的场景(会被 Caffeine 淘汰)。\n\n两层互补:布隆过滤器拦截「一定不存在」的 key,nullValueCache 接住布隆过滤器 false positive 漏过的少量 key。",
|
||||
"keywords": [
|
||||
"布隆过滤器",
|
||||
"false positive",
|
||||
"O(1)",
|
||||
"空值缓存",
|
||||
"NULL_PLACEHOLDER",
|
||||
"TTL",
|
||||
"互补"
|
||||
],
|
||||
"scoring_rubric": "评分标准:准确描述布隆过滤器的防穿透原理(1.5分);准确描述 nullValueCache 的防穿透原理(1.5分);分析各机制的优缺点(1分);说明两层机制的互补关系(1分)",
|
||||
"source": null,
|
||||
"related": [],
|
||||
"explanation": "系统中有两层防穿透机制:\n1. **布隆过滤器(BloomFilterManager)**:在最前置层快速判断 key 是否可能存在,false 则直接返回,减少对缓存层的无效访问。优点:O(1) 时间复杂度,内存占用极低。缺点:存在 false positive(误判),且不支持删除操作,容量固定不能动态扩容。\n2. **空值短缓存(nullValueCache)**:对 Redis 返回 null 的 key,缓存 NULL_PLACEHOLDER 30 秒。优点:实现简单,有效拦截同一 key 的短时间重复穿透。缺点:TTL 过长会浪费内存,过短则防穿透效果有限;不适用于攻击者使用大量随机 key 的场景(会被 Caffeine 淘汰)。\n\n两层互补:布隆过滤器拦截「一定不存在」的 key,nullValueCache 接住布隆过滤器 false positive 漏过的少量 key。"
|
||||
},
|
||||
{
|
||||
"id": "sa-002",
|
||||
"type": "short_answer",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"cache-protection",
|
||||
"mutex-lock"
|
||||
],
|
||||
"question": "缓存击穿(Cache Breakdown)是指某个热点 key 在缓存过期的瞬间,大量并发请求同时穿透到后端。请结合 CacheManager.get() 的代码,详细解释互斥锁 + double-check 模式如何解决缓存击穿问题,并指出该方案在多实例部署下的局限性。",
|
||||
"answer": "互斥锁 + double-check 模式的防护流程:\n1. 线程 A 检测到本地缓存未命中,在 lockMap 中通过 computeIfAbsent 为该 key 创建锁对象。\n2. 线程 A 获得 synchronized 锁,执行 double-check(再次检查本地缓存)。\n3. double-check 未命中,线程 A 从 Redis 加载数据,写入本地缓存,释放锁。\n4. 线程 B(在 A 持锁期间等待)获得锁后执行 double-check,此时本地缓存已有数据,直接返回,不查 Redis。\n\n通过这种方式,即使热点 key 同时过期,也只有一个线程穿透到 Redis,其余线程在锁上等待后命中 double-check。\n\n**多实例部署局限性**:lockMap 是 JVM 内存中的 ConcurrentHashMap,锁对象仅在当前 JVM 实例内有效。多实例部署时,实例 A 和实例 B 的线程各自持有独立的锁,无法互斥,仍可能同时穿透到 Redis。需要引入分布式锁(如 Redis SETNX)来解决跨实例的互斥问题。",
|
||||
"keywords": [
|
||||
"互斥锁",
|
||||
"double-check",
|
||||
"本地锁",
|
||||
"computeIfAbsent",
|
||||
"热点key",
|
||||
"多实例局限性",
|
||||
"分布式锁"
|
||||
],
|
||||
"scoring_rubric": "评分标准:准确描述互斥锁的获取和释放流程(1.5分);解释 double-check 的作用和必要性(1.5分);指出多实例部署下本地锁的局限性(1分);提出分布式锁等替代方案(1分)",
|
||||
"source": null,
|
||||
"related": [],
|
||||
"explanation": "互斥锁 + double-check 模式的防护流程:\n1. 线程 A 检测到本地缓存未命中,在 lockMap 中通过 computeIfAbsent 为该 key 创建锁对象。\n2. 线程 A 获得 synchronized 锁,执行 double-check(再次检查本地缓存)。\n3. double-check 未命中,线程 A 从 Redis 加载数据,写入本地缓存,释放锁。\n4. 线程 B(在 A 持锁期间等待)获得锁后执行 double-check,此时本地缓存已有数据,直接返回,不查 Redis。\n\n通过这种方式,即使热点 key 同时过期,也只有一个线程穿透到 Redis,其余线程在锁上等待后命中 double-check。\n\n**多实例部署局限性**:lockMap 是 JVM 内存中的 ConcurrentHashMap,锁对象仅在当前 JVM 实例内有效。多实例部署时,实例 A 和实例 B 的线程各自持有独立的锁,无法互斥,仍可能同时穿透到 Redis。需要引入分布式锁(如 Redis SETNX)来解决跨实例的互斥问题。"
|
||||
},
|
||||
{
|
||||
"id": "sa-003",
|
||||
"type": "short_answer",
|
||||
"difficulty": 5,
|
||||
"tags": [
|
||||
"cache-protection",
|
||||
"bloom-filter",
|
||||
"null-cache",
|
||||
"mutex-lock"
|
||||
],
|
||||
"question": "假设你需要将这套缓存穿透与击穿防护方案从单体应用迁移到微服务架构(10+ 实例),请分析现有方案中哪些组件需要改造,给出具体的改造方案,并评估每种改造的利弊。",
|
||||
"answer": "需要改造的组件及方案:\n\n1. **布隆过滤器 → 分布式布隆过滤器**\n - 现状:每个实例维护独立的 BloomFilter,数据不同步。\n - 方案:使用 Redis Bloom(RedisBloom 模块)或自建基于 Redis 的分布式布隆过滤器,所有实例共享同一个过滤器。\n - 利:数据一致,任意实例都能拦截不存在的 key。弊:引入 Redis 依赖,增加一次网络往返延迟。\n\n2. **本地锁 → 分布式锁**\n - 现状:lockMap 是 JVM 内存锁,无法跨实例互斥。\n - 方案:使用 Redis SETNX 或 Zookeeper 实现分布式锁替代 synchronized。\n - 利:真正实现跨实例互斥。弊:锁获取/释放需要网络通信,延迟增加;需要处理锁超时、续期等复杂场景。\n\n3. **nullValueCache → 共享空值缓存或保留本地**\n - 现状:每个实例独立维护空值缓存,可能导致多实例同时穿透。\n - 方案 A:将 nullValueCache 也存入 Redis(带短 TTL),实现跨实例共享。利:一致性好。弊:增加 Redis 读写压力。\n - 方案 B:保持本地 nullValueCache 不变,接受少量重复穿透。利:简单、无额外网络开销。弊:一致性略差。\n\n4. **本地缓存 → 分布式缓存或本地+二级缓存架构**\n - 需要评估是否保留本地缓存层,或使用 Caffeine + Redis 的二级缓存架构,通过消息总线(如 Redis Pub/Sub 或 Kafka)实现缓存失效通知。\n\n综合评估:改造的核心权衡是「一致性 vs 延迟/复杂度」。布隆过滤器和互斥锁建议优先改造为分布式方案,空值缓存可根据业务容忍度选择保留本地或共享。",
|
||||
"keywords": [
|
||||
"分布式布隆过滤器",
|
||||
"Redis Bloom",
|
||||
"分布式锁",
|
||||
"SETNX",
|
||||
"空值缓存共享",
|
||||
"二级缓存",
|
||||
"一致性",
|
||||
"延迟"
|
||||
],
|
||||
"scoring_rubric": "评分标准:准确识别布隆过滤器、本地锁、空值缓存三个需要改造的组件(1分);为布隆过滤器提出分布式方案(如 Redis Bloom)(1分);为互斥锁提出分布式锁方案(如 SETNX)(1分);为空值缓存提出两种可选方案并分析利弊(1分);整体分析有结构,权衡合理,结论清晰(1分)",
|
||||
"source": null,
|
||||
"related": [],
|
||||
"explanation": "需要改造的组件及方案:\n\n1. **布隆过滤器 → 分布式布隆过滤器**\n - 现状:每个实例维护独立的 BloomFilter,数据不同步。\n - 方案:使用 Redis Bloom(RedisBloom 模块)或自建基于 Redis 的分布式布隆过滤器,所有实例共享同一个过滤器。\n - 利:数据一致,任意实例都能拦截不存在的 key。弊:引入 Redis 依赖,增加一次网络往返延迟。\n\n2. **本地锁 → 分布式锁**\n - 现状:lockMap 是 JVM 内存锁,无法跨实例互斥。\n - 方案:使用 Redis SETNX 或 Zookeeper 实现分布式锁替代 synchronized。\n - 利:真正实现跨实例互斥。弊:锁获取/释放需要网络通信,延迟增加;需要处理锁超时、续期等复杂场景。\n\n3. **nullValueCache → 共享空值缓存或保留本地**\n - 现状:每个实例独立维护空值缓存,可能导致多实例同时穿透。\n - 方案 A:将 nullValueCache 也存入 Redis(带短 TTL),实现跨实例共享。利:一致性好。弊:增加 Redis 读写压力。\n - 方案 B:保持本地 nullValueCache 不变,接受少量重复穿透。利:简单、无额外网络开销。弊:一致性略差。\n\n4. **本地缓存 → 分布式缓存或本地+二级缓存架构**\n - 需要评估是否保留本地缓存层,或使用 Caffeine + Redis 的二级缓存架构,通过消息总线(如 Redis Pub/Sub 或 Kafka)实现缓存失效通知。\n\n综合评估:改造的核心权衡是「一致性 vs 延迟/复杂度」。布隆过滤器和互斥锁建议优先改造为分布式方案,空值缓存可根据业务容忍度选择保留本地或共享。"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"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<String, Object>,且锁对象通过 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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user