{ "topic": "distributed-lock", "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": [ "distributed-lock", "redis-lock" ], "question": "阅读以下 RedisLockUtil 的解锁 Lua 脚本和 unlock 方法代码,回答子问题。", "code": "// Lua 脚本:解锁时验证锁持有者\nprivate static final DefaultRedisScript UNLOCK_SCRIPT = new DefaultRedisScript<>(\"\"\"\n if redis.call('get', KEYS[1]) == ARGV[1] then\n return redis.call('del', KEYS[1])\n else\n return 0\n end\n \"\"\", Long.class);\n\npublic boolean unlock(String lockKey) {\n String fullLockKey = buildLockKey(lockKey);\n String lockValue = LOCK_VALUE_HOLDER.get();\n Long result = stringRedisTemplate.execute(UNLOCK_SCRIPT,\n Collections.singletonList(fullLockKey), lockValue);\n LOCK_VALUE_HOLDER.remove();\n return Long.valueOf(1L).equals(result);\n}", "language": "java", "sub_questions": [ { "index": 1, "type": "single_choice", "question": "如果线程 A 获取锁后因 GC 暂停导致锁过期,线程 B 随后获取了同名锁,此时线程 A 恢复执行调用 unlock,Lua 脚本的返回值是什么?", "options": { "A": "1,线程 A 成功删除了线程 B 的锁", "B": "0,因为 KEYS[1] 的值已经是线程 B 的 lockValue,与 ARGV[1] 不匹配", "C": "抛出异常", "D": "返回 nil" }, "answer": "B", "explanation": "这是经典的 GC 停顿导致的安全隐患。线程 A 的锁因 TTL 过期被 Redis 自动删除后,线程 B 获取锁并写入自己的 lockValue。线程 A 恢复后调用 unlock,Lua 脚本 GET 到的是线程 B 的 lockValue,与线程 A 的 lockValue(ARGV[1])不匹配,条件不成立,执行 else 分支返回 0。这意味着 unlock 不会误删线程 B 的锁。但问题在于:线程 A 在锁已失效后仍认为自己持有锁,继续执行临界区代码(如重复点赞),这才是真正的风险所在。" }, { "index": 2, "type": "single_choice", "question": "为什么解锁操作要在 Lua 脚本执行完毕后才调用 LOCK_VALUE_HOLDER.remove(),而不是在执行前?", "options": { "A": "没有区别,顺序无所谓", "B": "确保如果 Lua 脚本执行失败,ThreadLocal 中仍保留 lockValue 以便后续重试", "C": "Lua 脚本需要通过 ThreadLocal 读取 lockValue", "D": "remove 必须在 execute 之后才能释放数据库连接" }, "answer": "B", "explanation": "虽然在当前代码中,如果 Lua 脚本执行成功但 remove 失败(极端情况),ThreadLocal 泄露的影响有限。但如果反过来先 remove 再执行 Lua 脚本,一旦 Lua 脚本因网络异常等原因失败,lockValue 已经丢失,后续将无法重试解锁。保持 '先执行、后清理' 的顺序符合防御性编程原则:确保清理操作不会影响核心业务逻辑的执行。另外,从 ThreadLocal 的生命周期管理角度,remove 放在 finally 块中会更安全(当前代码放在 finally 之外,如果 execute 抛异常则不会 remove)。" } ], "explanation": "本题考查对 Redis 分布式锁解锁机制的理解,包括 Lua 脚本的原子性保证、GC 停顿场景下的安全性分析,以及 ThreadLocal 清理时机的设计考量。", "source": null, "related": [] }, { "id": "cr-002", "type": "code_reading", "difficulty": 4, "tags": [ "distributed-lock", "redis-lock" ], "question": "阅读以下 tryLock 方法和 doThumb 方法的代码,回答子问题。", "code": "public boolean tryLock(String lockKey, long acquireTimeout, long lockTimeout) {\n String fullLockKey = buildLockKey(lockKey);\n String lockValue = generateLockValue(); // UUID + threadId\n long startTime = System.currentTimeMillis();\n while (true) {\n Boolean result = stringRedisTemplate.opsForValue()\n .setIfAbsent(fullLockKey, lockValue, lockTimeout, TimeUnit.MILLISECONDS);\n if (Boolean.TRUE.equals(result)) {\n LOCK_VALUE_HOLDER.set(lockValue);\n return true;\n }\n if (System.currentTimeMillis() - startTime >= acquireTimeout) return false;\n Thread.sleep(RETRY_INTERVAL); // 50ms\n }\n}\n\n// ThumbServiceImpl 中的使用\npublic Boolean doThumb(DoThumbRequest doThumbRequest, HttpServletRequest request) {\n String lockKey = \"USERID:\" + userId;\n return redisLockUtil.executeWithLock(lockKey, () -> {\n return transactionTemplate.execute(status -> {\n Boolean exists = this.hasThumb(blogId, userId);\n if (exists) throw new BusinessException(ErrorCode.OPERATION_ERROR, \"用户已点赞\");\n // ... 更新数据库、Redis、布隆过滤器 ...\n });\n });\n}", "language": "java", "sub_questions": [ { "index": 1, "type": "single_choice", "question": "tryLock 中 Thread.sleep(RETRY_INTERVAL) 的自旋重试策略在高并发场景下有什么缺点?", "options": { "A": "sleep 时间太短(50ms),CPU 空转导致资源浪费", "B": "固定的 50ms 重试间隔无法自适应负载,可能导致大量线程在锁释放瞬间同时竞争(惊群效应)", "C": "sleep 会导致线程永久阻塞", "D": "retry 间隔必须等于 lockTimeout 的 1/200" }, "answer": "B", "explanation": "固定间隔自旋重试的核心问题是惊群效应(thundering herd):当锁释放时,所有等待的线程会在下一个 50ms 周期几乎同时尝试获取锁,只有一个成功,其余全部失败并再等 50ms。更好的方案包括:(1) 随机化重试间隔(jitter);(2) 使用 Redis 的发布/订阅(Pub/Sub)通知锁释放事件;(3) 使用 Redisson 等成熟框架的信号量机制。A 选项描述不准确,50ms 的 sleep 期间线程不占 CPU。C 选项错误,sleep 有超时且可被中断。D 选项无依据。" }, { "index": 2, "type": "short_answer", "question": "doThumb 方法中,分布式锁的粒度是按 userId 设置的。这种设计有什么优点和局限?如果两个不同用户同时对同一篇博客点赞,锁会互斥吗?", "answer": "优点:锁粒度是用户级别,同一用户的并发点赞操作被串行化,避免同一用户重复点赞或并发导致数据不一致。局限:不同用户对同一博客的点赞操作也会因为使用 executeWithLock 时 lockKey 为各自 userId 而不互斥,这其实是正确的——不同用户的点赞可以并行。但 lockKey 是 \"USERID:\" + userId,而不是 \"BLOGID:\" + blogId,意味着同一用户对不同博客的点赞会被互斥(串行),这是过度限制。两个不同用户同时点赞不会互斥,因为 lockKey 不同。", "keywords": [ "用户级锁粒度", "并发点赞不互斥", "同一用户串行", "不同用户可并行", "过度限制" ], "scoring_rubric": "满分(5分):准确指出锁粒度为 userId 级别(1分),说明同用户并发被串行化是正确设计(1分),说明不同用户点赞不互斥(1分),指出同用户对不同博客过度限制(1分),回答了锁互斥判断(1分)。每少一个要点扣1分,概念错误扣2分。", "explanation": "doThumb 方法中,分布式锁的粒度是按 userId 设置的。这种设计有什么优点和局限?如果两个不同用户同时对同一篇博客点赞,锁会互斥吗?" } ], "explanation": "本题考查 tryLock 的自旋重试机制在高并发下的表现,以及分布式锁粒度设计对业务的影响。", "source": null, "related": [] }, { "id": "cr-003", "type": "code_reading", "difficulty": 5, "tags": [ "distributed-lock", "redis-lock" ], "question": "阅读以下 executeWithLock 方法和旧方案对比代码,回答子问题。", "code": "// 新方案:Redis 分布式锁\npublic T executeWithLock(String lockKey, LockAction action) {\n boolean locked = false;\n try {\n locked = tryLock(lockKey);\n if (!locked) throw new RuntimeException(\"获取分布式锁失败\");\n return action.execute();\n } finally {\n if (locked) unlock(lockKey);\n }\n}\n\n// 旧方案:synchronized + String.intern()\nsynchronized ((\"LOCK-USERID:\" + loginUser.getId().toString()).intern()) {\n return transactionTemplate.execute(status -> { ... });\n}", "language": "java", "sub_questions": [ { "index": 1, "type": "single_choice", "question": "executeWithLock 中,如果 action.execute() 抛出未检查异常,锁会被释放吗?", "options": { "A": "不会,异常会阻止 finally 块执行", "B": "会,因为 finally 块在 try 块正常或异常退出时都会执行", "C": "取决于异常类型,RuntimeException 会被释放,Checked Exception 不会", "D": "会释放,但 Redis 会记录一个错误日志" }, "answer": "B", "explanation": "Java 语言规范保证 finally 块无论 try 块是正常返回还是抛出异常都会执行。doThumb 中的 BusinessException 是未检查异常,action.execute() 抛出后,finally 块中 if (locked) unlock(lockKey) 会正常执行释放锁。这是 try/finally 的核心价值——确保资源(这里是分布式锁)在异常场景下也能正确清理。选项 A 是初学者常见误解,选项 C 混淆了 Checked/Unchecked Exception 对 finally 的影响,实际上两者都会执行 finally。选项 D 无意义。" }, { "index": 2, "type": "short_answer", "question": "对比新方案(RedisLockUtil.executeWithLock)和旧方案(synchronized + String.intern()),除了跨进程能力外,新方案还有哪些设计优势?请至少列出两点。", "answer": "1. 锁自动过期:Redis 锁设置了 TTL(10秒),即使进程崩溃锁也会自动释放,避免死锁;synchronized 锁依赖 JVM 正常退出或代码显式释放。2. 锁持有者验证:Redis 锁通过 UUID+threadId 作为 value,解锁时用 Lua 脚本验证身份,防止误删其他线程/进程的锁;synchronized 无法做到跨进程的身份验证。3. 可配置超时:tryLock 支持 acquireTimeout 参数,获取锁超时可控制;synchronized 要么获取到,要么阻塞,没有超时机制。4. 业务解耦:executeWithLock 封装了锁逻辑,业务代码通过 LockAction 接口实现,职责分离更清晰;synchronized 将锁逻辑嵌入业务代码中。", "keywords": [ "锁自动过期", "TTL防死锁", "锁持有者验证", "Lua脚本原子性", "acquireTimeout可配置", "职责分离", "LockAction接口" ], "scoring_rubric": "满分(5分):正确列出至少两点设计优势(2分),每点解释合理且准确(每点1.5分,最高3分)。仅说'跨进程'不得分(题目已排除该点)。每列一点但解释不准确扣0.5分。", "explanation": "对比新方案(RedisLockUtil.executeWithLock)和旧方案(synchronized + String.intern()),除了跨进程能力外,新方案还有哪些设计优势?请至少列出两点。" } ], "explanation": "本题考查分布式锁的设计模式对比,包括异常安全性(finally 保证)、自动过期机制、身份验证等方面的设计考量。", "source": null, "related": [] } ] }