Redis 底层数据结构和持久化怎么理解:SDS、字典、跳表、RDB 与 AOF

2次阅读
没有评论

Redis 平时用起来很简单:StringHashListSetZSet。但如果只停留在命令层面,排查内存、持久化、复制和性能问题时,很容易只会调参数,不知道背后的代价。

这篇按一条主线整理:Redis 为什么不用普通 C 字符串,字典和跳表解决了什么问题,对象编码为什么会变化,RDB 与 AOF 分别适合怎样理解。

Redis 数据类型不是底层结构本身

Redis 对外暴露的是几类数据类型:

  • 字符串;
  • 列表;
  • 哈希;
  • 集合;
  • 有序集合。

但这些类型并不等于某一种固定底层结构。Redis 会根据数据规模、元素大小和配置,在不同编码之间切换,以兼顾内存和性能。

比如:

  • 字符串对象可能用整数编码、embstrraw
  • 列表在旧版本里可能用压缩列表或链表,新版本里还有 quicklist、listpack 等演进;
  • 哈希在元素少、内容短时可以用紧凑结构,变大后会转为哈希表;
  • 集合如果全是整数且数量小,可以用整数集合;
  • 有序集合常见底层思路是字典加跳表。

所以理解 Redis 时,最好把“对外数据类型”和“内部编码”分开看。

SDS:Redis 为什么不用普通 C 字符串

Redis 是 C 语言写的,但它没有直接把 C 字符串作为默认字符串实现,而是使用 SDS,也就是 Simple Dynamic String。

这样做主要解决几个问题。

第一,获取长度更快。C 字符串要从头扫到 \0,SDS 会记录长度,读取长度是常数复杂度。

第二,减少缓冲区溢出风险。字符串扩容时,SDS 能知道当前长度、剩余空间和需要扩展的大小,不依赖调用者手工保证空间足够。

第三,减少频繁重分配。SDS 会做空间预分配和惰性释放。字符串增长时多预留一点空间,字符串缩小时也不一定马上把空间还给系统。

第四,二进制安全。C 字符串依赖 \0 表示结尾,而 SDS 可以保存任意二进制内容。

这也是 Redis 字符串能既当文本用,又能当计数器、位图或二进制值用的基础。

字典:键空间和 Hash 的基础

Redis 的数据库键空间本质上依赖字典结构。你可以把它理解成一个哈希表:key 指向键对象,value 指向值对象。

字典内部会涉及哈希表、哈希节点、rehash 等机制。真正值得记的是 rehash 的代价和 Redis 的处理方式。

当哈希表负载因子过高时,需要扩容;负载因子过低时,可能需要收缩。直接一次性把旧表所有元素迁移到新表,会带来明显阻塞。Redis 使用渐进式 rehash:它会同时保留两个哈希表,然后在后续增删改查过程中逐步迁移。

这解释了一个现象:Redis 很快,但不是所有操作都没有额外成本。某些请求可能顺手承担了一点 rehash 迁移工作。不过这种成本被摊到了多个操作里,避免了单次大阻塞。

跳表:有序集合为什么能按分值查询

有序集合要同时支持两类能力:

  • 按成员快速查分值;
  • 按分值范围快速查排名和区间。

只用哈希表,按成员查分值很快,但不擅长有序范围;只用排序数组,更新成本又高。Redis 的典型方案是字典加跳表:字典负责成员到分值的映射,跳表负责有序访问。

跳表可以理解成多层链表。底层保存完整有序链路,上层提供更快的跳跃路径。它不像平衡树那样依赖复杂旋转,实现相对简单,范围查询和排名计算也比较自然。

有序集合里的“分值相同怎么排”也有规则:分值相同会继续按成员本身比较,保证顺序稳定。

紧凑结构:整数集合和压缩列表解决内存问题

Redis 很在意小对象的内存成本。很多业务里,一个 Hash 可能只有几个字段,一个 List 可能只有几个短元素。如果一开始就给它分配完整哈希表或链表结构,指针和对象头的开销会很大。

因此 Redis 会在小规模数据上使用更紧凑的编码。

整数集合适合保存少量整数集合元素。它有序、不重复,并且会根据元素范围升级编码。比如原来都是 16 位整数,后来插入一个更大的整数,就升级到更宽的编码。整数集合支持升级,但不降级,避免频繁来回转换。

压缩列表或后续类似紧凑结构,则把多个元素连续存放,减少指针开销。它适合元素少、内容短的场景。代价是更新时可能涉及连续内存移动,极端情况下还会带来连锁更新。

这就是 Redis 内存优化常见的权衡:小数据尽量紧凑,大数据再切换到更适合更新和查找的结构。

对象编码为什么会变化

Redis 对象里会记录类型、编码和指向底层数据结构的指针。

编码变化不是异常,而是正常策略。比如字符串刚开始是一个可表示为整数的值,可能用整数编码;短字符串可能用连续内存的 embstr;变长或被修改后,可能转成 raw

Hash、List、Set、ZSet 也类似。元素少、元素短时使用紧凑结构;超过阈值后转成哈希表、链表或跳表等结构。

这种设计带来的好处是:应用层命令保持一致,Redis 内部可以按数据规模选择更合适的结构。代价是排查问题时,不能只看数据类型,还要看对象编码和元素规模。

常用排查思路是:

  • 看 key 数量和单 key 大小;
  • 看对象编码是否符合预期;
  • 看是否存在大 Hash、大 List、大 ZSet;
  • 看是否有大量短小 key 造成对象头和指针开销。

过期键不是到点立即全部删除

Redis 里过期键删除常见三种思路:定时删除、惰性删除、定期删除。

定时删除最及时,但要为每个键维护定时任务,成本高。惰性删除是在访问 key 时才检查是否过期,对 CPU 友好,但如果一直没人访问,内存可能迟迟不释放。定期删除则是周期性抽样检查一批过期键。

Redis 实际采用惰性删除加定期删除的组合。这样既不会为每个 key 都维护昂贵定时器,也不会完全依赖访问触发。

这也解释了为什么设置了过期时间以后,内存不一定在那个毫秒点立刻下降。过期是逻辑时间,释放是具体策略。

RDB:适合快照恢复,不适合记录每次变更

RDB 保存的是某一时刻的数据快照。它的核心优点是文件紧凑、恢复速度相对快,适合备份和全量恢复。

常见触发方式有:

  • SAVE:同步保存,会阻塞主进程;
  • BGSAVE:fork 子进程后台保存,主进程继续处理请求;
  • 配置 save 900 1save 300 10 这类规则,满足条件后触发。

RDB 的弱点也很明显:两次快照之间如果 Redis 异常退出,期间写入可能丢失。它不是按每条命令记录变化,而是按时间点生成快照。

所以 RDB 更像“阶段性存档”,不是“完整操作日志”。

AOF:适合记录写命令,但需要重写

AOF 保存的是写命令。Redis 重启时,可以重新执行这些命令来恢复数据。

AOF 的关键配置是同步策略:

  • always:每次写都同步,最安全但最慢;
  • everysec:通常每秒同步一次,是常见折中;
  • no:交给操作系统决定,性能好但风险更高。

AOF 文件会随着写入越来越大,所以需要 AOF 重写。重写不是逐行分析旧 AOF,而是根据当前数据状态生成一份更短的新命令集合。比如一个 key 被改了很多次,最终只需要记录能恢复当前值的命令。

后台重写时,Redis 既要继续处理新写入,也要保证新旧 AOF 切换后一致,因此会使用额外缓冲记录重写期间的变更,最后再完成切换。

复制和持久化要一起看

Redis 复制里,旧版 SYNC 主要依赖全量同步和命令传播。断线重连时,如果只能重新全量同步,成本很高。

后来的 PSYNC 支持部分同步。它依赖几个关键点:

  • 主从复制偏移量;
  • 主服务器复制积压缓冲区;
  • 主服务器运行 ID。

如果从节点断开时间不长,缺失的数据还在复制积压缓冲区里,就可以补发缺失部分;如果缺得太多,就只能重新全量同步。

所以复制可靠性不只看有没有从库,也要看写入量、断线时间和复制 backlog 大小。

小结

Redis 的快,来自一系列工程取舍:SDS 让字符串更安全高效,字典支撑键空间和哈希访问,跳表支撑有序集合范围查询,紧凑编码降低小对象成本,RDB 和 AOF 在恢复速度与数据完整性之间取舍。

日常使用时,不必把每个结构源码都背下来,但要建立几个判断:数据类型不等于底层结构,编码会随规模变化,过期键不会总是立即物理删除,RDB 是快照,AOF 是命令日志,复制能否部分同步取决于偏移量和积压缓冲区。

有了这些基础,再去看内存占用、持久化策略、主从复制和慢操作,就不会只停留在调参数层面。

正文完
 0
bdspAdmin
版权声明:本站原创文章,由 bdspAdmin 于2026-06-15发表,共计3223字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)