Redis 平时用起来很简单:String、Hash、List、Set、ZSet。但如果只停留在命令层面,排查内存、持久化、复制和性能问题时,很容易只会调参数,不知道背后的代价。
这篇按一条主线整理:Redis 为什么不用普通 C 字符串,字典和跳表解决了什么问题,对象编码为什么会变化,RDB 与 AOF 分别适合怎样理解。
Redis 数据类型不是底层结构本身
Redis 对外暴露的是几类数据类型:
- 字符串;
- 列表;
- 哈希;
- 集合;
- 有序集合。
但这些类型并不等于某一种固定底层结构。Redis 会根据数据规模、元素大小和配置,在不同编码之间切换,以兼顾内存和性能。
比如:
- 字符串对象可能用整数编码、
embstr或raw; - 列表在旧版本里可能用压缩列表或链表,新版本里还有 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 1、save 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 是命令日志,复制能否部分同步取决于偏移量和积压缓冲区。
有了这些基础,再去看内存占用、持久化策略、主从复制和慢操作,就不会只停留在调参数层面。




