单线程模型与 IO 多路复用与分布式锁与缓存一致性

共 20 题
📑 题目列表 20 题
#
★★★

1. Redis 为什么选择单线程模型处理命令?单线程为何仍能达到十万级 QPS(纯内存操作、IO 多路复用、避免锁竞争与上下文切换)?

请说明 Redis 为什么选择单线程模型处理命令,以及单线程为何仍能达到十万级 QPS?

  • 单线程模型的由来(瓶颈在 IO 而非 CPU)
  • 纯内存操作 + IO 多路复用
  • 避免锁竞争与上下文切换

Redis 早期选择单线程模型的原因:命令处理是纯内存操作,CPU 不是瓶颈,瓶颈在网络 IO;单线程避免了多线程的锁竞争、上下文切换与数据同步开销,同时使命令天然原子、实现简单。配合 IO 多路复用(epoll),单线程能同时处理大量连接,在网络与内存足够快时达到十万级 QPS。单线程也能保证操作的原子性(无需加锁)。当网络 IO 成为新瓶颈时,Redis 6.0 引入多线程 IO 解决读写瓶颈,但命令执行仍单线程。

单线程并非性能差,而是"瓶颈不对称"下的正确选择:内存快、CPU 富余、网络与锁是主坑。多线程会引入锁竞争与一致性问题,反而抵消收益。

#
★★★

2. Redis 的 IO 多路复用底层(Linux epoll、macOS kqueue)与 select/poll 的本质差异(fd 数量限制、O(n) 遍历、水平触发 LT 与边缘触发 ET)是什么?

请说明 Redis 的 IO 多路复用底层(epoll、kqueue)与 select/poll 的本质差异?

  • select/poll 的 fd 限制与 O(n) 遍历
  • epoll 的事件驱动与 O(1)
  • LT 与 ET 触发模式

select 有 fd 数量限制(1024)且每次调用都需遍历所有 fd 检查就绪,O(n);poll 用链表无 fd 数量限制但仍是 O(n) 遍历。epoll(Linux)通过注册 fd 到内核事件表,由内核维护就绪链表,调用时仅返回就绪 fd,O(1) 且无 fd 数量限制;kqueue 是 macOS/BSD 的同类机制。epoll 支持水平触发(LT,就绪则反复通知)与边缘触发(ET,仅状态变化时通知一次)。Redis 使用 ae 事件驱动封装,Linux 用 epoll,macOS 用 kqueue。

三大多路复用机制的本质差异是"用户态遍历 vs 内核就绪事件":select/poll 需要用户反复扫描,epoll/kqueue 由内核主动通知就绪事件,从而支持海量连接时的高效。

#
★★★

3. Redis 6.0 的多线程 IO 如何工作(子线程只做 socket 读写与协议解析,命令执行仍单线程)?为什么命令执行阶段不多线程化?

请说明 Redis 6.0 多线程 IO 的工作原理,以及为什么命令执行阶段仍然单线程?

  • 多线程 IO 只做读写与解析
  • 命令执行仍单线程
  • 理由(避免竞态、事务、脚本、数据一致性)

Redis 6.0 引入多线程 IO:主线程负责命令执行,若干 IO 线程只负责 socket 的读写、协议解析与序列化,从而减少网络 IO 对主线程的占用。命令执行阶段仍由单线程完成,因为:命令执行需要保证操作原子性(事务、Lua、多命令),多线程执行命令会引入竞态、锁与顺序问题,且纯内存命令执行很快,CPU 不是瓶颈。多线程 IO 提升了网络吞吐,命令执行保持单线程以维护数据一致性与简单性。

多线程 IO 解决的是"网络 IO 慢"而非"命令执行慢";命令执行单线程保证原子性、避免加锁,是正确性优先的取舍。

#
★★★

4. Redis 事件驱动框架(aeEventLoop)如何统一处理文件事件(网络 IO)与时间事件(serverCron)?一次事件循环的处理流程是怎样的?

请说明 Redis 事件驱动框架 aeEventLoop 如何统一处理文件事件与时间事件,以及一次事件循环的处理流程?

  • aeEventLoop 的结构
  • 文件事件(网络 IO)与时间事件(serverCron)
  • 事件循环流程

Redis 用 aeEventLoop 事件驱动框架统一管理两类事件:文件事件(fd 上的网络 IO 可读/可写)与时间事件(定时任务,如 serverCron 周期任务)。一次事件循环流程:aeProcessEvents 计算距最近时间事件的时间差,作为 epoll_wait 的超时;调用 epoll_wait 等待文件事件就绪,期间超时则处理时间事件;文件事件就绪后按优先级调用对应 handler(读事件 → 处理命令 → 写事件);循环往复。serverCron 作为时间事件周期性执行过期清理、统计、集群心跳等后台任务。

事件循环是 Redis 单线程的"心脏",把"网络 IO 就绪"与"定时任务"统一调度到同一循环,既保证 IO 及时响应,又周期执行后台任务,且不阻塞命令执行。

#
★★★

5. 单线程模型下 BLPOP/XREAD BLOCK 等阻塞命令如何实现(把客户端挂入阻塞列表、有数据推送时唤醒,而不占用事件循环)?

请说明单线程模型下 BLPOP/XREAD BLOCK 等阻塞命令如何实现,不占用事件循环?

  • 阻塞命令的挂起机制
  • 客户端挂入阻塞列表
  • 数据到达时唤醒

阻塞命令(BLPOP、XREAD BLOCK)在数据未就绪时,会把客户端挂入对应的阻塞列表(阻塞 key 的等待队列),并释放事件循环,让事件循环继续处理其他客户端的请求。当数据到达(如 BLPOP 的 key 被 LPUSH),Redis 会唤醒阻塞的客户端,将数据返回并解除阻塞。关键是不用"自旋"或"独占事件循环",而是把等待状态挂起,由事件驱动唤醒,从而既支持阻塞语义又不影响其他请求。

阻塞命令的实现是"挂起等待 + 事件唤醒",把客户端从事件循环中摘除,数据就绪时再挂回,避免阻塞点占用主线程,是单线程模型下实现阻塞 IO 的关键。

#
★★★

6. Redis 后台持久化(bgsave/bgrewriteaof 的 fork + 写时复制)如何与主线程协作?fork 耗时与写放大对延迟有何影响?

请说明 Redis 后台持久化(bgsave/bgrewriteaof)的 fork + 写时复制如何与主线程协作,以及 fork 耗时与写放大对延迟的影响?

  • fork 子进程 + COW 的原理
  • 主线程与子进程协作
  • fork 耗时与写放大对延迟的影响

bgsave 与 bgrewriteaof 通过 fork 创建子进程,子进程利用"写时复制"(COW)读取内存快照并写入磁盘,主线程继续处理命令。COW 下,主线程处理命令时若修改某页,会先复制该页再修改,子进程仍看到快照。fork 过程会复制页表,大内存实例 fork 耗时较长,期间主线程阻塞;写放大体现在频繁写操作会触发大量 COW 页复制,增加内存与 CPU 开销。对延迟的影响:fork 阻塞主线程(毫秒级到秒级),COW 增加内存占用与 IO,可能引发延迟抖动。优化:控制实例内存、避免在高峰期 bgsave、合理配置 RDB 触发。

fork + COW 让持久化不阻塞命令处理,但 fork 本身与写放大带来隐性开销。理解 COW 页复制与 fork 时长的关系,才能评估持久化对延迟的影响。

#
★★★

7. Redis 单线程与 CPU 瓶颈的关系是什么?如何通过多实例部署充分利用多核?

请说明 Redis 单线程与 CPU 瓶颈的关系,以及如何通过多实例部署充分利用多核?

  • 单线程只能用一个核
  • CPU 何时成为瓶颈
  • 多实例部署(多进程、绑定核)

Redis 命令执行单线程,只能使用一个 CPU 核,因此单实例无法利用多核并行。当单实例达到 CPU 单核上限(如高并发命令、大对象操作)时,CPU 成为瓶颈。解决方案是多实例部署:在一台多核机器上启动多个 Redis 实例(不同端口、不同数据),每个实例绑定一个核(taskset/numactl),通过客户端分片或代理把数据分散到各实例,从而充分利用多核。Cluster 天然支持多实例分片。

单线程的"单核宿命"决定了扩展要靠"多进程"而非"多线程"。多实例 + 分片是 Redis 利用多核的标准手段,通常配合 Cluster 或代理。

#
★★★

8. Redis 7 在 IO 多线程上做了哪些改进(io-threads 动态启停)?

请说明 Redis 7 在 IO 多线程上做了哪些改进,特别是 io-threads 动态启停?

  • io-threads 配置
  • 多线程 IO 的动态启停
  • 改进点

Redis 7 改进了 IO 多线程:支持 io-threads 配置,且在运行时可以通过 CONFIG SET 动态调整 io-threads 的数量(动态启停 IO 线程),无需重启。相比 Redis 6(需重启才能改线程数),Redis 7 增强了灵活性,允许根据负载在线调整线程数,同时优化了多线程 IO 的调度与减少锁竞争。命令执行仍保持单线程。

动态启停 IO 线程让运维能按实际负载在线调整,避免重启,是 Redis 7 对多线程 IO 可运维性的改进。

#
★★★

9. Redis 分布式锁的标准实现 SET key val NX PX + Lua 脚本解锁(先校验 val 再 DEL)是什么?为什么解锁必须原子、val 为什么要唯一?

请说明 Redis 分布式锁的标准实现(SET key val NX PX + Lua 解锁),以及为什么解锁必须原子、val 必须唯一?

  • SET NX PX 加锁
  • Lua 校验 + DEL 解锁
  • 原子性、唯一 val 防误删

加锁用 SET lock:key unique_value NX PX 30000:NX 保证只有锁不存在时才能设置(互斥),PX 设置过期时间(防死锁)。解锁用 Lua 脚本:先 GET 校验 value 是否等于自己持有的唯一值,相等才 DEL,保证原子性。解锁必须原子:若先 GET 再 DEL 两步,可能在校验后、删除前锁已过期被他人获取,导致误删他人锁。val 必须唯一:每个客户端持有随机唯一值,解锁时校验,防止误删(自己超时后锁被他人持有,仍能判断不是自己的锁)。

标准锁的三大要素是"NX 互斥 + PX 防死锁 + 唯一 val 防误删"。唯一 val 与 Lua 原子校验构成"只有持有者才能删除"的语义,是防止误删他人锁的关键。

# 加锁
SET lock:order 550e8400-e29b-41d4-a716-446655440000 NX PX 30000
# 解锁(Lua,原子)
if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1])
else
  return 0
end
#
★★★

10. Redis 单线程下 Lua 脚本的原子性边界与集群模式的多 key(slot)限制

请说明 Redis 单线程下 Lua 脚本的原子性边界,以及集群模式下多 key 的 slot 限制?

  • Lua 单节点原子性
  • 集群下脚本 key 必须同槽
  • 原子性边界

单线程模型下 Lua 脚本在单节点内原子执行,脚本内的多步操作不会被打断,也不会被其他命令插入。但原子性边界限于单节点:集群模式下脚本中所有 key 必须落在同一槽位(否则 CROSSSLOT 错误),因为脚本只在单个节点执行,无法跨节点原子操作。因此脚本的"原子事务"作用域是"单节点内的多个 key",跨节点/跨槽位操作无法通过 Lua 保证原子性。

Lua 的原子性 = 单节点内原子。跨节点一致性需通过业务层或其他机制保证,设计脚本时需确保 key 通过 hash tag 聚到同槽。

#
★★

11. 单线程 Redis 的阻塞点盘点,慢命令、大 Key 删除、AOF fsync、内存淘汰与网络拥塞如何逐项治理?

请盘点单线程 Redis 的阻塞点,并说明慢命令、大 Key 删除、AOF fsync、内存淘汰与网络拥塞如何逐项治理?

  • 各阻塞点的识别
  • 每项的治理手段
  • 阻塞的监控

单线程 Redis 的阻塞点与治理:慢命令(如 KEYS、SORT、大范围操作)用 SCAN 替代、避免 O(N) 命令;大 Key 删除(DEL 大 key)用 UNLINK 异步删除;AOF fsync(everysec 时 fsync 阻塞)用 no-appendfsync-on-rewrite 或调整策略;内存淘汰(大量 key 同时淘汰)合理设置 maxmemory 与淘汰策略、避免内存耗尽;网络拥塞(客户端并发/慢速客户端)用 io-threads 多线程、限制慢客户端、连接池。治理原则是"识别阻塞源 + 异步化 + 避免 O(N) + 限流与监控"。

单线程下任何耗时的操作都会阻塞所有请求,因此治理核心是"消除阻塞点":把重操作异步化(UNLINK、多线程 IO)、避免 O(N) 命令、控制内存与网络。

#
★★

12. Redis Pipeline 的原理与收益,一次 RTT 批量发送命令如何提升吞吐?与事务、多路复用的关系?

请说明 Redis Pipeline 的原理与收益,以及它与事务、多路复用的关系?

  • Pipeline 批量发送减少 RTT
  • 与事务(MULTI/EXEC)的差异
  • 与多路复用的关系

Pipeline 把多条命令一次发送到服务端,服务端批量执行后一次返回结果,减少多次网络往返(RTT),显著提升吞吐,尤其在高延迟网络下收益明显。与事务的差异:Pipeline 只是减少 RTT,不保证原子性(命令可被其他客户端命令插入);事务(MULTI/EXEC)保证原子执行。Pipeline 通常与多路复用结合(客户端 fd 复用),但 Pipeline 本身靠"批量发送 + 批量接收"减少往返,与多路复用(服务端处理多个连接)是不同层面的优化。

Pipeline 优化的是"客户端到服务端的往返次数",事务保证"服务端执行原子性",两者可组合(Pipeline 内含 MULTI/EXEC)。Pipeline 不适合有依赖顺序的批量(需依赖上一次结果)。

#
★★

13. Redis 事务 WATCH 的乐观锁语义,CAS 式检测与冲突重试的工程实现,与分布式锁的适用边界?

请说明 Redis 事务 WATCH 的乐观锁语义(CAS 式检测与冲突重试),以及它与分布式锁的适用边界?

  • WATCH 的 CAS 语义
  • 冲突重试的工程实现
  • 与分布式锁的边界

WATCH 提供乐观锁语义:WATCH 监控若干 key,执行 MULTI/EXEC 事务时,若任一被监控 key 在 WATCH 后被其他客户端修改,EXEC 返回 null(事务放弃),客户端需重试。这类似 CAS(比较并交换):只有当 key 未变时才执行。工程上需循环重试直到成功。适用边界:WATCH 适合"基于当前值做条件更新"的乐观并发控制(如计数、库存),冲突多时重试开销大;分布式锁适合"需要互斥访问临界区"的场景,用悲观互斥避免重试。WATCH 无锁、轻量,锁有等待与死锁风险。

WATCH 是"乐观并发",靠版本检测 + 重试;分布式锁是"悲观互斥",靠持有锁排他。选 WATCH 还是锁取决于冲突频率与操作复杂度。

#
★★

14. Redisson 可重入锁与公平锁的实现,为什么默认推荐可重入非公平锁?看门狗续期与故障转移风险?

请说明 Redisson 可重入锁与公平锁的实现,为什么默认推荐可重入非公平锁,以及看门狗续期与故障转移风险?

  • 可重入锁(RLock)与公平锁(FairLock)
  • 默认可重入非公平锁的原因
  • 看门狗续期与故障转移风险

Redisson 可重入锁(RLock)基于 Redis Hash + Lua 实现,同一线程可重复加锁(计数);公平锁(FairLock)在 Redis 上维护队列保证先到先得。默认推荐可重入非公平锁,因为:公平锁要维护队列、多一次 RTT 与额外开销,性能更低;非公平锁在大多数场景足够且性能好、实现简单。可重入满足同线程嵌套加锁。看门狗(watchdog)默认 30 秒,每 10 秒续期,防止业务未完成锁已过期;但存在故障转移风险:主从切换时锁未同步到新主,可能丢失锁导致互斥被破坏。

默认可重入非公平锁是"性能优先"的选择,公平锁以性能换公平性。看门狗解决锁过期问题,但主从异步复制下锁丢失是分布式锁的固有风险,需结合 fencing 或业务容忍。

#
★★

15. 分布式锁的替代方案对比,数据库唯一约束、ZooKeeper 临时顺序节点与 Redis 锁各自的一致性保证与性能?

请对比数据库唯一约束、ZooKeeper 临时顺序节点与 Redis 锁三种分布式锁方案的一致性保证与性能?

  • 数据库唯一约束锁
  • ZK 临时顺序节点锁
  • Redis 锁

数据库唯一约束(如唯一索引 / SELECT ... FOR UPDATE)提供强一致(数据库事务),但性能低、有单点与锁粒度问题;ZooKeeper 临时顺序节点提供强一致(ZK 的 ZAB 协议保证 + 临时节点自动释放),性能中等,适合对一致性要求高的场景;Redis 锁(SET NX PX)性能最高、实现简单,但主从异步复制下可能丢锁(弱一致)。一致性排序:ZK/DB 强一致 > Redis 弱一致;性能排序:Redis > ZK > DB。选型依据业务对一致性与性能的要求。

分布式锁的三方权衡是"一致性 vs 性能 vs 复杂度":DB 简单但慢、ZK 强一致但重、Redis 快但弱一致。大多数缓存/分布式场景用 Redis,强一致场景用 ZK。

#
★★

16. 锁释放的原子性,为什么解锁必须用 Lua 比较删除?业务超时后锁被他人持有时的误删如何避免?

请说明锁释放的原子性,为什么解锁必须用 Lua 比较删除,以及业务超时后锁被他人持有时的误删如何避免?

  • 解锁的 GET + DEL 原子性问题
  • Lua 比较删除
  • 唯一 val 防误删

解锁必须用 Lua 脚本原子地"先校验 val 再 DEL":如果分两步(GET 校验、DEL 删除),在校验与删除之间锁可能过期被他人获取,此时 DEL 会误删他人锁。用 Lua 在单条原子命令内完成校验与删除,只有 val 匹配自己持有的唯一值才删除。业务超时后,锁被他人持有,此时自己再解锁会因为 val 不匹配而失败,从而避免误删。

解锁原子性 + 唯一 val 是防误删的双保险:原子校验保证"校验与删除不可分割",唯一 val 保证"只删自己的锁"。两者缺一不可。

#
★★

17. 锁续期(Redisson 看门狗 watchdog)如何解决业务执行时间超过锁过期时间的问题?续期线程异常或进程假死会带来什么风险?

请说明 Redisson 看门狗(watchdog)如何解决业务执行时间超过锁过期时间的问题,以及续期线程异常或进程假死带来的风险?

  • 看门狗续期机制
  • 锁过期时间与续期
  • 续期异常的风险

Redisson 看门狗(watchdog)在加锁后启动后台线程,周期性(默认每 10 秒)检查锁是否仍被持有,若是则把锁过期时间续期(默认 30 秒),从而保证业务执行时间超过初始锁过期时间时锁不会提前失效。风险:若续期线程异常或进程假死(未续期),锁会到期自动释放,此时其他客户端可获取锁,而原业务仍在执行,导致两个客户端同时持有锁(互斥被破坏)。因此续期本身依赖进程健康,进程假死会破坏锁语义。

看门狗解决"锁过期 vs 业务超时"的冲突,但依赖持续续期。续期失败(进程假死)反而造成锁提前释放,这是分布式锁的固有风险,需用 fencing 或业务兜底。

#
★★

18. Redlock 争议的核心是什么(Kleppmann 指出时钟跳变与 GC/进程暂停会破坏安全性,antirez 的反驳)?哪些场景下 Redlock 仍可接受、哪些场景必须用 fencing?

请说明 Redlock 争议的核心(Kleppmann 的时钟跳变与 GC 暂停批评、antirez 的反驳),以及哪些场景下 Redlock 仍可接受、哪些必须用 fencing?

  • Redlock 原理
  • Kleppmann 的批评(时钟跳变、GC 暂停)
  • antirez 的反驳

Redlock 通过"向 N 个独立 Redis 节点加锁,多数派成功才算持有"来提升可靠性。Kleppmann 批评:时钟跳变或进程 GC 暂停会导致锁过期时间被误判,多个客户端可能同时持有锁;且即使拿到锁,异步的复制/GC 也可能让"锁持有者"与"资源操作"未对齐。antirez 反驳:这些是分布式系统的固有风险,Redlock 在合理假设下(时钟漂移可控、多数派存活)足够安全,且多数场景不需要极端强保证。可接受场景:对锁偶尔失效影响小的业务(如幂等操作、缓存重建);必须用 fencing(单调递增 token 由资源端校验)的场景:涉及资金、库存等强一致、要求"锁持有者身份可验证"的临界资源操作。

Redlock 争议本质是"分布式锁安全性的理论边界 vs 工程实用"。Redlock 适合多数工程场景,但强一致临界资源必须用 fencing token 让资源端拒绝过期锁持有者的操作。

#
★★

19. 为什么基于 Redis 主从异步复制的分布式锁在主从切换瞬间可能破坏互斥(锁尚未同步到从库即完成故障转移)?fencing token 单调递增编号如何兜底?

请说明为什么基于 Redis 主从异步复制的分布式锁在主从切换瞬间可能破坏互斥,以及 fencing token 单调递增编号如何兜底?

  • 主从异步复制下锁丢失
  • 锁未同步即切换
  • fencing token 兜底

Redis 主从复制是异步的,客户端加锁的 SET 命令在主库执行后异步同步给从库。若主库在锁尚未同步到从库时故障,Sentinel/Cluster 可能把未持有锁的从库提升为新主,新主没有该锁,其他客户端可重新加锁,导致两个客户端同时"持有"锁,破坏互斥。fencing token 兜底:加锁时分配单调递增的 token(如时间戳或递增序号),资源端校验 token,只接受 token 大于当前记录的请求,从而拒绝持有旧 token(锁已过期)的客户端操作,即使逻辑上它"以为"自己还持有锁。

主从切换丢锁是异步复制 + 故障转移的固有风险,无法靠锁本身完全消除。fencing token 让"资源端"来验证锁持有者的时代,是唯一能兜底的方式,适合强一致场景。

#

20. Redis 的慢查询(SLOWLOG)与延迟监控(LATENCY 事件)定位阻塞源

请说明 Redis 的慢查询(SLOWLOG)与延迟监控(LATENCY 事件)如何用于定位阻塞源?

  • SLOWLOG 记录慢命令
  • LATENCY 事件(延迟监控)
  • 定位阻塞源的方法

SLOWLOG 记录执行时间超过 slowlog-log-slower-than 的命令(含命令、耗时、参数),用于定位慢命令(如 KEYS、过大范围操作)。LATENCY 事件(LATENCY DOCTORLATENCY LATESTLATENCY HISTORY)监控 Redis 内部延迟事件(如 fork、aof-write、eviction、command),帮助定位延迟来源(如后台持久化、淘汰、RDB 生成)。结合两者可定位阻塞源:慢命令看 SLOWLOG,系统级事件看 LATENCY,再结合 CPU、内存、网络诊断为完整定位。

SLOWLOG 定位"命令级"慢,LATENCY 定位"系统/事件级"延迟,两者互补。定位阻塞源后针对性优化(改命令、调参数、异步化)。