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

This commit is contained in:
2026-09-09 21:59:46 +08:00
parent 3447dbd4ee
commit 85710ded73
21 changed files with 2222 additions and 0 deletions
@@ -0,0 +1,143 @@
{
"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": []
}
]
}
+29
View File
@@ -0,0 +1,29 @@
{
"slug": "distributed-lock",
"name": "单机锁与分布式锁",
"description": "synchronized到Redis SETNX演进、Lua脚本释放锁、锁续期问题",
"tags": [
"distributed-lock",
"lock-design",
"redis-lock"
],
"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,77 @@
{
"topic": "distributed-lock",
"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": [
"distributed-lock",
"lock-design"
],
"question": "请解释 RedisLockUtil 中 DEFAULT_LOCK_TIMEOUT(10秒)和 DEFAULT_ACQUIRE_TIMEOUT(3秒)两个参数的区别,以及它们分别在什么场景下需要调整。",
"answer": "DEFAULT_LOCK_TIMEOUT(10秒)是锁的持有时间上限,即 Redis key 的 TTL。业务必须在此时间内完成操作,否则锁自动释放,可能导致并发问题。如果业务逻辑执行时间较长(如涉及远程调用、大量数据处理),需要适当增大该值。DEFAULT_ACQUIRE_TIMEOUT(3秒)是等待获取锁的最长时间,即客户端自旋等待的上限。在高并发场景下,如果多个客户端竞争同一把锁,超过此时间未获取到就返回失败。如果希望提高请求成功率而非快速失败,可以增大该值,但会增加线程阻塞时间。",
"keywords": [
"锁持有时间",
"TTL",
"锁等待时间",
"acquireTimeout",
"业务执行时间",
"高并发",
"自旋等待"
],
"scoring_rubric": "满分(5分):准确区分两个超时的含义(1分),说明 lockTimeout 是锁 TTL 自动释放(1分),说明 acquireTimeout 是自旋等待上限(1分),各给出一个调整场景(1分),区分概念清晰无混淆(1分)。混淆两者含义扣3分,只回答一个扣2分。",
"explanation": "DEFAULT_LOCK_TIMEOUT(10秒)是锁的持有时间上限,即 Redis key 的 TTL。业务必须在此时间内完成操作,否则锁自动释放,可能导致并发问题。如果业务逻辑执行时间较长(如涉及远程调用、大量数据处理),需要适当增大该值。DEFAULT_ACQUIRE_TIMEOUT(3秒)是等待获取锁的最长时间,即客户端自旋等待的上限。在高并发场景下,如果多个客户端竞争同一把锁,超过此时间未获取到就返回失败。如果希望提高请求成功率而非快速失败,可以增大该值,但会增加线程阻塞时间。"
},
{
"id": "sa-002",
"type": "short_answer",
"difficulty": 4,
"tags": [
"distributed-lock",
"redis-lock"
],
"question": "RedisLockUtil 使用 setIfAbsent(SET NX)实现锁的获取。请解释该方案的\"安全性\"(Safety)和\"活性\"(Liveness)分别由什么保证,以及各自的局限是什么。",
"answer": "安全性(Safety)——互斥性保证:由 Redis 的 SET NX 原子操作保证。NX 参数确保只有当 key 不存在时才能设置成功,从而保证同一时刻最多只有一个客户端持有锁。局限:在 Redis 主从架构下,主节点写入锁后未同步到从节点就宕机,从节点提升为主后锁丢失,安全性被破坏。活性(Liveness)——最终可获取性保证:由锁的 TTL(lockTimeout)自动过期保证。即使持有锁的客户端崩溃或网络分区,锁最终会过期释放,其他客户端可以获取。局限:如果业务执行时间超过 TTL,锁提前释放,安全性又被打破(活性的保证反而威胁了安全性)。这是一个经典的两难困境,也是 Redisson 引入看门狗(watchdog)自动续期机制的原因。",
"keywords": [
"SET NX原子操作",
"互斥性",
"主从同步",
"TTL自动过期",
"锁续期",
"watchdog",
"活性与安全性的权衡"
],
"scoring_rubric": "满分(5分):准确解释安全性由 SET NX 保证(1分),说明主从架构下的局限(1分),准确解释活性由 TTL 保证(1分),说明业务超时与安全性的矛盾(1分),提及看门狗/续期机制作为解决方案(1分)。仅回答一半内容扣2分,概念错误扣3分。",
"explanation": "安全性(Safety)——互斥性保证:由 Redis 的 SET NX 原子操作保证。NX 参数确保只有当 key 不存在时才能设置成功,从而保证同一时刻最多只有一个客户端持有锁。局限:在 Redis 主从架构下,主节点写入锁后未同步到从节点就宕机,从节点提升为主后锁丢失,安全性被破坏。活性(Liveness)——最终可获取性保证:由锁的 TTL(lockTimeout)自动过期保证。即使持有锁的客户端崩溃或网络分区,锁最终会过期释放,其他客户端可以获取。局限:如果业务执行时间超过 TTL,锁提前释放,安全性又被打破(活性的保证反而威胁了安全性)。这是一个经典的两难困境,也是 Redisson 引入看门狗(watchdog)自动续期机制的原因。"
},
{
"id": "sa-003",
"type": "short_answer",
"difficulty": 5,
"tags": [
"distributed-lock",
"lock-design"
],
"question": "如果要将 RedisLockUtil 升级为支持看门狗自动续期和可重入的生产级分布式锁,需要做哪些改造?请从架构设计角度列出至少三项关键改造点并简述实现思路。",
"answer": "1. 看门狗自动续期(Watchdog):在后台启动一个定时任务(如每 lockTimeout/3 时间),检查当前线程是否仍持有锁,如果持有则续期(重新 SET EX)。Redisson 的做法是在获取锁成功后启动一个 Netty 时间轮,定时续期,锁释放时取消定时器。2. 可重入锁支持:当前实现不支持可重入。改造思路是用 Hash 结构存储锁信息(field=锁持有者标识,value=重入次数),获取锁时如果已是持有者则 incr 重入计数,解锁时 decr,计数归零才真正删除 key。Lua 脚本需要相应改造以支持重入逻辑。3. 看门狗参数可配置化:当前的 lockTimeout 硬编码为 10 秒,应支持自定义续期间隔、最大续期时间等参数,以适配不同业务场景。4. 公平锁支持:当前实现是非公平锁(先到不一定先得),可引入 Redis 有序集合(Sorted Set)实现 FIFO 队列,按请求时间戳排序获取锁。5. 线程安全优化:移除 ThreadLocal 存储 lockValue 的设计,改为将 lockValue 作为 tryLock 的返回值传递给 unlock,避免线程池复用导致的 lockValue 错乱问题。",
"keywords": [
"看门狗watchdog",
"自动续期",
"可重入锁",
"Hash结构",
"重入计数",
"Lua脚本改造",
"公平锁",
"Sorted Set",
"参数可配置",
"ThreadLocal替代"
],
"scoring_rubric": "满分(5分):列出至少三项改造点(1分),每项有实现思路描述(每项1分,共3分),至少一项提到 Redisson 作为参考实现(0.5分),回答逻辑清晰、无概念错误(0.5分)。只列改造点无实现思路扣2分,概念错误(如说可重入用 List 实现)扣2分。",
"explanation": "1. 看门狗自动续期(Watchdog):在后台启动一个定时任务(如每 lockTimeout/3 时间),检查当前线程是否仍持有锁,如果持有则续期(重新 SET EX)。Redisson 的做法是在获取锁成功后启动一个 Netty 时间轮,定时续期,锁释放时取消定时器。2. 可重入锁支持:当前实现不支持可重入。改造思路是用 Hash 结构存储锁信息(field=锁持有者标识,value=重入次数),获取锁时如果已是持有者则 incr 重入计数,解锁时 decr,计数归零才真正删除 key。Lua 脚本需要相应改造以支持重入逻辑。3. 看门狗参数可配置化:当前的 lockTimeout 硬编码为 10 秒,应支持自定义续期间隔、最大续期时间等参数,以适配不同业务场景。4. 公平锁支持:当前实现是非公平锁(先到不一定先得),可引入 Redis 有序集合(Sorted Set)实现 FIFO 队列,按请求时间戳排序获取锁。5. 线程安全优化:移除 ThreadLocal 存储 lockValue 的设计,改为将 lockValue 作为 tryLock 的返回值传递给 unlock,避免线程池复用导致的 lockValue 错乱问题。"
}
]
}
@@ -0,0 +1,168 @@
{
"topic": "distributed-lock",
"type": "single_choice",
"schema_version": "1.0.0",
"generated": "2026-09-09T21:42:00+08:00",
"questions": [
{
"id": "sc-001",
"type": "single_choice",
"difficulty": 1,
"tags": [
"distributed-lock",
"redis-lock"
],
"question": "在 RedisLockUtil 中,setIfAbsent 方法的底层对应的 Redis 命令是什么?",
"options": {
"A": "SET key value",
"B": "SETNX key value",
"C": "SET key value NX EX ttl",
"D": "INCR key"
},
"answer": "C",
"explanation": "Spring Data Redis 的 setIfAbsent(key, value, timeout, unit) 底层会拼接 NX(不存在才设置)和 EX/PX(过期时间)参数,等价于 Redis 的 SET key value NX EX ttl 原子命令。SETNX 命令(B 选项)是 Redis 2.6.12 之前的旧方式,不具备原子性地设置过期时间的能力;单纯 SET(A 选项)不带 NX 参数会直接覆盖已有值。",
"source": null,
"related": []
},
{
"id": "sc-002",
"type": "single_choice",
"difficulty": 2,
"tags": [
"distributed-lock",
"redis-lock"
],
"question": "RedisLockUtil 中 unlock 方法使用 Lua 脚本的核心原因是什么?",
"options": {
"A": "提高执行性能,减少网络往返",
"B": "保证 get 和 del 操作的原子性,防止误删其他线程的锁",
"C": "避免 Redis 主从切换导致锁丢失",
"D": "支持可重入锁的实现"
},
"answer": "B",
"explanation": "unlock 中的 Lua 脚本先 GET 锁的值,与当前线程持有的 lockValue 比较,匹配才 DEL。如果不使用 Lua 脚本,GET 和 DEL 是两条独立命令,在两步之间锁可能已过期并被其他线程获取,此时 DEL 会误删新线程的锁。Lua 脚本在 Redis 中是原子执行的,消除了这个竞态条件。选项 C 是 Redis 锁的已知局限(红锁解决),不是使用 Lua 脚本的原因;D 需要额外设计,不是 Lua 脚本本身的功能。",
"source": null,
"related": []
},
{
"id": "sc-003",
"type": "single_choice",
"difficulty": 2,
"tags": [
"distributed-lock",
"synchronized"
],
"question": "旧方案 synchronized + String.intern() 的主要缺陷是什么?",
"options": {
"A": "String.intern() 会污染永久代/元空间,导致 OOM",
"B": "synchronized 无法跨 JVM 进程工作,在分布式部署下无效",
"C": "String.intern() 返回的是不可变引用,无法释放锁",
"D": "synchronized 是公平锁,性能极差"
},
"answer": "B",
"explanation": "synchronized 是 JVM 级别的内置锁,只能在同一 JVM 进程内的线程之间互斥。在分布式部署(多实例)场景下,不同 JVM 中的线程持有各自的 monitor,无法实现全局互斥,可能导致重复点赞等并发问题。虽然 A 选项(intern 污染字符串常量池)也是一个缺点,但它不是迁移到分布式锁的根本原因。C 选项错误,intern 返回的字符串可以正常 GC。D 选项错误,synchronized 默认是非公平锁。",
"source": null,
"related": []
},
{
"id": "sc-004",
"type": "single_choice",
"difficulty": 3,
"tags": [
"distributed-lock",
"lock-design"
],
"question": "RedisLockUtil 中的锁超时时间(DEFAULT_LOCK_TIMEOUT = 10000L)的作用是什么?如果业务执行超过该时间会发生什么?",
"options": {
"A": "锁永远不会释放,需要手动调用 unlock",
"B": "Redis 自动删除锁 key,其他线程可获取锁,当前线程仍持有 lockValue 但实际已失去保护",
"C": "Redis 会向当前线程发送中断信号",
"D": "锁会自动续期,直到业务完成"
},
"answer": "B",
"explanation": "DEFAULT_LOCK_TIMEOUT 是 Redis 锁 key 的过期时间(TTL),由 SET 命令的 EX 参数控制。当业务执行时间超过该 TTL,Redis 会自动删除锁 key,此时其他线程可以成功获取锁,破坏互斥性。而当前线程的 ThreadLocal 中仍保留着 lockValue,但该值在 Redis 中已不存在,Lua 脚本的 get 比较会失败。这正是没有看门狗(watchdog)机制的经典问题——可参考 Redisson 的自动续期方案。该实现没有锁续期机制。",
"source": null,
"related": []
},
{
"id": "sc-005",
"type": "single_choice",
"difficulty": 3,
"tags": [
"distributed-lock",
"lock-design"
],
"question": "tryLock 方法中使用 ThreadLocal 存储 lockValue 的设计有什么潜在问题?",
"options": {
"A": "ThreadLocal 存储的值会被其他线程读取,导致数据竞争",
"B": "如果 tryLock 成功但 unlock 未被调用(异常路径),ThreadLocal 不会被清理,可能导致线程池复用时锁泄露",
"C": "ThreadLocal 不支持序列化,无法在分布式环境使用",
"D": "ThreadLocal 会在每次 set 时触发 GC,影响性能"
},
"answer": "B",
"explanation": "executeWithLock 方法虽然在 finally 中调用了 unlock(会清理 ThreadLocal),但如果直接调用 tryLock 后忘记调用 unlock,或者中间发生未捕获的异常导致 unlock 未执行,ThreadLocal 中的 lockValue 就不会被 remove。在使用线程池的场景下,线程被复用时旧的 lockValue 仍在,可能导致后续锁操作使用错误的值。虽然 executeWithLock 封装了 try/finally,但作为一个独立工具类,这仍是设计上的隐患。实际生产中通常将 lockValue 作为方法返回值而非存储在 ThreadLocal 中。",
"source": null,
"related": []
},
{
"id": "sc-006",
"type": "single_choice",
"difficulty": 4,
"tags": [
"distributed-lock",
"redis-lock"
],
"question": "在 Redis 主从架构下,RedisLockUtil 的 tryLock 方法存在什么已知缺陷?",
"options": {
"A": "主节点宕机后从节点提升为主,但锁数据可能未同步,导致多个客户端同时获得锁",
"B": "从节点不支持 SETNX 命令",
"C": "主从同步延迟会导致 setIfAbsent 返回 false 但实际已写入",
"D": "哨兵模式下无法连接 Redis"
},
"answer": "A",
"explanation": "这是 Redis 单节点/主从模式分布式锁的经典问题(Martin Kleppmann 在分析 Redisson 时指出):客户端 A 在主节点写入锁 key,但主节点在同步到从节点之前宕机;从节点提升为主后,该锁 key 不存在,客户端 B 可以成功获取锁——此时 A 和 B 同时持有锁,互斥性被打破。这就是 RedLock 算法(Redis 官方提出)试图解决的问题,但其本身也存在争议。选项 B 错误,从节点支持所有写命令(通过主节点转发)。选项 C 描述的是主从异步复制的延迟现象,但不会导致 setIfAbsent 行为异常。",
"source": null,
"related": []
},
{
"id": "sc-007",
"type": "single_choice",
"difficulty": 4,
"tags": [
"distributed-lock",
"lock-design"
],
"question": "generateLockValue 方法返回的是 UUID + threadId 组合。为什么不直接用 userId 作为 lockValue?",
"options": {
"A": "userId 是数字,不能作为 Redis 的 value",
"B": "如果用 userId 作为 lockValue,持有锁的线程无法唯一标识,unlock 时的 Lua 脚本无法区分不同请求来源",
"C": "UUID 生成速度比 userId 快",
"D": "userId 不满足 Redis key 的命名规范"
},
"answer": "B",
"explanation": "unlock 的 Lua 脚本通过比较锁的 value 来判断是否为当前持有者的锁。如果用 userId 作为 lockValue,那么同一用户的多次请求(可能在不同线程甚至不同 JVM 中)会产生相同的 lockValue,导致:(1) 后来的请求的 SETNX 会失败(锁已被自己的旧请求持有);(2) 或者在锁过期后,新请求获取锁后用 userId 解锁时,可能误删已过期但被其他实例获取的锁。UUID + threadId 能保证每个锁持有者有唯一标识,确保 unlock 时精确匹配。选项 A 错误,Redis value 可以是任何字符串。选项 D 错误,lockKey 用 userId,lockValue 是锁的凭证。",
"source": null,
"related": []
},
{
"id": "sc-008",
"type": "single_choice",
"difficulty": 5,
"tags": [
"distributed-lock",
"lock-design"
],
"question": "RedisLockUtil 的 tryLock 方法在获取锁失败时会抛出异常吗?如果 acquireTimeout 内未获取到锁,executeWithLock 会如何表现?",
"options": {
"A": "tryLock 返回 false,executeWithLock 抛出 RuntimeException",
"B": "tryLock 返回 false,executeWithLock 返回 null",
"C": "tryLock 抛出 InterruptedException,executeWithLock 捕获并重试",
"D": "tryLock 会无限等待直到获取锁"
},
"answer": "A",
"explanation": "tryLock 方法在 acquireTimeout(默认 3000ms)内未获取到锁时返回 false(而非抛异常)。executeWithLock 中,locked 变量为 false 时会主动抛出 RuntimeException(\"获取分布式锁失败\")。这意味着获取锁超时被视为错误场景,需要调用方处理该异常。在 doThumb 场景中,用户点赞操作获取锁失败会直接报错,而不是静默返回 false——这是合理的降级策略,避免在高并发下静默失败。",
"source": null,
"related": []
}
]
}