Files
examination/topics/thumbup/distributed-lock/code_reading.json
T

143 lines
12 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": "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<Long> 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> T executeWithLock(String lockKey, LockAction<T> 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": []
}
]
}