Skip to content

R15. 当 Redis 内存达到 maxmemory 时,默认采用哪种行为? ​

考点:内存淘汰策略

A. allkeys-random——随机淘汰 B. allkeys-lru——在所有 key 中淘汰最近最少使用的 C. volatile-lru——只在设置了过期时间的 key 中淘汰 D. noeviction——拒绝写入并返回错误

答案:D

【考点】八种内存淘汰策略的分类与默认值。

【结论】 Redis 的 maxmemory-policy 默认就是 noeviction:内存写满后读命令正常、写命令直接报错 —— 选 D。

【逐项辨析】

  • A allkeys-random:错。存在,同样不是默认值,且随机淘汰命中率差、生产极少使用。
  • B allkeys-lru:错。策略真实存在且是缓存场景的推荐值,但它不是默认值。
  • C volatile-lru:错。同样存在,但也不是默认值;且淘汰范围限定在带 TTL 的 key 上,这类 key 太少会导致淘汰失败。
  • D noeviction —— 拒绝写入并返回错误:正确。 这就是默认值,写命令会收到 OOM command not allowed when used memory > 'maxmemory'。

注意:B、C、A 描述的策略本身都是真的,本题的陷阱只在“默认”二字。

【知识点】 八种策略由两个维度正交组合而成:淘汰范围(allkeys- 全部 key / volatile- 仅带过期时间的 key)× 淘汰算法(lru 最久没被访问、lfu 访问频率最低、random 随机、ttl 剩余寿命最短,其中 ttl 仅 volatile- 有)。故 allkeys- 3 种 + volatile- 4 种 + noeviction = 8 种:

策略范围算法典型用途
noeviction—(不淘汰)—默认值,写满即报错
allkeys-lru全部LRU缓存场景首选
allkeys-lfu全部LFU有明显冷热分层的缓存
allkeys-random全部随机极少用
volatile-lru带 TTLLRU缓存与常驻数据混布
volatile-lfu带 TTLLFU同上
volatile-random带 TTL随机极少用
volatile-ttl带 TTL剩余寿命最短优先清快过期的

推导:noeviction 之所以是默认值,是因为 Redis 无法替用户判断“哪些数据可以丢” —— 默认保守、拒绝写入并报错,让问题立刻暴露,而不是静默删掉用户以为还在的数据。代价是不做配置时 Redis 写满就写不进去。选型:纯缓存用 allkeys-lru;若希望没设 TTL 的数据绝不被动,用 volatile-lru 并给所有缓存都设上过期时间。

【记忆锚点】 “默认 noeviction 只读不写、直接报错;纯缓存改 allkeys-lru、要保数据用 volatile-lru” —— 默认值是“什么都不淘汰”。

【易混对比】

  • LRU vs LFU:LRU 看“多久没访问”,LFU 看“访问了多少次”。偶发的一次批量扫描会把 LRU 的热点挤掉,LFU 更抗干扰。
  • allkeys-* vs volatile-*:前者在全部 key 里淘汰,后者只在带 TTL 的 key 里淘汰;后者在几乎没有带 TTL 的 key 时效果等同 noeviction。
  • 换问法:问“缓存场景推荐哪个策略”,答案变成 allkeys-lru —— 注意区分“默认值”与“推荐值”,这是最常互换的考法。

【自测】 某实例设了 maxmemory 2gb 但没配 maxmemory-policy,且大量 key 都没有过期时间。内存涨到 2gb 后会发生什么?

答:写命令报 OOM command not allowed,读命令仍正常。默认 noeviction 不做淘汰;即使改成 volatile-lru,因 key 都没设 TTL 也淘汰不出空间。与 R14、R16 连考。

【知识关联】

  • 补题关联:无直接补题;容量规划与补-22(Buffer Pool 命中)思想类似:都是“有限内存下的保留策略”。
  • 面试/工程:缓存场景多用 allkeys-lru 或 allkeys-lfu(8.0 后 LFU 更抗扫描型访问);有明确过期语义可用 volatile-*。随机策略用于可重现实验。注意:淘汰与过期不同;设了 TTL 不等于会及时腾内存。noeviction 在只读缓存+严格不过期时可选,但写入会 OOM 错误。
  • 面试追问:① LRU 与 LFU 差在哪?(LRU 看最近使用时间,LFU 看访问频率;LFU 更不易被一次性全量扫描冲掉热点) ② 缓存击穿场景该选哪种淘汰?(与淘汰无直接关系,应靠互斥/逻辑过期;淘汰策略影响的是“谁先被挤出”)

【拓展延伸】

  • 变式问法:给出业务(纯缓存 vs 计数持久)选拨;把“设置了过期就一定不会被内存淘汰”设为错误项。
  • 参数/命令:maxmemory、maxmemory-policy noeviction|allkeys-lru|allkeys-lfu|volatile-lru|volatile-lfu|allkeys-random|volatile-random|volatile-ttl;INFO memory 的 used_memory/evicted_keys;LFU 调参 lfu-log-factor/lfu-decay-time。

R16. 关于 Redis 的内存与阻塞,下列做法不合理的是? ​

考点:内存碎片与内存优化

A. 用 MEMORY USAGE key 或 redis-cli --bigkeys 排查大 key B. 开启 activedefrag 或通过重启/主从切换缓解内存碎片 C. 用 INFO memory 观察 used_memory 与 mem_fragmentation_ratio D. 线上直接执行 KEYS * 扫描全部 key 来做容量统计

答案:D

【考点】Redis 内存排查工具与阻塞风险。

【结论】 KEYS * 会遍历整个 keyspace,在单线程模型下长时间阻塞 Redis —— 生产环境绝不能用它做容量统计,选 D。

【逐项辨析】

  • A 用 MEMORY USAGE key 或 redis-cli --bigkeys 排查大 key:合理。前者精确查单个 key 的内存占用,后者内部用 SCAN 安全统计,都不会阻塞。
  • B 开启 activedefrag 或通过重启 / 主从切换缓解内存碎片:合理。前者是主动碎片整理,后者通过重建内存让碎片自然消失。
  • C 用 INFO memory 观察 used_memory 与 mem_fragmentation_ratio:合理。这是判断内存是否紧张、碎片是否严重的第一手指标。
  • D 线上直接执行 KEYS * 扫描全部 key 来做容量统计:不合理(本题选它)。 关键字眼是“线上”与“全部 key” —— Redis 命令处理是单线程的,KEYS * 执行期间无法处理任何其他请求,key 越多阻塞越久。

【知识点】 为什么危险:Redis 的核心命令处理走单线程(6.0 起网络 IO 多线程,但命令执行仍是单线程)。因此任何复杂度为 O(N) 且 N 不可控的命令都会独占主线程,期间所有请求排队等待,客户端侧表现为超时、重连,极端情况下引发雪崩式故障。KEYS * 的复杂度正是 O(N),N 等于整个库的 key 数量。

目的危险做法安全替代复杂度
遍历全部 keyKEYS *SCAN cursor(游标增量遍历,非阻塞)O(N) → 分批 O(1)
统计大 keyKEYS * 后逐个 STRLENredis-cli --bigkeys(内部用 SCAN)安全
看单个 key 占用无MEMORY USAGE keyO(1)
看 key 总数KEYS * 再计数INFO keyspace / DBSIZEO(1)

内存碎片指标 mem_fragmentation_ratio = used_memory_rss / used_memory:大于 1.5 说明碎片较多、物理内存浪费明显,可开启 activedefrag 或重启 / 主从切换重建内存;约等于 1.0 正常;小于 1 说明部分内存被操作系统换出(swap),是危险信号。

推导:SCAN 之所以安全,是因为它采用游标(cursor)分批返回,每次只扫描一小部分并立即返回,把一次大阻塞拆成多次小阻塞,主线程在两次调用之间仍能处理其他命令。代价是它只保证“遍历期间一直存在的 key 一定被返回”,对遍历期间新增 / 删除的 key 不保证,且可能返回重复元素 —— 但用于容量统计完全够用。

【记忆锚点】 “KEYS 一次扫全库、单线程全阻塞;要遍历就用 SCAN、要统计就用 --bigkeys”。

【易混对比】

  • KEYS * vs SCAN:前者一次性返回全部(O(N)、阻塞),后者游标分批(非阻塞、可能重复、不保证实时一致)。
  • SCAN vs HSCAN / SSCAN / ZSCAN:同一套游标思想,分别遍历大 hash / set / zset 的内部元素 —— 同样要避免 HGETALL。
  • 换问法:问“统计 key 总数最安全的方式”,答案是 INFO keyspace 或 DBSIZE;注意“内存碎片(> 1.5,有内存用不上)”与“内存不足(used_memory 逼近 maxmemory)”排查方向完全不同。

【自测】 运维同学要统计线上实例里所有 user:* 开头的 key 数量,写了 redis-cli KEYS "user:*"。这个命令安全吗?正确写法是什么?

答:不安全。KEYS 是 O(N) 全库遍历、单线程阻塞,加模式匹配也要先扫全部 key。正确写法是用 SCAN 游标迭代 + 客户端 MATCH "user:*" 过滤,或用 INFO keyspace / DBSIZE 估总量。与 R15、R09 连考。

【知识关联】

  • 补题关联:补-28(编码与内存布局,大 key/大编码对象)、补-26(限流间接保护)。
  • 面试/工程:KEYS *、大 SORT、大 HGETALL、SAVE、同步大 RDB 传输都是阻塞或高开销命令,生产事故常来自“运维在高峰执行 KEYS”。正确姿势:SCAN 游标渐进遍历、大 Hash/Set 分片(hash tag 拆桶)、离线分析用 RDB 副本而非主库。SCAN 也不是绝对无阻塞:单次 COUNT 过大或实例 CPU 打满时仍有影响。
  • 面试追问:① SCAN 就一定完全无阻塞吗?(相对 KEYS 好得多,但仍占 CPU;COUNT 与速率要控制) ② 大 key 如何发现与拆分?(--bigkeys、MEMORY USAGE、RDB 离线分析;按业务维度拆 Hash 字段或改多 key)

【拓展延伸】

  • 变式问法:题干给一组命令选“最容易阻塞”(KEYS/FLUSHALL/大 SORT/SAVE);或问“遍历千万 key 应该用什么”→ SCAN。
  • 参数/命令:SCAN cursor MATCH p* COUNT 100;HSCAN/SSCAN/ZSCAN;redis-cli --bigkeys/--hotkeys;MEMORY USAGE key;LATENCY DOCTOR、SLOWLOG GET;CONFIG SET 调 hz 等需谨慎。

R17. 用 Redis 实现分布式锁,正确的加锁方式是? ​

考点:分布式锁的加锁

A. SET lock:key 1(不带 NX) B. 先 SETNX,成功后再 EXPIRE(分两条命令) C. SET lock:key <uuid> NX PX 30000(一条命令完成“不存在才设置”并带过期时间) D. INCR lock:key

答案:C

【考点】分布式锁的原子性加锁与唯一标识。

【结论】 正确写法是 SET lock:key <uuid> NX PX 30000 —— 一条命令同时完成“不存在才设置”与“设置过期时间”,并带上唯一标识,选 C。

【逐项辨析】

  • A SET lock:key 1(不带 NX):错。关键字眼是“不带 NX” —— 没有 NX 就是普通覆盖写,任何客户端都能改掉别人的锁值,互斥语义完全丧失。
  • B 先 SETNX,成功后再 EXPIRE(分两条命令):错。关键字眼是“分两条命令” —— 若客户端在 SETNX 成功之后、EXPIRE 执行之前崩溃,这把锁就永远没有过期时间,形成死锁。
  • C SET lock:key <uuid> NX PX 30000:正确。 三个要素齐备:NX 保证互斥、PX 防死锁、唯一 value 便于校验归属。
  • D INCR lock:key:错。关键字眼是“INCR” —— 它不携带归属标识、也不带过期时间,无法表达“谁持有”,不是互斥锁语义。

【知识点】 一把合格的 Redis 分布式锁,加锁必须同时满足三个条件:

条件手段不满足会怎样
互斥NX(Not eXists)多个客户端同时持锁,锁形同虚设
防死锁PX / EX 设过期时间持锁客户端崩溃 → 锁永不释放
可归属value 用唯一标识(UUID / 线程 ID)无法安全释放锁(见 R18)

为什么必须“一条命令”?Redis 单线程执行命令,单条命令天然原子,两条命令之间则可能插入其他客户端的命令:

时刻 T1  客户端 A: SETNX lock:key 1   → 成功(锁归 A)
时刻 T2  客户端 A 崩溃(EXPIRE 尚未执行)
时刻 T3  锁永远存在,无人能再获取     → 死锁

而 SET ... NX PX ... 把“判断 + 写入 + 设过期”压在一条命令里,不存在中间状态;PX(毫秒)与 EX(秒)二选一。过期时间太短则业务没跑完锁就自动释放,太长则崩溃后其他请求要等很久 —— 常见做法是先估算业务耗时取其数倍,再配合看门狗(watchdog)自动续期(如 Redisson 的续期机制)在业务未完成时延长锁的 TTL。

边界:单实例(或主从)下仍有局限 —— 主节点写入锁后尚未同步到从节点就宕机,从节点升主后锁丢失,可能出现两个客户端同时持锁。更强的方案是 Redlock(多数派加锁),但它在业界存在争议,不要盲目使用。

【记忆锚点】 “一条 SET 带 NX、带 PX、带 UUID” —— 互斥、防死锁、可归属,三要素缺一不可。

【易混对比】

  • NX vs XX:NX 是“不存在才设置”(加锁),XX 是“存在才设置”(常用于更新),方向相反。
  • 加锁 vs 释放:加锁的关键是“原子地抢占”,释放的关键是“原子地校验归属再删”(见 R18)—— 两题考点互为镜像,必须成对掌握。
  • 换问法:问“为什么不能先 SETNX 再 EXPIRE”,答案就是“两条命令之间存在崩溃窗口,会导致锁永不过期”。

【自测】 客户端 A 执行 SETNX lock:key A 成功后进程被 kill -9,锁没有任何过期时间。后果是什么?正确写法应如何避免?

答:锁永久无法释放(死锁)。正确写法是 SET lock:key <uuid> NX PX 30000,把设过期与加锁放在同一条原子命令里。与 R18 连考。

【知识关联】

  • 补题关联:补-21(悲观互斥的分布式实现就是 Redis 锁)、补-26(令牌桶取令牌要靠 Lua 保证「读-判断-扣」原子,与加锁是不同场景)。
  • 面试/工程:正确加锁必须一次原子完成“设值+NX+过期”:SET key uuid NX PX ttl。两步 SETNX+EXPIRE 在进程崩溃于两步之间时会留下永久死锁。value 用唯一标识,为的是解锁时校验持有者(见 R18)与问题排查。更强一致性考虑 etcd/ZK;Redis 锁要接受 failover 可能丢锁的边界。
  • 面试追问:① 为什么 SETNX+EXPIRE 两步不行?(非原子,中间崩溃可导致锁无 TTL) ② 锁的 value 为什么要唯一?(解锁比对持有者,防止误删他人锁;便于日志审计)

【拓展延伸】

  • 变式问法:正向选 SET lock:order:1 uuid NX PX 3000;错误项含“先 SETNX 再 EXPIRE”“只用 SETNX 不设过期”“value 用固定 1 无法校验持有者”。
  • 参数/命令:SET key value NX PX milliseconds;Lua 脚本原子加锁/解锁;Redisson RLock/tryLock(waitTime, leaseTime);SET lock uuid NX EX 3 等价秒级写法。

R18. 释放 Redis 分布式锁时,为避免误删他人持有的锁,正确做法是? ​

考点:分布式锁的释放

A. 直接 DEL lock:key B. 用 Lua 脚本执行“校验 value 是否等于自己的标识,相等才 DEL”,保证判断与删除的原子性 C. 用 EXPIRE 把锁设为立即过期 D. 先 GET 校验,再 DEL(两条命令)

答案:B

【考点】释放锁的原子性要求与 Lua 脚本的使用。

【结论】 必须用 Lua 脚本把“校验 value 是否等于自己的标识”和 DEL 合并成一个原子操作,否则存在竞态窗口、可能误删他人的锁 —— 选 B。

【逐项辨析】

  • A 直接 DEL lock:key:错。关键字眼是“直接” —— 完全不做归属校验,A 的锁到期自动释放后 B 拿到锁,A 若此时才执行 DEL,删掉的正是 B 的锁。
  • B 用 Lua 脚本执行“校验 value 是否等于自己的标识,相等才 DEL”,保证判断与删除的原子性:正确。 校验与删除必须不可分割,Lua 脚本在 Redis 中作为一个整体执行。
  • C 用 EXPIRE 把锁设为立即过期:错。关键字眼是“立即过期” —— 这不是标准释放做法,且同样不校验归属,并未解决“删错”的问题。
  • D 先 GET 校验,再 DEL(两条命令):错。关键字眼是“两条命令” —— 方向对(有校验),但 GET 与 DEL 之间存在竞态窗口,正是本题要排除的写法。

【知识点】 竞态窗口是本题的核心,把它画出来:

时刻 T1  客户端 A 的锁 TTL 到期,Redis 自动删除 lock:key
时刻 T2  客户端 B 执行 SET lock:key <B> NX PX 30000 → 加锁成功
时刻 T3  客户端 A 的 GET 发生在 T1 之前,读到 <A>,校验通过
时刻 T4  客户端 A 执行 DEL lock:key  →  删掉的是 B 的锁!

关键点:A 的“校验通过”与“执行删除”之间存在时间差,只要锁在这段时间内易主,A 就会删掉别人的锁。把两步合成一步即可消除该窗口 —— Redis 保证单条命令与 Lua 脚本整体执行的原子性。标准写法(Redis 官方文档):

lua
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

KEYS[1] 传锁的 key、ARGV[1] 传自己的唯一标识,用 == 比较、相等才删;返回 0 表示“锁已不属于自己”(可能已过期或被他人持有)。

写法是否校验归属是否原子结论
DEL lock:key✗✓可能删掉他人的锁
GET + DEL✓✗存在竞态窗口,仍可能误删
Lua 脚本✓✓正确

边界:如果业务在锁过期后才执行完,说明锁的 TTL 设短了 —— 此时无论释放写得多正确,互斥都已被破坏(A、B 曾同时持锁)。因此续期(看门狗)比“更精巧的释放脚本”更重要:Lua 脚本解决“误删”,解决不了“锁提前过期”(见 R17)。

【记忆锚点】 “加锁一条 SET、释放一段 Lua” —— 判断与删除必须原子,中间不能有缝。

【易混对比】

  • “误删他人锁” vs “锁提前过期”:前者是释放环节的原子性问题(本题,用 Lua 解决);后者是TTL 设置问题(用续期 / 加长 TTL 解决)。两者都表现为“互斥被破坏”,但根因不同。
  • Lua 脚本 vs 事务(MULTI/EXEC):MULTI 只是把命令排队,无法在事务内根据上一条命令的结果做判断(没有条件分支),所以“校验后再删”只能用 Lua 实现。
  • 换问法:问“GET + DEL 的问题是什么”,答案就是“两条命令之间存在竞态窗口,锁可能在窗口内易主”。与 R17 连考。

【自测】 客户端 A 持有 lock:key(value 为 A-uuid,TTL 30 秒),业务执行了 35 秒。A 执行释放脚本时会删掉谁的锁?根本问题出在哪?

答:不会删掉任何人的锁(脚本校验发现 value 已不是 A-uuid,返回 0)—— 但锁在 30 秒时已自动过期,期间 B 可成功加锁,互斥已被破坏。根本问题是 TTL 设短了,解法是加长 TTL 或用看门狗续期。与 R17 连考。

【知识关联】

  • 补题关联:补-21(悲观锁/乐观锁与锁标识)、R21(解锁脚本同样要用 Lua 保证原子;补-26 是限流脚本,别混)。
  • 面试/工程:误删他人锁是分布式锁最经典事故:线程 A 超时锁自动释放 → B 拿到锁 → A 恢复后直接 DEL 删掉 B 的锁,临界区被并发破坏。因此解锁必须“值匹配才删”,且比较与删除必须在同一原子单元(Lua,或 WATCH+MULTI)。Redisson 的 unlock() 即封装该逻辑;value 推荐 UUID:threadId 便于审计。
  • 面试追问:① 只用 DEL 行不行?(不行:无法判断锁是否仍归自己) ② 解锁脚本如何保证原子?(Lua 在 Redis 内整体执行,期间不插入其他客户端命令)

【拓展延伸】

  • 变式问法:正向题选“Lua:GET 比较 value 再 DEL”;错误项含“直接 DEL”“先 GET 再 DEL(两步非原子)”“用 UNLOCK 原生命令(不存在)”。
  • 参数/命令:典型解锁 Lua:if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end;EVAL/EVALSHA 执行;Redisson RLock.unlock();可选解锁前再校验持有线程标识。

R19. 关于 Redis 分布式锁的可靠性,下列说法正确的是? ​

考点:锁续期与 Redlock

A. Redlock 是绝对可靠的方案,不存在任何争议 B. 设置了过期时间的锁一定不会提前释放 C. Redlock 由 Antirez 提出,在多个独立 Redis 实例上多数派加锁;其正确性业界存在争议(如 Martin Kleppmann 的质疑),生产更常用 Redisson 的看门狗(watchdog)自动续期方案 D. Redlock 与 Redis 无关,是 ZooKeeper 的机制

答案:C

【考点】分布式锁的续期问题与 Redlock 争议。

【结论】 Redlock 由 Antirez 提出,在多个独立 Redis 实例上多数派加锁,其正确性在业界存在争议;生产更常用 Redisson 的看门狗(watchdog)自动续期方案 —— 选 C。

【逐项辨析】

  • A Redlock 是绝对可靠的方案,不存在任何争议:错。错在“绝对”二字 —— Redlock 恰恰是分布式锁领域最有名的一场论战的主角,它依赖各实例的时钟,无法严格保证互斥。
  • B 设置了过期时间的锁一定不会提前释放:错。错在“一定” —— 过期时间与业务执行时长无法预估匹配,业务跑得比 TTL 长,锁就会在业务未完成时提前释放。
  • C Redlock 由 Antirez 提出,在多个独立 Redis 实例上多数派加锁;其正确性业界存在争议(如 Martin Kleppmann 的质疑),生产更常用 Redisson 的看门狗自动续期方案:正确。 提出者、加锁方式、争议事实、生产替代方案四个要点全部成立。
  • D Redlock 与 Redis 无关,是 ZooKeeper 的机制:错。错在“与 Redis 无关” —— Redlock 是 Redis 作者提出的基于 Redis 的算法;ZooKeeper 用的是临时顺序节点 + Watch,是另一套机制。

【知识点】 分布式锁的可靠性,本质是“锁的持有时间必须覆盖业务的执行时间”,而这两者天然无法精确匹配。

TTL 设置后果
太短业务未执行完锁就过期 → 互斥失效,出现并发写
太长持锁者宕机后锁迟迟不释放 → 其他请求长时间阻塞

Redisson 看门狗 解决“太短”这一侧:加锁成功后启动后台定时任务,默认 lockWatchdogTimeout 为 30 秒,看门狗每 10 秒(TTL 的 1/3)把锁的过期时间重置回 30 秒,业务结束时再主动释放。因此只要持锁进程活着,锁就不会提前过期;进程崩溃后不再续期,锁最长约 30 秒后自动释放。

Redlock 的加锁流程(N 个相互独立的实例,通常 N = 5):

text
1. 记录起始时间 T1
2. 依次向 N 个实例用同一个 key、同一个随机 value 请求加锁,单实例超时远小于锁 TTL
3. 当且仅当在超过半数实例上加锁成功,且总耗时 < 锁 TTL,才判定加锁成功
4. 锁的真实有效时间 = 初始 TTL − 加锁总耗时
5. 加锁失败则向所有实例发送释放请求,避免残留

争议的推导链条:Redlock 的互斥性依赖“锁在 TTL 内一定不会过期”这一假设,而 TTL 由各实例的本地时钟计时 —— 一旦发生时钟漂移、GC 停顿或网络延迟,某实例可能提前认为锁已过期并让其他客户端加锁成功,于是两个客户端同时持锁。Kleppmann 因此认为它不适合资金类等正确性要求高的场景,建议改用带 fencing token 的方案或 ZooKeeper / etcd。

【记忆锚点】 「看门狗续命,Redlock 靠时钟」 —— 生产用 Redisson 看门狗解决“提前释放”,Redlock 解决“单点故障”但把正确性押在了时钟上。

【易混对比】

  • Redisson 看门狗 vs Redlock:前者自动续期,解决“锁提前释放”;后者跨多实例多数派加锁,解决“单点故障”,代价是依赖时钟。
  • Redis 分布式锁 vs ZooKeeper 分布式锁:Redis 性能高,但正确性依赖 TTL 与续期;ZooKeeper 靠临时顺序节点 + 会话超时自动释放,一致性更强但性能较低。
  • 换问法:若题干问“Redlock 的争议根源是什么”,答案就变成“它依赖各实例时钟,在时钟漂移 / GC 停顿下无法严格保证互斥”。

【自测】 Redisson 使用默认配置(TTL 30 秒、看门狗每 10 秒续期),业务实际执行了 5 分钟。锁会在第 30 秒被释放吗?

答:不会。看门狗每 10 秒把 TTL 重置回 30 秒,只要持锁进程存活,锁就一直被续期到业务主动释放;若进程中途崩溃,则锁最长约 30 秒后自动过期。与 R20、R22、R27 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-21(锁的正确性边界)、补-20(时钟/持久化与恢复对一致性的影响可对照)。
  • 面试/工程:看门狗解决“TTL 太短业务没跑完锁没了”;Redlock 试图用多独立实例降低单点与 failover 造成双主锁的概率,但依赖本地时钟假设,争议大。资金类场景更推荐 etcd/ZooKeeper + fencing token,或业务层幂等/唯一约束兜底。工程上要先问:业务能否容忍短暂双锁?若不能,别只靠 Redis 锁。
  • 面试追问:① 看门狗默认多久续期?(Redisson 默认 30s TTL,每 10s 续) ② Redlock 争议的核心是什么?(时钟漂移/GC/网络导致“锁已过期”的假设被打破,可能出现双持有)

【拓展延伸】

  • 变式问法:问“TTL 设多少合适”→ 不应固定拍脑袋,应用看门狗动态续期;问“主从切换时锁会不会丢”→ 异步复制下可能丢,这是单实例/普通主从锁的弱点。
  • 参数/命令:Redisson lockWatchdogTimeout(默认 30000ms);SET lock val NX PX ttl;Redlock 客户端 RLock 多节点;CLUSTER/哨兵下锁 key 的落点;对比 etcd lease/TTL 与 ZooKeeper ephemeral node。

R20. Redis 事务(MULTI / EXEC)的特点是? ​

考点:Redis 事务

A. 支持完整的回滚(ACID 中的 A、I、D 都满足) B. 命令先入队、EXEC 时顺序执行且不被打断,但不支持回滚(运行期某条命令失败,其余命令仍会执行),也没有隔离级别的概念 C. 与关系型数据库事务完全等价 D. 是分布式事务的标准实现

答案:B

【考点】Redis 事务的能力边界(“原子性”的准确含义)。

【结论】 Redis 事务把命令先入队、EXEC 时顺序执行且不被打断,但不支持回滚(运行期某条命令失败,其余命令仍会执行),也没有隔离级别的概念 —— 选 B。

【逐项辨析】

  • A 支持完整的回滚(ACID 中的 A、I、D 都满足):错。错在“完整的回滚” —— Redis 事务根本没有回滚能力,运行期出错后已执行的命令不会撤销。
  • B 命令先入队、EXEC 时顺序执行且不被打断,但不支持回滚(运行期某条命令失败,其余命令仍会执行),也没有隔离级别的概念:正确。 这三句恰好划出了 Redis 事务能力的准确边界。
  • C 与关系型数据库事务完全等价:错。错在“完全等价” —— 关系型数据库有 undo / redo 与隔离级别,Redis 事务两者都没有。
  • D 是分布式事务的标准实现:错。错在“分布式” —— Redis 事务只在单个实例内生效、不跨实例,分布式事务需要 Seata、TCC 等方案。

【知识点】 Redis 事务的流程是:MULTI 开启事务 → 后续命令入队并返回 QUEUED → EXEC 按入队顺序执行,或用 DISCARD 放弃整个事务;配合 WATCH 可实现乐观锁。

关键是把两种错误分开看:

错误发生阶段典型例子EXEC 的行为是否回滚
入队阶段(语法错误 / 命令不存在)SET 少参数、写了不存在的命令整体拒绝执行,返回 EXECABORT全部不执行,无需回滚
执行阶段(运行期错误)对 String 执行 LPUSH其余命令照常执行不回滚,数据可能处于中间状态

再看 ACID 四性的实际成色:

属性Redis 事务的表现说明
原子性 A部分具备只保证“不被穿插执行”,不保证“全部成功或全部失败”
一致性 C弱出错后可能落在中间状态,靠开发者自行保证
隔离性 I靠单线程天然获得执行期间不处理其他客户端命令,等价于串行
持久性 D与事务无关由 RDB / AOF 决定

为什么 Redis 不回滚:回滚需要额外的 undo 机制,而 Redis 认为运行期错误属于编程错误,应由开发者修正而非由引擎兜底 —— 这与它“保持简单、追求高性能”的设计取向一致。

WATCH 乐观锁:WATCH key 之后,若该 key 在 EXEC 之前被其他客户端修改,EXEC 返回 nil 且整个事务不执行,客户端需重试。

【记忆锚点】 「Redis 事务只打包,不兜底」 —— 能保证“一口气执行完、中间不被插队”,不能保证“出错全撤销”。

【易混对比】

  • Redis 事务 vs Lua 脚本:两者都“执行期不被打断”;区别是 Lua 脚本内可读中间结果、写 if 判断,事务做不到。
  • Redis 事务 vs Pipeline:Pipeline 只是批量传输,服务端仍会穿插执行其他客户端的命令;事务保证执行期不被穿插。
  • 入队错误 vs 运行期错误:前者让整个事务不执行,后者只让出错那条失败、其余照常执行且不回滚。
  • 换问法:若题干问“对 String 执行 LPUSH 后事务会怎样”,答案仍是“该条失败、其余命令照常执行、不回滚”。

【自测】 客户端依次执行 MULTI、SET k1 v1、LPUSH k1 v2、EXEC,其中 k1 此前是 String 类型的 key。最终结果如何?

答:SET 成功执行,LPUSH 因类型不匹配报错,事务不回滚,k1 的最终值仍是 v1。与 R21、R25 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-17(ACID:Redis 事务无回滚、持久性也不完整)、补-21(需要条件判断时 Lua/乐观更合适)。
  • 面试/工程:MULTI/EXEC 只是“命令排队后连续执行”,不提供回滚与隔离级别;语法错误整队拒绝,运行期错误默认继续执行后面命令。需要“判断再执行”“失败回滚”“复杂逻辑”时用 Lua。Pipeline 是性能优化(省 RTT),与事务正交,可组合但概念不要混。
  • 面试追问:① EXEC 中某条命令运行期错误会怎样?(默认不回滚,已执行命令效果保留;Redis 不支持类似 SQL 的自动回滚) ② WATCH 属于什么锁思想?(乐观锁/CAS:执行前检查 key 是否被改,被改则 EXEC 失败)

【拓展延伸】

  • 变式问法:三方对比题:MULTI/EXEC vs Pipeline vs Lua(原子性 / 隔离 / 回滚 / 适用场景);或问“Redis 事务是否满足 ACID”→ 不满足完整 A/C/I/D 口径。
  • 参数/命令:MULTI/EXEC/DISCARD/WATCH key/UNWATCH;EXEC 返回数组逐条结果;需要原子+条件 → EVAL Lua;客户端 Spring SessionCallback 可把 MULTI 包进 pipeline。

R21. Redis 执行 Lua 脚本时,关于其原子性,下列说法正确的是? ​

考点:Lua 脚本的原子性

A. 整个脚本作为一个整体执行,期间不会插入其他客户端的命令(具备原子性),但脚本执行过久会阻塞整个 Redis B. 不保证原子性,脚本中的命令可能与其他客户端的命令交错 C. 脚本中若某条命令失败,整个脚本会回滚 D. Lua 脚本由多线程并行执行

答案:A

【考点】Lua 脚本的原子性与阻塞风险。

【结论】 整个 Lua 脚本被当作一个命令执行,执行期间不会插入其他客户端的命令,因此具备原子性;但脚本执行过久会阻塞整个 Redis —— 选 A。

【逐项辨析】

  • A 整个脚本作为一个整体执行,期间不会插入其他客户端的命令(具备原子性),但脚本执行过久会阻塞整个 Redis:正确。 原子性来自“当作一个命令执行”,代价则是主线程被长时间独占。
  • B 不保证原子性,脚本中的命令可能与其他客户端的命令交错:错。错在“交错” —— 脚本执行期间 Redis 不处理其他客户端的命令。
  • C 脚本中若某条命令失败,整个脚本会回滚:错。错在“回滚” —— Lua 脚本没有回滚机制,已执行的命令不会撤销(与 R20 同理)。
  • D Lua 脚本由多线程并行执行:错。错在“多线程并行” —— Redis 处理命令的主线程是单线程的,脚本串行执行。

【知识点】 理解本题的关键是把“原子性”拆成两层含义:

含义Lua 脚本是否具备说明
不被穿插(隔离性)具备脚本整体作为一条命令提交给主线程,主线程串行处理
全部成功或全部失败(回滚)不具备中途报错,已执行的命令不撤销

阻塞风险的推导:Redis 主线程单线程 → 脚本执行时间 = 主线程被独占的时间 → 脚本里若有遍历大集合、循环调用 redis.call 等重操作,所有其他客户端的请求全部排队等待。因此脚本必须短小。

两个调用函数的区别:redis.call 出错时中断脚本并把错误抛回客户端;redis.pcall 把错误作为返回值交给脚本、脚本可继续执行。但即使 redis.call 中断脚本,此前已执行成功的命令依然不回滚。

典型用途:分布式锁的安全释放(GET 比对 value 再 DEL,把“判断 + 删除”合成一个原子步骤)、限流(INCR + EXPIRE 合并)、库存扣减。演进:Redis 7.0 起提供 Function(FUNCTION LOAD),可持久化、可复用,是脚本管理的演进方案。

【记忆锚点】 「脚本是一整条命令:原子、不回滚、还怕长」 —— 只写短小脚本,重逻辑不要塞进 Lua。

【易混对比】

  • Lua 脚本 vs MULTI / EXEC:两者都保证“执行期不被穿插”;区别是脚本内可读中间结果并做 if 判断,事务不能。
  • Lua 脚本 vs Pipeline:Pipeline 不保证原子性,服务端仍会穿插执行其他客户端的命令(见 R25)。
  • 原子性 vs 不回滚:Redis 语境下“原子性”通常只指“不被穿插”,不要理解成“可回滚”。
  • 换问法:若题干问“Lua 脚本执行时间过长的后果”,答案是“阻塞整个 Redis,其他客户端请求全部排队等待”。

【自测】 用 Lua 脚本实现分布式锁的安全释放:脚本先 redis.call('GET', key),若取到的值与客户端持有的随机值不相等,应该怎么做?

答:直接 return 0 返回,不执行 DEL —— 判断与删除在同一脚本内完成,中间不会被其他客户端插入,从而避免“删掉了别人持有的锁”。与 R19、R20 连考。

【知识关联】

  • 补题关联:补-26(令牌桶用 Lua 保证取令牌原子)、补-21(比 WATCH/MULTI 更适合条件复杂逻辑)。
  • 面试/工程:库存扣减、限流、防重复提交、解锁校验都是 Lua 典型场景。脚本要短:长脚本阻塞整个实例。先用 SCRIPT LOAD+EVALSHA 减少网络传输;注意集群下所有 key 要同槽,否则报错。Redis 7+ 有 Function,便于集中管理脚本。
  • 面试追问:① Lua 里能调用任意 Redis 命令吗?(有调用规范与副作用限制,随机性命令慎用;集群要求 key 同槽) ② 脚本执行中能被其他命令打断吗?(默认原子执行;超时后可能被 SCRIPT KILL,若已写数据则只能 SHUTDOWN NOSAVE 级处理——故脚本要短)

【拓展延伸】

  • 变式问法:问“如何原子实现‘不存在则写入并设置过期’”→ Lua 或 SET NX EX;问“Pipeline 和 Lua 谁更原子”→ Lua。
  • 参数/命令:EVAL script numkeys key... arg...、SCRIPT LOAD、EVALSHA、SCRIPT EXISTS/FLUSH/KILL;redis.replicate_commands()(旧版);集群 hash tag 保证同槽;lua-time-limit。

R22. Redis 主从复制的默认同步方式是? ​

考点:主从复制的同步方式

A. 同步复制,主库等待从库确认后才返回成功 B. 异步复制,主库写完后立即返回,从库异步追赶,存在数据丢失窗口 C. 半同步复制(等待至少一个从库确认) D. 主从之间没有数据复制

答案:B

【考点】异步复制的含义及其带来的数据丢失风险。

【结论】 Redis 主从复制默认是异步复制 —— 主库写完立即向客户端返回成功,从库异步追赶,因此存在数据丢失窗口 —— 选 B。

【逐项辨析】

  • A 同步复制,主库等待从库确认后才返回成功:错。错在“等待从库确认” —— Redis 默认不等从库确认就返回。
  • B 异步复制,主库写完后立即返回,从库异步追赶,存在数据丢失窗口:正确。 “立即返回”带来低延迟,“不等确认”带来丢失窗口,一体两面。
  • C 半同步复制(等待至少一个从库确认):错。错在“半同步” —— 这是 MySQL 的概念(如 rpl_semi_sync_master),Redis 原生没有这种复制模式。
  • D 主从之间没有数据复制:错。错在“没有复制” —— 主从复制正是 Redis 高可用的基础。

【知识点】 主从复制分三个阶段:

阶段做什么关键点
建立连接从库发 PSYNC,主库返回 FULLRESYNC协商 run id 与复制偏移量
全量同步主库 BGSAVE 生成 RDB 发给从库期间的新写命令暂存到复制缓冲区
增量同步主库把写命令持续传播给从库断线重连后用 PSYNC + offset 续传

三种同步模式的对照:

模式主库返回时机数据安全性Redis 是否默认
异步复制本地执行完立即返回有丢失窗口是
同步复制等从库确认后返回不丢否
半同步复制等至少一个从库确认较安全否(MySQL 概念)

丢失窗口的推导:主库执行写命令 → 立即向客户端返回成功 → 写命令进入复制缓冲区 → 经网络传播到从库。若在传播完成前主库宕机,且从库尚未收到该命令,则主从切换后这部分数据永久丢失。这既解释了“Redis 主从切换可能丢数据”,也是 R19 中 Redlock 争议的同一根源。

WAIT 的正确理解:WAIT numreplicas timeout 让当前客户端阻塞等待,直到指定数量的从库确认收到之前的写命令。它不改变复制模式本身,只是一次性同步等待,超时后即使未达数量也返回已确认的副本数 —— 所以不能说 Redis “支持同步复制”。

生产缓解:配置 min-replicas-to-write N 与 min-replicas-max-lag S,要求至少有 N 个从库的延迟不超过 S 秒,否则主库拒绝写入 —— 用可用性换取更小的丢失窗口。

【记忆锚点】 「异步复制:快在前,丢在后」 —— 不等从库所以快,也因为不等所以可能丢。

【易混对比】

  • 主从复制 vs Sentinel vs Cluster:复制解决“数据有备份”,Sentinel 解决“故障自动切换”,Cluster 解决“容量与写入扩展”(见 R23、R24)。
  • 复制模式 vs WAIT:WAIT 只是“这一次等一等”,不是把模式改成同步复制;Redis 原生也没有半同步复制。
  • 换问法:若题干问“Redis 主从切换为什么会丢数据”,答案仍是“异步复制下主库返回成功时,从库可能还没收到写命令”。

【自测】 主库执行 SET k v 后立即宕机,此时从库尚未收到该命令。哨兵把从库提升为新主库后,k 还存在吗?

答:不存在(数据已丢失)。这正是异步复制的丢失窗口;若业务不允许丢数据,需配合 min-replicas-to-write 限制写入,或改用强一致的存储。与 R19、R23 连考。

【知识关联】

  • 补题关联:补-29(集群视角下的节点状态变化)、补-20(日志位点与恢复)。
  • 面试/工程:全量同步成本高(RDB 生成+传输+从库清空加载),应尽量走部分重同步:保证 repl-backlog-size 足够覆盖主库峰值写入 × 断线时长。无盘复制降低主库磁盘峰值但占网络。复制仍可能丢:异步复制下主库 ACK 前宕机。要 RPO≈0 需半同步/组提交+确认策略。
  • 面试追问:① 复制积压缓冲区作用?(环形缓冲保存最近写命令流,断线后从 offset 续传) ② 何时全量何时部分?(runid 匹配且 offset 在 backlog 内 → 部分;否则全量 PSYNC/FULLRESYNC)

【拓展延伸】

  • 变式问法:问“从库重启后走全量还是部分同步”——取决于 backlog 与 runid/offset 是否仍匹配;问“为什么从库只读还可能延迟大”→ 网络、大 RDB、单线程重放、硬件。
  • 参数/命令:replicaof/REPLICAOF、masterauth、repl-backlog-size、repl-diskless-sync、repl-diskless-sync-delay、min-replicas-to-write;INFO replication 看 master_repl_offset/slave_repl_offset。

R23. Redis Sentinel(哨兵)的核心作用是? ​

考点:Sentinel 哨兵

A. 对数据进行分片存储 B. 实现请求限流 C. 负责持久化 D. 监控主从节点、自动进行故障转移(选主)与通知客户端,解决 Redis 的高可用问题

答案:D

【考点】哨兵的三项职责与它“不做什么”。

【结论】 哨兵负责监控主从节点、自动进行故障转移(选主)与通知客户端,解决 Redis 的高可用问题 —— 选 D。

【逐项辨析】

  • A 对数据进行分片存储:错。错在“分片” —— 分片是 Redis Cluster 的职责(见 R24),哨兵不做数据分片。
  • B 实现请求限流:错。错在“限流” —— 限流属于网关或业务侧职责,哨兵不参与。
  • C 负责持久化:错。错在“持久化” —— 持久化由 RDB / AOF 完成,与哨兵无关。
  • D 监控主从节点、自动进行故障转移(选主)与通知客户端,解决 Redis 的高可用问题:正确。 监控、通知、故障转移三项职责齐备,定位也准确。

【知识点】 哨兵是独立进程,职责固定为三项:

职责具体动作
监控周期性向主库、从库及其他哨兵发送 PING(心跳探测)
通知哨兵之间用 __sentinel__:hello 频道互相发现;故障判定与切换结果走 +switch-master 等频道发布,客户端订阅后更新拓扑(两类频道别混)
自动故障转移选主 → 让其他从库复制新主库 → 通知客户端新地址

主观下线与客观下线(最容易考错的一对概念):

概念判定条件作用
主观下线 SDOWN单个哨兵在 down-after-milliseconds 内收不到 PING 回复初步判断,可能误判
客观下线 ODOWN认为主观下线的哨兵数达到 quorum(法定票数)确认主库真的下线,避免单点误判

选主规则(优先级从高到低):replica-priority 数值最小 → 复制偏移量最大(数据最新)→ run id 最小。随后由哨兵 Leader(Raft 式投票选出,需半数以上哨兵同意)执行切换。

部署建议:哨兵通常部署 3 个及以上奇数个,分散在不同机器或机架,避免脑裂与单点故障。

【记忆锚点】 「哨兵只做监控、通知、切换三件事,不管分片、不管持久化」 —— 定位高可用,不解决容量。

【易混对比】

  • Sentinel vs Cluster:Sentinel 只解决主从的高可用切换,数据仍在一台主库上,容量与写入无法扩展;Cluster 既做分片又自带故障转移。
  • 主观下线 vs 客观下线:前者是单个哨兵的判断,后者是达到 quorum 后的集体结论,只有客观下线才触发切换。
  • 换问法:若题干问“哨兵部署几个合适”,答案是“3 个及以上奇数个,且分散部署”。

【自测】 3 个哨兵监控 1 主 2 从,quorum 配置为 2。某一时刻只有 1 个哨兵收不到主库的 PING 回复,会触发故障转移吗?

答:不会。只达到主观下线,认为主库下线的哨兵数(1)未达到 quorum(2),不构成客观下线。与 R22、R24 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-29(集群拓扑变化与重定向;Sentinel 是高可用方案,Cluster 是分片+高可用方案)、补-20(故障恢复思想)。
  • 面试/工程:Sentinel 适合“单主多从 + 自动 failover + 客户端发现主节点”;Cluster 适合“数据分片”。客户端必须支持 Sentinel 协议(订阅 +switch-master 或查询当前主)。脑裂:旧主网络隔离仍可写,恢复后数据被新主覆盖——可用 min-replicas-to-write 限制写。哨兵部署要奇数个、跨机房/机架。
  • 面试追问:① quorum 与 majority 区别?(quorum=判定客观下线所需的同意数;majority=选举领导者/授权 failover 的多数派) ② 脑裂如何缓解?(多数派哨兵、min-replicas-to-write/min-replicas-max-lag、客户端 fencing)

【拓展延伸】

  • 变式问法:问“主观下线/客观下线/领导者选举”三阶段;或问“哨兵挂了会怎样”→ 仍需多数派存活才能 failover。
  • 参数/命令:sentinel monitor mymaster ip port quorum;sentinel down-after-milliseconds;sentinel failover-timeout;sentinel parallel-syncs;min-replicas-to-write/min-replicas-max-lag;SENTINEL get-master-addr-by-name。

R24. Redis Cluster 的数据分片机制是? ​

考点:Cluster 分片机制

A. 一致性哈希 B. 随机分配到任意节点 C. 按 key 长度取模分片 D. 哈希槽(共 16384 个 slot)——key 经 CRC16 取模映射到槽,槽再分配给各节点

答案:D

【考点】哈希槽机制及其相对一致性哈希的优势。

【结论】 Redis Cluster 把键空间划分为 16384 个哈希槽,key 经 CRC16 取模映射到槽,槽再分配给各节点 —— 选 D。

【逐项辨析】

  • A 一致性哈希:错。错在“一致性哈希” —— 那主要是客户端分片(如 Memcached 客户端)的做法,Redis Cluster 用的是哈希槽。
  • B 随机分配到任意节点:错。错在“随机” —— 同一个 key 每次必须落到同一个槽,否则读写会找不到数据。
  • C 按 key 长度取模分片:错。错在“按 key 长度” —— 分片依据是 key 的哈希值,按长度取模会让数据分布严重倾斜。
  • D 哈希槽(共 16384 个 slot)—— key 经 CRC16 取模映射到槽,槽再分配给各节点:正确。 公式是 CRC16(key) % 16384,槽是逻辑层、节点是物理层,二者解耦。

【知识点】 哈希槽的本质是两级映射:

text
key  --CRC16(key) % 16384-->  slot(0 ~ 16383)  --槽分配表-->  node

为什么用哈希槽而不是一致性哈希:一致性哈希需维护哈希环与虚拟节点,扩缩容时要重算大量 key 的归属;哈希槽把“key → 槽”(稳定不变)与“槽 → 节点”(可调整)解耦,扩缩容时只需迁移槽,无需重算每个 key。3 个主节点时大致各负责 5461 个槽(16384 ÷ 3 ≈ 5461)。

常用命令:CLUSTER KEYSLOT key 查 key 属于哪个槽;CLUSTER NODES 查看节点与槽的分配;CLUSTER SETSLOT <slot> NODE <id> 把槽指派给指定节点。

两种重定向(必须分清):

重定向含义客户端动作
MOVED该槽已永久归属别的节点更新本地槽映射表后重试
ASK该槽正在迁移,该 key 已迁到目标节点只对本次请求先发 ASKING 再转向,不更新映射表

跨槽限制:不支持跨槽的多 key 操作(跨槽 MGET、SUNION 会报 CROSSSLOT 错误)。需要多 key 同槽时使用 hash tag:{user:1}:name 与 {user:1}:age 只对 {} 内的 user:1 计算 CRC16,因此落到同一个槽。

【记忆锚点】 「先算槽,再看谁管这个槽」 —— CRC16(key) % 16384 决定槽(不变),槽到节点的映射才是可变的。

【易混对比】

  • 哈希槽 vs 一致性哈希:前者是“固定 16384 个逻辑桶 + 槽迁移”,后者是“哈希环 + 虚拟节点 + 环上重算”。
  • MOVED vs ASK:MOVED 是永久重定向(要更新映射表),ASK 是临时重定向(不更新映射表)。
  • Cluster vs Sentinel:Cluster 解决分片与容量扩展(自带故障转移),Sentinel 只解决主从的高可用切换、不分片。
  • 换问法:若题干问“{user:1}:name 与 {user:1}:age 会不会同槽”,答案是“会”。

【自测】 3 主 3 从的 Redis Cluster 中,CRC16("user:1") % 16384 的结果是槽 8000,而槽 8000 当前由节点 B 负责。客户端误向节点 A 发送 GET user:1,会得到什么响应?

答:返回 MOVED 8000 <节点 B 的地址>,客户端据此更新本地槽映射表,再转向节点 B 重试。与 R23 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-29(MOVED/ASK 重定向语义必须与槽迁移一起记)。
  • 面试/工程:16384 的取舍:槽太少迁移粒度粗、心跳包与槽位图更大;官方认为 16384 在常见规模下够用且心跳图小。客户端应缓存 slot→node 并处理 MOVED/ASK;跨槽多 key 要用 hash tag 保证同槽,否则 CROSSSLOT。扩缩容是“迁槽 + 迁数据”,不是改 key 哈希。
  • 面试追问:① 为何不用更多槽?(槽位图/心跳与元数据变大,收益有限) ② 迁移中客户端读到什么?(可能 ASK 重定向;目标节点上若 key 尚未迁完,源节点仍可服务,直到槽标记迁移完成)

【拓展延伸】

  • 变式问法:对比一致性哈希(环+虚拟节点,扩缩容重算归属)与哈希槽(key→槽稳定,槽→节点可变);问 hash tag 如何解决 CROSSSLOT。
  • 参数/命令:CLUSTER KEYSLOT key、CLUSTER NODES、CLUSTER ADDSLOTS、CLUSTER GETKEYSINSLOT、CLUSTER SETSLOT <slot> MIGRATING/IMPORTING/NODE;cluster-require-full-coverage;客户端 MOVED 后刷新映射表。

R25. Redis Pipeline(管道)的作用是? ​

考点:Pipeline 管道

A. 保证多条命令的原子性 B. 将多条命令一次性发送、减少网络往返(RTT),提升批量操作吞吐;但不保证原子性 C. 实现事务回滚 D. 用于持久化

答案:B

【考点】Pipeline 的优化原理与与事务的区别。

【结论】 Pipeline 把多条命令一次性发送、减少网络往返(RTT)以提升批量吞吐,但不保证原子性 —— 选 B。

【逐项辨析】

  • A 保证多条命令的原子性:错。错在“原子性” —— Pipeline 只是打包传输,服务端仍会穿插执行其他客户端的命令。
  • B 将多条命令一次性发送、减少网络往返(RTT),提升批量操作吞吐;但不保证原子性:正确。 收益在于“少跑几趟网络”,而不在于“执行不被打断”。
  • C 实现事务回滚:错。错在“回滚” —— Pipeline 与事务、回滚都无关(Redis 事务本身也不回滚,见 R20)。
  • D 用于持久化:错。错在“持久化” —— 持久化由 RDB / AOF 负责,与 Pipeline 无关。

【知识点】 一条命令的端到端耗时可拆成三部分:

组成含义Pipeline 是否优化
网络往返 RTT客户端与服务端之间一次往返的耗时是,把 N 次压缩为 1 次
服务端执行时间命令本身的处理耗时否,完全不变
客户端处理时间解析结果、调用开销部分优化

推导:单条命令耗时 ≈ RTT + 执行时间。当命令本身很短(如 SET、INCR)时 RTT 占主导,逐条发送 N 条命令的总耗时 ≈ N × RTT;用 Pipeline 后 ≈ 1 × RTT + N × 执行时间,吞吐量可提升数倍到数十倍。反之,若单条命令本身就很慢,提升非常有限 —— 它省的是路费,不是执行时间。

三种批量方案的对照:

方案减少 RTT执行期不被打断脚本内可做逻辑判断
Pipeline是否否
MULTI / EXEC是是否
Lua 脚本是是是

使用建议:单次 Pipeline 的命令数量要适度(如 500 至 1000 条),过多会占用大量内存、拉长阻塞时间;服务端是读一批执行一批,期间仍会穿插处理其他客户端的请求,因此 Pipeline 内的命令并非“整体不可分割”。

【记忆锚点】 「Pipeline 省的是路费,不是加锁」 —— 减少网络往返,但不提供原子性保证。

【易混对比】

  • Pipeline vs 事务:Pipeline 不保证不被穿插;MULTI / EXEC 保证执行期不被打断,但两者都不回滚。
  • Pipeline vs Lua 脚本:只是“批量写”用 Pipeline 即可;需要“读中间结果再做判断”必须用 Lua。
  • Pipeline vs 批量命令(MSET、MGET):批量命令减少的是命令条数,Pipeline 减少的是网络往返次数,两者可叠加使用。
  • 换问法:若题干问“Pipeline 能提升吞吐的根本原因”,答案是“把 N 次 RTT 压缩为 1 次”。

【自测】 客户端需要连续执行 1000 次 SET key_i value_i。使用 Pipeline 与不使用,服务端执行这些命令的总时间会变化吗?

答:基本不变。服务端执行每条 SET 的时间相同,变的是网络往返次数:从约 1000 次 RTT 降为约 1 次,因此端到端吞吐大幅提升。与 R20、R21 连考;大厂面试高频。

【知识关联】

  • 补题关联:无直接补题;与补-21(事务边界)、补-26(令牌桶限流的 Lua 脚本;Pipeline 只批量不原子,原子性见 R21 的 EVAL)对比记忆:Pipeline 是性能手段,不是正确性手段。
  • 面试/工程:批量写入/导出用 Pipeline 把 N 次 RTT 压成 1 次,吞吐提升一个数量级常见;但单次 pipeline 过大(万级命令/数百 MB)会造成延迟尖刺与客户端缓冲膨胀,应分批(几百到几千条)。需要原子则 Pipeline 包 MULTI,或直接 Lua。
  • 面试追问:① Pipeline 能否与 MULTI 一起用?(可以,客户端把整段 MULTI…EXEC 打包进 pipeline) ② 为什么 Pipeline 不保证原子?(只是打包发命令,中间仍可被其他客户端命令插入执行)

【拓展延伸】

  • 变式问法:对比“减少 RTT”与“保证原子”;把“Pipeline=事务”设为错误项。
  • 参数/命令:Jedis Pipeline、Lettuce Flux/dispatch、Spring Data Redis executePipelined;建议分批大小数百条并压测;监控 INFO stats 的 instantaneous_ops_per_sec 与慢日志。

R26. 在 Redis 单线程执行命令的模型下,下列哪组操作最容易导致阻塞? ​

考点:阻塞风险命令

A. GET / SET 单个 key B. KEYS *、FLUSHALL、删除大 key(DEL)、执行耗时 Lua 脚本 C. MGET 少量 key D. INCR 计数

答案:B

【考点】生产环境的“阻塞命令黑名单”。

【结论】 KEYS *、FLUSHALL、删除大 key(DEL)、耗时 Lua 脚本的共同点是时间复杂度随数据规模增长、且在单线程中同步执行,最容易阻塞 Redis —— 选 B。

【逐项辨析】

  • A GET / SET 单个 key:错。错在“单个 key” —— 二者都是 O(1) 操作,执行时间极短,几乎不构成阻塞。
  • B KEYS *、FLUSHALL、删除大 key(DEL)、执行耗时 Lua 脚本:正确。 四者耗时都与数据规模正相关,且都要在主线程里同步跑完。
  • C MGET 少量 key:错。错在“少量” —— MGET 是 O(N) 但 N 很小,开销可忽略。
  • D INCR 计数:错。错在“计数” —— INCR 是 O(1) 的原子自增,几乎不耗时。

【知识点】 判断阻塞风险的公式是:时间复杂度随数据规模增长 × 在单线程中同步执行,两个条件同时成立才危险。

危险命令复杂度替代方案
KEYS *O(N),N 为全库 key 数SCAN(游标分批,单次开销小)
FLUSHALL / FLUSHDBO(N)低峰期执行;4.0 起可用 FLUSHALL ASYNC
DEL 大 keyO(N),N 为该 key 的元素数UNLINK(后台线程异步释放内存)
耗时 Lua 脚本取决于脚本内容拆分脚本、限制脚本长度
HGETALL / SMEMBERS 大集合O(N)HSCAN / SSCAN 分批遍历
SORTO(N log N)移到业务侧排序;必要时加 LIMIT
SAVEO(N),阻塞式快照BGSAVE(fork 子进程)

SCAN 的代价:用游标分批遍历,每次只返回少量元素、不阻塞主线程;但同一 key 可能被返回多次(客户端需去重),且不保证看到一致的快照。生产上禁止使用 KEYS,一律用 SCAN。

排查与兜底:SLOWLOG GET 查慢命令日志(阈值由 slowlog-log-slower-than 设定);LATENCY DOCTOR / LATENCY HISTORY 查延迟事件;redis-cli --bigkeys 定位大 key;INFO commandstats 查看各命令调用统计。此外可在 redis.conf 中用 rename-command 把危险命令重命名或禁用。

【记忆锚点】 「单线程怕的不是命令多,而是一条命令太慢」 —— 凡是 O(N) 且 N 不可控的命令,都要进黑名单。

【易混对比】

  • DEL vs UNLINK:功能相同,前者同步释放内存(阻塞),后者把内存回收交给后台线程(不阻塞)。
  • SAVE vs BGSAVE:前者阻塞主线程,后者 fork 子进程做快照、主线程继续服务。
  • KEYS vs SCAN:前者一次返回全部(阻塞),后者游标分批(不阻塞、但不保证快照一致)。
  • 换问法:若题干问“删除一个含百万字段的 Hash 应该用什么命令”,答案是 UNLINK。

【自测】 某个 key 是一个含 200 万字段的 Hash,直接 DEL 会阻塞 Redis 主线程。改用哪个命令可以避免阻塞?为什么?

答:UNLINK。它只把 key 从键空间中摘除并立即返回,真正的内存回收交给后台线程异步完成,因此不会卡住主线程。与 R25、R28 连考;生产运维高频。

【知识关联】

  • 补题关联:补-28(大对象编码带来的额外开销)、补-26(限流从客户端保护 Redis)。
  • 面试/工程:线上禁用 KEYS/FLUSHALL/FLUSHDB(rename-command);大 Hash/List/Set 要拆分;分析用 SCAN 或离线 RDB。HGETALL 大 Hash 会一次返回巨量数据,阻塞主线程并打满网卡。MONITOR 在生产几乎必挂。用 busy-reply-threshold/lua-time-limit 给脚本设超时。
  • 面试追问:① HGETALL 大 Hash 为什么危险?(O(N) 且一次返回全部字段,阻塞+带宽双杀) ② 如何替代 KEYS?(SCAN 游标 + MATCH;或业务侧维护索引结构)

【拓展延伸】

  • 变式问法:给出命令列表选最危险:KEYS、SAVE、大 SORT、大 LRANGE、大 HGETALL。
  • 参数/命令:rename-command KEYS "";lua-time-limit;busy-reply-threshold(6.0+);CLIENT KILL;LATENCY HISTORY;客户端读超时与熔断。

R27. 保证缓存与数据库数据一致性,生产中最常用的策略是? ​

考点:缓存与数据库一致性

A. Cache Aside:先更新数据库,再删除缓存(配合延迟双删、失败重试/MQ 补偿处理并发与失败) B. 先删除缓存再更新数据库,且不做任何补偿 C. 只更新缓存,不更新数据库 D. 两边都写,不关心顺序与失败

答案:A

【考点】Cache Aside 模式的正确顺序与兜底手段。

【结论】 Cache Aside 的标准做法是“读时回填、写时删除” —— 先更新数据库、再删除缓存,并用延迟双删、失败重试 / MQ 补偿处理并发与失败 —— 选 A。

【逐项辨析】

  • A Cache Aside:先更新数据库,再删除缓存(配合延迟双删、失败重试 / MQ 补偿处理并发与失败):正确。 顺序对(先库后缓存)、动作对(删除而非更新)、兜底也对。
  • B 先删除缓存再更新数据库,且不做任何补偿:错。错在“先删除缓存”与“不做任何补偿” —— 删完到更新完之间若有读请求,会把旧值回填进缓存;没有兜底则会长期脏读。
  • C 只更新缓存,不更新数据库:错。错在“不更新数据库” —— 数据库是权威数据源,不写库则数据随时可能丢失。
  • D 两边都写,不关心顺序与失败:错。错在“不关心顺序” —— 并发写场景下两边写入顺序错乱必然留下脏数据。

【知识点】 四种缓存更新策略的定位:

策略读操作写操作一致性适用场景
Cache Aside读缓存,miss 则查库并回填更新库 + 删除缓存最终一致生产最常用
Read Through由缓存层代理回源由缓存层代理写库最终一致依赖缓存组件支持
Write Through同上写缓存同时同步写库较强写多、对一致性要求较高
Write Behind同上只写缓存,异步批量刷库弱写极多、可容忍丢数据

为什么是“删除”而不是“更新”:更新缓存需要把新值写入,并发场景下两个写请求可能以错误顺序落到缓存,留下旧值;删除则让“下次读自然回填最新值”,把并发问题收敛到“回填”这一步。

为什么是“先库后缓存” —— 推导两种顺序的脏窗口:

text
先删缓存、后更新库:
  删缓存 → [窗口] → 更新库
  窗口内读请求:缓存 miss → 从库里查到旧值 → 回填旧值
  → 更新库完成,但缓存里长期留着旧值(不自愈)

先更新库、后删缓存:
  更新库 → [窗口] → 删缓存
  窗口内读请求:读到的是缓存里的旧值(稍后即被删除)
  窗口结束后:缓存为空 → 下次读回填新值(可自愈)

剩余风险与兜底:① 并发回填窗口 —— 读请求 A 在“更新库”之前查到旧值,但回填动作发生在“删缓存”之后,旧值被写回缓存;用延迟双删(更新库后延迟一段时间再删一次,延迟时间应大于一次读操作加回填的耗时)收敛。② 删除缓存失败 —— 用本地消息表 / 定时任务重试、MQ 异步重试,或订阅 binlog(如 Canal)异步删除,保证“库改了,缓存最终也会被删”。③ 极端一致性要求 —— 可考虑 binlog + MQ 驱动的最终一致方案。

一致性等级的提醒:强一致无法只靠缓存实现,工程上必须先明确业务能接受的一致性等级(最终一致 / 准实时一致),再选方案。

【记忆锚点】 「读时回填、写时删除;先库后缓存,失败要补偿」 —— 顺序、动作、兜底三件事缺一不可。

【易混对比】

  • 删除缓存 vs 更新缓存:删除更安全(避免并发写顺序错乱),代价是下一次读多一次 miss 回填。
  • 先删缓存 vs 先更新库:先删缓存会把旧值回填进缓存(即 B 的问题);先更新库残留的脏窗口更短且能自愈。
  • 延迟双删 vs binlog 订阅:前者是应用侧定时补偿,简单但有延迟上限;后者由数据库侧驱动、更可靠,适合对一致性要求更高的场景。
  • 换问法:若题干问“为什么写操作是删除缓存而不是更新缓存”,答案是“避免并发写入顺序错乱导致缓存长期留存旧值”。

【自测】 写请求更新数据库成功,但删除缓存时因网络抖动失败了。缓存里会长期是旧值吗?怎么兜底?

答:只要有兜底就不会长期脏读。可用本地消息表或 MQ 重试删除,或订阅 binlog(Canal)异步删除,再配合延迟双删缩短脏窗口。与 R19、R28 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-30(先更库再删缓存,删除失败要补偿)、补-17(一致性要求决定策略)。
  • 面试/工程:Cache Aside(先更 DB 再删缓存)最常用:读未命中回源并回填;写只删不更,避免并发写覆盖。若删缓存失败,可靠补偿是重试队列/MQ + 订阅 binlog(Canal/Debezium)异步对账删;再叠加 TTL 兜底。延迟双删可缓解“先删后更”的旧值回填窗口,延迟时长难精确,只能缓解。
  • 面试追问:① 为什么不是“先删缓存再更库”?(删后、更前若有读回源会把旧值填回去,不一致窗口更难控) ② 延迟双删延迟多久合适?(经验值如几百 ms–1s,本质是覆盖主从延迟+回源耗时,无法理论精确)

【拓展延伸】

  • 变式问法:对比 Cache Aside / Read-Through / Write-Through / Write-Behind;问“删除失败怎么办”→ MQ 重试 + binlog 订阅 + TTL 兜底。
  • 参数/命令:Canal/Debezium 订阅 binlog → 消费删除;重试表/MQ 幂等消费;缓存 TTL 作最终一致;监控删除失败率与 binlog 延迟。

R28. 线上发现某个 key 的 QPS 极高(热 key),下列优化思路无助于缓解热 key 的是? ​

考点:热 key 治理

A. 增加本地缓存(多级缓存),让读请求在应用层就被挡住 B. 把热 key 复制成多个副本,分散到不同 Redis 节点,读取时随机选一个 C. 对读请求做限流、降级与熔断,保护后端 D. 给该 key 设置更长的过期时间,让它更长时间命中缓存

答案:D

【考点】热 key 的成因与治理手段。

【结论】 热 key 的瓶颈是单个节点的处理能力上限,延长过期时间与这个瓶颈无关,反而延长了热点的持续时间 —— 因此 D 不合理,选 D。

【逐项辨析】

  • A 增加本地缓存(多级缓存),让读请求在应用层就被挡住:合理。这是“减少请求量”,把大部分读请求在应用进程内消化掉。
  • B 把热 key 复制成多个副本,分散到不同 Redis 节点,读取时随机选一个:合理。这是“分散压力”,用随机后缀把单 key 的 QPS 摊到多个节点上。
  • C 对读请求做限流、降级与熔断,保护后端:合理。这是“服务治理”,在压力超过承载能力时保护 Redis 与数据库不被压垮。
  • D 给该 key 设置更长的过期时间,让它更长时间命中缓存:对缓解热 key 无帮助(本题选它)。 热 key 的瓶颈是单个节点的处理能力上限,过期时间长短与这个上限无关;延长 TTL 只会让热点在同一个节点上停留更久,还可能因长期不回源而读到更旧的数据。(注意区分:延长 TTL/永不过期是治「缓存击穿」与减少回源的常规手段,但不是热 key 的解法。)

【知识点】 热 key 指单位时间内访问次数远高于其他 key(如单 key QPS 占实例总量很大比例)的 key。典型成因:秒杀商品、明星八卦、突发热点新闻、大促爆款、被爬虫集中访问的 key。

热 key 的两层瓶颈:

瓶颈层表现说明
单节点 / 单线程该节点 CPU 打满、延迟升高Redis 主线程是单线程,单实例 QPS 有天花板
网络带宽单机网卡打满单 key 返回值越大越先撞上带宽瓶颈(大 value 热 key 更危险)

治理思路分三层:

层次手段代价
减少请求量本地缓存(如 Caffeine)+ 多级缓存本地缓存有一致性与容量上限问题
分散压力热 key 加随机后缀拆成 N 个副本(hotkey:1 至 hotkey:N),随机读取需维护 N 个副本的一致性
服务治理读限流、降级返回兜底数据、熔断会牺牲部分功能或返回降级数据

副本方案的关键细节:写操作必须同步更新所有副本(或借助 MQ、定时任务对齐),否则各副本取值不一致;副本数 N 按“预估 QPS ÷ 单节点承载能力”估算,N 越大一致性维护成本越高。

排查手段:redis-cli --hotkeys 基于 LFU 统计(要求 maxmemory-policy 为 LFU 系列,只统计内存中的 key、有采样误差);MONITOR 实时打印所有命令,但本身开销极大、只能短时使用;生产上更常用客户端埋点 / 代理层统计或监控平台的热 key 探测。

【记忆锚点】 「热 key 要“分流”,不要“续命”」 —— 延长过期时间只让热点更持久,正确做法是本地缓存分流、副本分散压力、限流兜底。

【易混对比】

  • 热 key vs 大 key:热 key 是访问频率高(QPS 问题),大 key 是单 key 体积大(内存与网络问题);两者叠加时最危险。治理手段不同:热 key 靠分流,大 key 靠拆分(如 Hash 分桶)。
  • 热 key vs 缓存击穿:击穿是热点 key 过期瞬间的并发回源(一次性事件,解法是互斥锁 / 逻辑过期);热 key 是持续高频访问(长期状态,解法是分流与本地缓存)。
  • 本地缓存 vs Redis 缓存:本地缓存无网络开销、性能最好,但多实例之间不一致、容量受 JVM 堆限制。
  • 换问法:若题干问“热 key 加随机后缀副本的代价是什么”,答案是“需要维护多个副本的一致性,写入时要写全部副本”。

【自测】 秒杀场景中商品详情 key 的 QPS 达到 10 万,而 Redis 单节点约能承受 8 万。除了加本地缓存,还可以怎么把压力摊到多个节点?

答:把该 key 拆成 N 个副本(如 item:1001:1 至 item:1001:10),读取时随机取一个副本,把 10 万 QPS 摊到 10 个 key 上;写入时需同步更新全部副本。与 R27 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-26(限流)、补-27(数据结构与访问模式匹配)、补-28(编码/大对象)。
  • 面试/工程:热 key 与大 key 双高最危险:单分片 CPU/带宽打满。治理组合:发现(--hotkeys、客户端采样、网关统计)→ 本地缓存(Caffeine)挡读 → 多副本/拆 key 分散 → 限流保命。写路径要注意多副本同步与热点重建风暴(可与 R08 互斥重建组合)。延长 TTL 只能缓解过期风暴,治不了访问热点本身。
  • 面试追问:① 如何发现热 key?(LFU 下 --hotkeys、Proxy/网关采样、MONITOR 抽样慎用、业务监控) ② 副本方案的写路径要注意什么?(多副本一致性/失效广播,避免读到长期旧副本)

【拓展延伸】

  • 变式问法:题干换成“不合理的是”时,把“延长 TTL/让热点更久”“只靠加机器”设为错误项;对比热 key vs 大 key vs 两者叠加。
  • 参数/命令:redis-cli --hotkeys(需 maxmemory-policy allkeys-lfu 或 volatile-lfu);MEMORY USAGE;客户端本地缓存随机 TTL;hot:{id}:r1/r2 多后缀副本;网关按 key 限流。

持续学习,持续积累。