Easton Notes
八股Redis 面试题

Redis 线程模型、事务与持久化

Redis 单线程语义、I/O 多路复用、事务与 Lua 原子性边界,以及 RDB、AOF 和混合持久化高频面试题。

Redis 是单线程的吗,为什么单线程仍然很快?

回答重点

“Redis 单线程”通常指命令在主线程中串行执行,核心数据结构不会被多个命令执行线程同时修改,并不表示 Redis 进程只有一个线程。

Redis 6.0 以前,普通客户端的网络读写、协议解析和命令执行主要由主线程完成;Redis 6.0 起可以启用 I/O 线程并行处理部分网络读写和协议解析,但命令执行仍主要由主线程串行完成。

Redis 快的主要原因是:

  • 数据主要在内存中处理,避免了普通请求路径上的随机磁盘 I/O。
  • String、哈希表、跳表、quicklist 等结构针对常用访问模式做了优化。
  • 命令串行执行避免了数据路径上的大量锁竞争和线程切换。
  • I/O 多路复用允许一个事件循环管理大量连接,不需要为每个连接创建线程。

不能简单表述为“单线程一定比多线程快”。Redis 的优势来自工作负载和实现取舍;大 key 操作、全量集合运算、复杂 Lua 脚本等长任务仍会阻塞主线程。CPU 成为瓶颈时,通常通过拆分数据、使用多个实例或 Cluster 扩展,而不是依赖一个实例并行执行普通命令。

Redis 哪些工作使用了后台线程或 I/O 线程?

回答重点

Redis 会把部分可能阻塞主线程的工作交给后台执行:

  • 较早版本已通过 BIO 线程处理延迟关闭文件和 AOF fsync。
  • Redis 4.0 增加 lazy free 后台任务。UNLINK、FLUSHDB ASYNC、FLUSHALL ASYNC 等命令可以把实际内存释放推迟到后台,降低删除大对象造成的停顿。
  • BGSAVE 和 BGREWRITEAOF 的主体工作通常由 fork 出来的子进程完成,它们是子进程,不是后台线程。

Redis 6.0 引入了可选的网络 I/O 多线程:

io-threads 4
io-threads-do-reads yes
  • io-threads 4 表示主线程加 3 个 I/O 工作线程,不是额外增加 4 个线程。
  • 默认值是 io-threads 1,即不创建额外 I/O 工作线程;不能说 Redis 6 默认启动多个 I/O 线程。
  • 启用多个 I/O 线程后,默认主要并行写 socket;把 io-threads-do-reads 设为 yes 后,读取和协议解析也可并行。
  • I/O 线程不会并行执行普通 Redis 命令,数据操作仍由主线程协调和执行。

I/O 线程数不是越大越好,应在网络 I/O 已成为瓶颈时按 CPU 核数和压测结果配置。命令本身耗时或大 key 阻塞时,增加 I/O 线程不能解决根因。

Redis 如何用 I/O 多路复用处理大量连接?

回答重点

I/O 多路复用让一个事件循环同时关注多个 socket 的就绪状态:

  1. Redis 把监听 socket 和客户端 socket 注册到事件多路复用器。
  2. 事件循环等待可读、可写或连接到达等就绪事件,而不是阻塞在某一个客户端上。
  3. 内核返回已经就绪的文件描述符后,Redis 调用对应的 accept、读请求、写响应等处理器。
  4. 主线程解析并执行可以处理的命令,再进入下一轮事件循环。

Redis 会按操作系统选择 epoll、kqueue、select 等实现。它采用的是 Reactor 风格:多路复用器负责通知“哪些连接已就绪”,事件处理器负责真正读写和处理。

I/O 多路复用不等于多个请求在 CPU 上并行执行。Redis 6.0 以前,多个连接仍由一个主线程依次处理;Redis 6.0 的 I/O 线程只并行化网络阶段,命令执行阶段仍保持串行。

Redis 事务能保证原子性吗?

回答重点

需要先明确“原子性”的含义。MULTI、EXEC 提供的是命令组连续执行、不被其他客户端命令插入的隔离保证,但不提供关系型数据库式的失败回滚。

事务流程是:

  1. MULTI 开启事务。
  2. 后续命令进入队列,只返回 QUEUED。
  3. EXEC 按入队顺序执行全部命令。
  4. DISCARD 可以在 EXEC 前清空队列,此时没有命令被执行。

错误分为两类:

  • 入队前错误:命令名、参数个数等语法错误。Redis 2.6.5 起会标记事务,EXEC 时拒绝执行整个队列。
  • 执行期错误:命令语法正确,但执行时遇到类型不匹配等错误。只有该命令失败,队列中的其他命令仍会继续执行,已经产生的修改不会回滚。

所以不能回答“Redis 事务出错也保证原子性”。更准确的说法是:事务内命令不会被其他客户端插入;正常执行时可以整体连续完成,但执行期错误可能留下部分成功结果。

扩展知识

WATCH 提供乐观锁语义。被监视的 key 在 WATCH 到 EXEC 之间发生变化时,EXEC 会放弃执行并返回空结果,客户端需要重新读取和重试。它解决并发条件更新问题,也不提供执行后的回滚。

Lua 脚本如何保证原子性,边界在哪里?

回答重点

Redis 会把一个 Lua 脚本作为整体执行,脚本运行期间不会插入其他客户端命令。因此,脚本适合把“读取、判断、修改”组合成一个并发不可见中间状态的操作。

例如,释放分布式锁时先比较唯一令牌,再删除锁:

if redis.call("GET", KEYS[1]) == ARGV[1] then
  return redis.call("DEL", KEYS[1])
end

return 0

Lua 的原子性同样不代表失败回滚:

  • 脚本在执行若干写命令后发生运行时错误,之前的修改仍然保留。
  • redis.call() 遇到错误会终止脚本并把错误返回客户端;redis.pcall() 可以让脚本捕获错误,但不会自动撤销已有写入。
  • 长脚本会长时间占用主线程。达到时间阈值后 Redis 可以返回 BUSY,但已执行写操作的脚本不能再用 SCRIPT KILL 安全终止。

脚本应保持短小,并在执行写入前完成参数、类型和业务条件校验。在 Redis Cluster 中,脚本访问的 key 必须通过 KEYS 显式传入,并位于同一哈希槽。

RDB 快照如何生成,有什么优缺点?

回答重点

RDB 保存某一时刻数据集的紧凑二进制快照:

  • SAVE 在主进程中同步生成快照,会阻塞客户端请求,生产环境通常不直接使用。
  • BGSAVE 由主进程执行 fork,子进程把数据写入临时 RDB 文件,完成后原子替换旧文件。
  • 定时 save 规则、主从全量同步和正常关闭等流程也可能触发 RDB 保存。

BGSAVE 利用写时复制:父子进程起初共享内存页,父进程继续处理请求;快照期间被修改的页才会复制。它避免了整个写盘阶段阻塞主线程,但 fork 本身仍可能造成短暂停顿,大数据集和高写入量还可能带来明显的额外内存占用。

RDB 的优点是文件紧凑,适合备份、归档和跨环境传输,大数据集的加载通常也比逐条重放 AOF 更快。缺点是只能恢复到最近一次快照,故障时可能丢失两次快照之间的数据。

AOF 如何记录数据,三种刷盘策略有什么区别?

回答重点

AOF 记录会改变数据集的命令。Redis 执行写命令后,把对应协议内容追加到 AOF 缓冲区,再根据策略写入并同步到磁盘;重启时通过重放这些命令恢复数据。

appendfsync 有三种常见策略:

策略刷盘时机取舍
always每批写入 AOF 后执行 fsync数据丢失窗口最小,但延迟和吞吐受磁盘影响最大
everysec通常由后台线程每秒执行一次 fsync默认策略,性能与可靠性折中,故障时可能丢失约 1 秒数据
noRedis 不主动 fsync,交给操作系统性能较好,但数据丢失窗口由操作系统刷盘策略决定
appendonly yes
appendfsync everysec

AOF 一般比等价 RDB 占用更多空间,恢复时还需要执行命令,速度可能更慢。always 也不能替代磁盘可靠性、备份和跨节点容灾,不应表述成绝对“零丢失”。

AOF 重写和混合持久化是怎样实现的?

回答重点

AOF 重写不是简单压缩旧日志,而是根据当前数据状态生成一组足以重建数据集的最简命令。例如计数器连续增加 100 次,重写后不必保留 100 条历史增量。

版本差异如下:

  • Redis 7.0 以前:子进程生成新的单体 AOF;重写期间父进程继续写旧 AOF,并把增量积累到重写缓冲区,最后追加到新文件再原子替换。
  • Redis 7.0 起:采用多段 AOF。一个 BASE 文件表示重写时的基础数据,一个或多个 INCR 文件保存后续增量,manifest 记录文件及加载顺序。重写开始后父进程直接写新的 INCR 文件,减少旧方案的内存缓冲和重复写入问题。

“混合持久化”通常指 AOF 的基础部分使用 RDB 二进制格式,后续仍追加 AOF 命令:

aof-use-rdb-preamble yes

该能力在 Redis 4.0 引入,当时默认关闭;Redis 5.0 起默认开启。Redis 7.0 的多段 AOF 中,默认表现为 RDB 格式的 BASE 文件加 AOF 格式的 INCR 文件,兼顾基础数据加载速度与增量日志的持久性。

还要区分“混合 AOF 格式”和“同时开启 RDB、AOF”:

  • 混合 AOF 仍属于 AOF 恢复链路。
  • 同时开启两种机制时,可以保留独立 RDB 作为周期备份;Redis 启动时通常优先加载更完整的 AOF。

生产环境应该如何选择 RDB、AOF 或混合持久化?

回答重点

选择依据是可接受的数据丢失窗口、恢复时间、写入延迟、磁盘容量和备份要求:

需求常见选择
数据可随时从上游重建的纯缓存可以关闭持久化,但仍要评估缓存重建冲击
能接受分钟级数据丢失,重视备份和快速恢复RDB
希望把故障丢失窗口缩小到秒级AOF everysec
同时关注恢复速度和较小丢失窗口开启 AOF 并使用 RDB preamble,同时保留周期 RDB 备份
极度关注每次写入的落盘评估 AOF always,并接受明显性能成本

实践中常见组合是 AOF everysec 负责较新的恢复点,RDB 负责周期备份和灾难恢复。无论选择哪种模式,都应:

  • 定期把备份复制到独立故障域,并实际演练恢复。
  • 为 fork、写时复制和 AOF 重写预留内存与磁盘空间。
  • 监控最近一次 RDB/AOF 状态、AOF 重写、磁盘延迟和文件增长。
  • 配合复制与高可用降低单机故障影响,但不要把副本当成不可变备份。

持久化只能决定单实例可恢复到什么程度,不等于业务已经具备强一致、跨机房容灾或绝对不丢数据的能力。

评论

On this page