--- tags: [test/review, architecture, arch/distributed, redis-distributed-lock] create time: 2026-08-09 12:00 --- # Redis 分布式锁 — 测试题 ## 概述 本测试覆盖 Redis 分布式锁的核心原理,包括 SETNX 原子性陷阱、Redlock 多实例容错、Redisson 看门狗续期、可重入锁 hash key 设计以及常见死锁场景分析。共 10 道题目(6 选择 + 3 填空 + 1 简答)。 --- ## 一、选择题(6道,由浅入深) > **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景 ### Q1(基础)— 考察定义层面 在 Redis 中尝试获取分布式锁时,以下哪段代码存在**最严重**的缺陷? A. `redis.set("lock", uuid, "NX")` B. `redis.set("lock", uuid, "NX", "EX", 30)` C. `redis.setnx("lock", uuid); redis.expire("lock", 30)` D. Lua 脚本:`if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) end` ### Q2(基础)→ 使用 SET NX EX 获取锁后,客户端在持有锁期间宕机了。当锁过期时间到达时,Redis 会自动删除该 key。但如果在业务执行完成之前锁就过期了,会出现什么问题? A. Redis 会阻塞等待业务完成后再删除 key B. 其他客户端可能获取到同一把锁,导致数据竞争 C. 自动触发 Redlock 选举新的持有者 D. Lua 脚本检测到异常,自动延长锁有效期 ### Q3(进阶)— 核心原理 Redisson 的看门狗(Watch Dog)机制在什么条件下才会启动? A. 每次调用 `lock.lock()` 时都会启动 B. 只在传入 leaseTime 参数时启动 C. 只有在加锁时**不指定** leaseTime 参数时才启动 D. 看门狗需要手动通过 `startWatchDog()` 命令启用 ### Q4(进阶)— 比较/辨析 Redisson 实现可重入锁的数据结构是 Hash 类型,其 key 中存储的内容是什么? A. client_id 和整数计数 B. threadID 和重入次数 C. UUID 和时间戳 D. hostname 和端口号 ### Q5(深入)— 场景推理 在一个包含 5 个独立 Redis 实例的 Redlock 部署中,客户端依次向所有 5 个实例发送 SET lock uuid NX EX 30。其中实例 R1、R3、R5 返回 OK,R2 返回 nil,R4 返回 OK。总耗时 T = 12ms。请问以下判断正确的是: A. 加锁失败,因为只拿到了 3/5,未达到多数派 B. 加锁成功,有效 TTL 为 30s C. 加锁成功,有效 TTL 约为 19.988s D. 加锁成功,有效 TTL 为 20ms ### Q6(深入)— 源码级/边界场景 关于解锁操作,为什么必须用 Lua 脚本保证"检查 value + 删除"的原子性?下面哪个场景最能说明不加原子性的危险? A. 客户端 A 读取到 key 的 value 是自己的 clientId,但在执行 DEL 之前,锁已过期且客户端 B 重新获得了锁并设置了新的 value。此时 A 的 DEL 会把 B 的锁删掉。 B. Redis 主从切换时主节点未同步 DEL 命令导致从节点仍有锁 C. 客户端网络抖动导致 DEL 超时,锁永远不会被释放 D. SET NX 在高并发下存在多个客户端同时返回 OK 的情况 --- ## 二、填空题(3道) ### F1 — 填空1 Redlock 算法要求获得至少 _____ 个节点的 OK 响应才能判定加锁成功(假设部署了 5 个实例)。有效 TTL 的计算公式是:_____ − T(T 为总耗时)。 > **提示**: 回忆 Redlock 的核心规则中关于 N/2+1 的部分。 ### F2 — 填空2 Redisson 看门狗的后台任务每 _____ 秒检查一次锁是否仍持有,每次续期到默认 _____ 秒。 > **提示**: 原文中有一个表格列出了看门狗的行为细节。 ### F3 — 填空3 解锁 Lua 脚本的核心逻辑:只有当 `redis.call("get", key)` 返回的值等于传入的 _____ 时,才执行删除操作。这是为了防止误删其他客户端持有的锁。 > **提示**: 考虑竞态场景中谁有资格删除这把锁。 --- ## 三、简答题(1道) ### S1 某金融系统需要在用户扣款接口上做防重复提交。面试官问你:"为什么不依赖 Redis 分布式锁来保证幂等,而要在数据库层面再加一层唯一约束+CAS?"请结合 Redis 分布式锁的局限性,给出你的完整回答思路。 > **答题框架提示**: > 1. 从锁本身的可靠性局限谈起(clock drift、主从切换) > 2. 说明即使锁正确工作也无法替代的业务语义需求 > 3. 提出"双重保障"架构建议 --- ## 参考答案与解析 ### 选择题答案 | 题号 | 正确答案 | 解析 | |------|---------|------| | Q1 | A | A 选项只用了 SET NX 而没有设置过期时间(EX/PX),客户端宕机后锁永久不释放——死锁。B 是正确写法(原子 SET NX EX),C 虽然非原子但有 expire 兜底,D 是正确的解锁 Lua 脚本而非加锁。 | | Q2 | B | 锁自动释放后,其他客户端可以获取到同一把锁,而此时原客户端仍在执行业务逻辑,两个客户端同时对同一资源做写操作,必然产生数据竞争。 | | Q3 | C | 看门狗只在**不指定** leaseTime 时启动。如果明确传入了 leaseTime,Redisson 认为你已经知道业务执行时间上限,无需续期。 | | Q4 | B | Redisson 使用 Hash 存储 `{threadID}: count`,threadID 标识是哪个线程获得了锁,count 记录重入次数。unlock 时递减计数,归零才真正删除 key。 | | Q5 | C | 5 个实例中获得 4 个 OK,4 >= 5/2+1=3,满足多数派所以加锁成功。有效 TTL = 30s - 0.012s ≈ 29.988s。 | | Q6 | A | 这是经典的"误删他人锁"场景:没有 value 校验就直接 DEL,任何读到旧 value 的客户端都可以删除当前持有者的锁。Lua 脚本将 get + del 打包成原子操作是唯一解。 | ### 填空题答案 | 题号 | 答案 | 解析 | |------|------|------| | F1 | `3`;`30s`(或"设定值") | Redlock 要求 ≥ N/2+1 = 3 个实例 OK。有效 TTL = 设定过期时间 − 实际耗时,确保即使部分节点因网络延迟稍晚收到 DEL 也不会误判。 | | F2 | `10`;`30` | 每 10 秒检查一次,续期到默认 30 秒。这意味着只要业务不超过 30 秒且不连续错过 3 次续期,锁就不会意外释放。 | | F3 | `clientId`(或"客户端标识值") | 只有 value 匹配证明当前客户端确实是锁的持有者,才允许删除。否则可能是锁过期后被新客户端重新获取的场景。 | ### 简答题参考答案 S1:**参考答案要点**: 1. **时钟漂移风险**:NTP 同步异常可能导致系统时钟回拨,使得一个实例已释放锁而客户端仍持旧锁,Redlock 也不能完全规避此问题。 2. **主从切换丢锁**:Redis 单实例在主从切换时,主节点上的锁可能还未复制到从节点就宕机了,从节点成为新主后锁丢失。 3. **业务语义不足**:分布式锁只能保证互斥访问,不能保证"之前是否已经成功执行过"——如果第一次请求成功但网络超时导致客户端重试,锁无法区分这两次请求。 4. **数据库兜底的不可替代性**:唯一索引 + CAS 是最终防线,即使锁层面全部失效,数据库层面仍然能保证不会重复扣款。 5. **最佳实践**:应采用"锁 + 数据库二次校验"的双重保障策略,锁提升性能减少冲突,数据库保证最终一致性。 **评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。 ## 关联笔记 - [[ZooKeeper 分布式锁]] - [[限流熔断降级]]