作为互联网技术栈中最主流的内存数据库,Redis 凭借极致的性能、丰富的数据类型和灵活的持久化能力,成为缓存、限流、排行榜、分布式锁等场景的首选方案。很多开发者对 Redis 的认知停留在 "单线程、速度快" 的表层,却对其背后的设计思想一知半解。本文将从底层数据结构、IO 网络模型、持久化机制三大核心维度,系统拆解 Redis 的高性能与高可靠设计,带你吃透其核心实现原理。
一、底层数据结构:五种基本类型的编码实现
Redis 对外提供了 String、List、Hash、Set、Sorted Set 五种常用数据类型,但每种类型并非只有一种底层实现。Redis 会根据元素数量、元素大小自动切换底层编码,在空间占用和访问性能之间做动态权衡,这也是它兼顾内存效率与运行速度的关键。
1. 各数据类型的底层编码
-
String:简单动态字符串(SDS) String 是 Redis 最基础的数据类型,底层并未直接使用 C 语言原生字符串,而是自主实现了简单动态字符串(Simple Dynamic String, SDS) 。相比 C 字符串,SDS 具备三大优势:二进制安全(不以
\0作为结束标志,可存储二进制数据)、O (1) 获取字符串长度、通过空间预分配与惰性释放减少内存重分配次数。 -
List:双向链表 + 压缩列表 List 类型采用双编码设计:当列表元素数量少、单个元素体积较小时,使用压缩列表(ziplist) 存储,利用连续内存大幅节省空间;当元素数量或大小超过配置阈值时,自动转换为双向链表(linkedlist),支持 O (1) 的首尾插入与删除。
-
Hash:压缩列表 + 哈希表 Hash 类型同样遵循 "小数据用压缩列表、大数据用哈希表" 的策略。小数据量下将键值对连续存储在压缩列表中,内存利用率极高;当元素数量或单元素大小超标时,切换为哈希表(hashtable),实现 O (1) 的键值查找。
-
Set:整数集合 + 哈希表 Set 类型底层有两种实现:当集合中所有元素均为整数且数量较少时,使用**整数集合(intset)**连续存储;当元素不满足整数条件或数量超过阈值时,切换为哈希表,保证 O (1) 的查找、插入、删除性能。
-
Sorted Set:压缩列表 + 跳表 Sorted Set(有序集合)是 Redis 的特色类型,小数据量下使用压缩列表;数据量较大时,采用跳表(skiplist)+ 哈希表的组合结构 ------ 哈希表保证 O (1) 的成员分数查询,跳表支持 O (logN) 的范围查找与排序操作,二者互补实现高性能有序集合。
2. 哈希表核心:哈希冲突与渐进式 rehash
哈希表是 Redis 多种数据类型的底层基础,其性能直接决定了 Redis 的访问效率,其中最核心的两个设计就是哈希冲突解决与渐进式扩容。
-
哈希冲突:链地址法 Redis 的哈希表采用**链地址法(拉链法)**解决哈希冲突:当多个 key 的哈希值映射到同一个哈希桶时,会在该桶上挂载一个链表,依次存放冲突的键值对。查询时先通过哈希值定位哈希桶,再遍历链表匹配具体 key。
-
渐进式 rehash:避免阻塞的扩容方案 当哈希表元素持续增多,冲突链表会越来越长,查询性能逐步下降,此时需要对哈希表进行扩容(rehash)。如果一次性将所有哈希桶全部迁移,会导致主线程长时间阻塞,这对追求极致性能的 Redis 来说是不可接受的。
因此 Redis 设计了渐进式 rehash机制,核心思路是 "分而治之,逐步迁移",具体原理如下:
- 哈希表内部维护两个哈希表结构
ht[0]和ht[1],正常服务时仅使用ht[0]; - 触发扩容条件时,为
ht[1]分配扩容后的空间,容量通常为ht[0]的 2 倍; - rehash 期间,每次对哈希表执行增删改查操作时,除完成正常业务操作外,还会顺带将
ht[0]中对应索引的哈希桶迁移到ht[1]; - 同时 Redis 后台通过定时任务,主动分批迁移剩余的哈希桶,避免长时间无操作导致扩容停滞;
- 当
ht[0]的所有数据全部迁移完成后,释放ht[0]空间,将ht[1]设置为新的ht[0],rehash 完成。
这种设计将一次性的大开销分摊到多次操作中,既完成了哈希表扩容,又避免了主线程长时间阻塞,是 Redis 高性能设计的典型体现。
3. 压缩列表 vs 跳表:时间复杂度的权衡
- 压缩列表本质是一块连续的内存空间,元素紧密排列,无多余指针,内存利用率极高,但插入、删除、查找都需要遍历,时间复杂度为O(N),因此仅适合小数据量场景。
- 跳表是一种多层有序链表结构,通过维护多级索引,将查找、插入、删除的时间复杂度降到O(logN),同时相比平衡树实现更简单,支持高效的范围查询,因此成为大数据量下有序集合的底层实现。
二、IO 模型:单线程设计与多路复用机制
很多人对 Redis "单线程" 的认知存在误区:Redis 的单线程,指的是命令的执行与内存操作由单线程完成,而非整个 Redis 服务只有一个线程 ------ 后台的 AOF 刷盘、文件关闭、数据持久化等操作,由专门的后台线程或子进程完成。
1. 为什么选择单线程设计?
Redis 坚持单线程命令执行模型,核心有三点考量:
- 纯内存操作无 CPU 瓶颈:Redis 所有数据都存放在内存中,绝大多数操作的瓶颈不在 CPU 计算,而在网络 IO,单线程足以处理内存级的读写操作;
- 规避多线程额外开销:多线程会带来上下文切换、锁竞争、线程创建销毁的额外成本,在内存操作场景下反而可能降低整体性能;
- 实现简单可控:单线程无需考虑并发安全问题,代码实现更简洁,不易出现死锁、竞态条件等问题,可维护性与稳定性更高。
2. IO 多路复用:单线程处理海量连接的核心
单线程要同时处理成百上千个客户端连接,靠的就是IO 多路复用机制。
简单来说,IO 多路复用允许单个线程同时监听多个网络连接(socket),当某个连接准备好可读或可写时,内核会主动通知线程,线程再去处理对应的 IO 操作。整个过程中,线程不会阻塞在某个单一连接上,而是可以高效地轮询所有就绪的连接。
在 Linux 系统下,Redis 默认使用 epoll 作为 IO 多路复用的实现。相比传统的 select/poll,epoll 没有连接数上限,且性能不会随连接数增长而显著下降,完美支撑了 Redis 的高并发网络处理。
正是 "单线程命令执行 + IO 多路复用网络模型" 的组合,让 Redis 在保持实现简洁的同时,达到了万级甚至十万级的 QPS。
三、持久化机制:内存数据的可靠性保障
作为内存数据库,Redis 的数据全部存放在内存中,一旦进程退出或服务器宕机,数据就会全部丢失。为了解决内存数据的可靠性问题,Redis 提供了两种经典持久化方案:AOF 日志和 RDB 快照,以及后续推出的混合持久化模式。
1. AOF 日志:写后日志的利与弊
AOF(Append Only File)的核心思路是:记录 Redis 执行的每一条写命令,宕机恢复时重新执行所有命令,即可完整恢复数据。
执行机制与优缺点
Redis 采用写后日志:先执行命令、将数据写入内存,再将命令写入 AOF 日志。 这种设计带来两个直接优点:
- 不会记录错误命令,只有执行成功的命令才会写入日志;
- 日志写入在命令执行之后,不会阻塞当前命令的执行。
同时 AOF 也存在明显缺点:
- 数据丢失风险:如果命令执行完、日志还没刷到磁盘就发生宕机,这条命令就会丢失,丢失量取决于刷盘策略;
- 潜在阻塞风险:虽然不阻塞当前命令,但日志刷盘操作可能影响后续命令的处理。
三种写回策略
Redis 提供了三种 AOF 刷盘策略,供用户在性能和数据安全之间权衡:
always:每执行一条写命令,就同步将日志刷到磁盘。数据安全性最高,几乎不丢数据,但性能损耗最大,会严重降低 Redis 吞吐量;everysec:每秒刷盘一次,是 Redis 的默认配置。在性能和安全性之间做了平衡,最多丢失 1 秒的数据,是绝大多数场景的最优选择;no:不主动刷盘,完全由操作系统决定刷盘时机。性能最好,但数据丢失风险最高,宕机时可能丢失较多数据。
AOF 重写机制:解决文件膨胀问题
随着运行时间增长,AOF 文件会越来越大,不仅占用磁盘空间,还会导致宕机恢复时间变长。为此 Redis 设计了AOF 重写机制,对 AOF 文件进行压缩。
重写的原理很简单:读取当前数据库中所有的键值对,为每一个键值对生成一条对应的写命令,写入新的 AOF 文件,替代原来对同一个 key 的多次操作命令,从而大幅减小文件体积。
很多人关心:AOF 重写会阻塞主线程吗? 答案是不会。Redis 通过bgrewriteaof命令,由主线程 fork 出一个后台子进程来执行重写操作,主线程可以继续处理客户端命令。
完整的 AOF 重写流程如下:
- 触发重写:可通过手动执行
bgrewriteaof命令触发,也可通过配置文件设置阈值(如文件增长比例)自动触发; - 主线程 fork 子进程:fork 瞬间会短暂阻塞主线程,fork 完成后主线程恢复正常服务;
- 子进程重写:子进程基于当前内存中的数据,遍历所有键值对,生成新的 AOF 文件;
- 增量命令缓存:重写期间,主线程收到的新写命令,一方面照常写入旧的 AOF 缓冲区保证原有 AOF 正常工作,另一方面写入AOF 重写缓冲区,保证重写期间的新数据不会丢失;
- 追加增量数据:子进程完成新 AOF 文件写入后,通知主线程;主线程将重写缓冲区中的增量命令追加到新 AOF 文件末尾;
- 原子替换:主线程用新的 AOF 文件原子替换旧的 AOF 文件,重写完成。
2. RDB 快照:内存数据的全量镜像
RDB(Redis DataBase)是另一种持久化方式,本质是内存快照:将某一时刻 Redis 内存中的全部数据,以二进制格式写入磁盘文件。宕机恢复时,直接将 RDB 文件加载到内存即可。
相比 AOF,RDB 最大的优势是恢复速度极快,因为是二进制数据直接加载,不需要逐条执行命令,非常适合灾难恢复、全量数据备份场景。
两种快照命令:阻塞与非阻塞
save:同步执行快照,整个过程会阻塞主线程,期间 Redis 无法处理任何命令,生产环境几乎不会使用;bgsave:后台执行快照,主线程 fork 出子进程,由子进程完成 RDB 文件写入,主线程仅在 fork 瞬间短暂阻塞,之后可以正常处理命令,是 Redis 的默认快照方式。
写时复制(COW):快照期间数据可修改
很多人会有疑问:快照过程中,如果主线程修改了数据,快照的一致性怎么保证?答案是**写时复制(Copy-On-Write, COW)**技术。
bgsave 的子进程通过 fork 创建,它会共享父进程(主线程)的所有内存页。当主线程要修改某一个内存页的数据时,操作系统会复制该页的副本,主线程修改副本,而子进程仍然使用原来的内存页生成快照。 这样既保证了快照数据是某个时间点的完整一致版本,又允许主线程在快照期间正常修改数据,两全其美。
全量快照的局限与增量快照
全量快照虽然恢复快,但也有明显问题:
- 频繁执行 bgsave 会带来持续的磁盘 IO 压力;
- 内存越大,fork 子进程的阻塞时间越长,大内存实例下 fork 开销不可忽视;
- 两次快照之间宕机,会丢失两次快照间隔内的所有数据。
至于增量快照,Redis 原生并没有提供官方实现。增量快照的思路是只记录两次全量快照之间修改的数据,但实现复杂,需要维护修改标记,还会引入额外的内存开销,因此 Redis 并未采用,而是通过混合持久化来解决这个问题。
3. 混合持久化:兼顾速度与安全
Redis 4.0 之后推出了混合持久化模式,结合了 RDB 和 AOF 各自的优点,也是目前主流推荐的持久化方案。
混合持久化的工作方式是:在执行 AOF 重写时,不再是纯命令重写,而是先将当前内存数据做 RDB 快照,把 RDB 内容写入新 AOF 文件的开头,再把重写期间的增量写命令以 AOF 格式追加在文件末尾。
这种设计带来的好处十分明显:
- 恢复速度快:先加载 RDB 部分,相当于加载快照,速度远快于逐条执行 AOF 命令;
- 数据更可靠:增量部分用 AOF 记录,丢失的数据更少;
- 文件体积更小:RDB 的二进制格式比纯命令 AOF 更紧凑。
当然它也有缺点:AOF 文件不再是纯文本格式,可读性变差,且不兼容 Redis 4.0 之前的版本。
四、总结
Redis 的高性能与高可靠,不是靠单一的 "银弹" 实现的,而是层层设计叠加的结果:
- 底层数据结构上,针对不同场景设计多种编码,通过渐进式 rehash、跳表等机制平衡空间占用与访问效率;
- 网络模型上,用单线程规避并发开销,用 IO 多路复用突破单线程的网络瓶颈;
- 持久化上,提供 AOF、RDB、混合模式多种方案,让用户可以根据业务场景在性能、数据安全、恢复速度之间做灵活选择。