143 lines
12 KiB
JSON
143 lines
12 KiB
JSON
{
|
||
"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": []
|
||
}
|
||
]
|
||
} |