{ "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": [] } ] }