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 yesio-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 的就绪状态:
- Redis 把监听 socket 和客户端 socket 注册到事件多路复用器。
- 事件循环等待可读、可写或连接到达等就绪事件,而不是阻塞在某一个客户端上。
- 内核返回已经就绪的文件描述符后,Redis 调用对应的 accept、读请求、写响应等处理器。
- 主线程解析并执行可以处理的命令,再进入下一轮事件循环。
Redis 会按操作系统选择 epoll、kqueue、select 等实现。它采用的是 Reactor 风格:多路复用器负责通知“哪些连接已就绪”,事件处理器负责真正读写和处理。
I/O 多路复用不等于多个请求在 CPU 上并行执行。Redis 6.0 以前,多个连接仍由一个主线程依次处理;Redis 6.0 的 I/O 线程只并行化网络阶段,命令执行阶段仍保持串行。
Redis 事务能保证原子性吗?
回答重点
需要先明确“原子性”的含义。MULTI、EXEC 提供的是命令组连续执行、不被其他客户端命令插入的隔离保证,但不提供关系型数据库式的失败回滚。
事务流程是:
MULTI开启事务。- 后续命令进入队列,只返回
QUEUED。 EXEC按入队顺序执行全部命令。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 0Lua 的原子性同样不代表失败回滚:
- 脚本在执行若干写命令后发生运行时错误,之前的修改仍然保留。
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 秒数据 |
no | Redis 不主动 fsync,交给操作系统 | 性能较好,但数据丢失窗口由操作系统刷盘策略决定 |
appendonly yes
appendfsync everysecAOF 一般比等价 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 重写、磁盘延迟和文件增长。
- 配合复制与高可用降低单机故障影响,但不要把副本当成不可变备份。
持久化只能决定单实例可恢复到什么程度,不等于业务已经具备强一致、跨机房容灾或绝对不丢数据的能力。
评论