
作为互联网技术栈中最主流的内存数据库Redis 凭借极致的性能、丰富的数据类型和灵活的持久化能力成为缓存、限流、排行榜、分布式锁等场景的首选方案。很多开发者对 Redis 的认知停留在 “单线程、速度快” 的表层却对其背后的设计思想一知半解。本文将从底层数据结构、IO 网络模型、持久化机制三大核心维度系统拆解 Redis 的高性能与高可靠设计带你吃透其核心实现原理。一、底层数据结构五种基本类型的编码实现Redis 对外提供了 String、List、Hash、Set、Sorted Set 五种常用数据类型但每种类型并非只有一种底层实现。Redis 会根据元素数量、元素大小自动切换底层编码在空间占用和访问性能之间做动态权衡这也是它兼顾内存效率与运行速度的关键。1. 各数据类型的底层编码String简单动态字符串SDSString 是 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/pollepoll 没有连接数上限且性能不会随连接数增长而显著下降完美支撑了 Redis 的高并发网络处理。正是 “单线程命令执行 IO 多路复用网络模型” 的组合让 Redis 在保持实现简洁的同时达到了万级甚至十万级的 QPS。三、持久化机制内存数据的可靠性保障作为内存数据库Redis 的数据全部存放在内存中一旦进程退出或服务器宕机数据就会全部丢失。为了解决内存数据的可靠性问题Redis 提供了两种经典持久化方案AOF 日志和 RDB 快照以及后续推出的混合持久化模式。1. AOF 日志写后日志的利与弊AOFAppend 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 快照内存数据的全量镜像RDBRedis DataBase是另一种持久化方式本质是内存快照将某一时刻 Redis 内存中的全部数据以二进制格式写入磁盘文件。宕机恢复时直接将 RDB 文件加载到内存即可。相比 AOFRDB 最大的优势是恢复速度极快因为是二进制数据直接加载不需要逐条执行命令非常适合灾难恢复、全量数据备份场景。两种快照命令阻塞与非阻塞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、混合模式多种方案让用户可以根据业务场景在性能、数据安全、恢复速度之间做灵活选择。