书接上文,我们今天继续结合源码来谈谈Redis是怎么在内存中存储的
第二层——Redis对象的各种编码方式
上一节我们讲到了RedisDb里存储Reids对象最重要的数据结构就是一个dict字典,在这里我们先不考虑重哈希等次要问题,对于一个键值对,这个键值对的key当然只能是字符串了,而这里的value是用一个名为redisObject的数据结构存储的:
typedef struct redisObject {
unsigned type:4; // 数据类型(如 REDIS_STRING, REDIS_LIST, REDIS_HASH)
unsigned encoding:4; // 内部编码(实现该数据类型的具体底层数据结构)
unsigned lru:24; // 内存淘汰策略相关信息(LRU时间或LFU计数)
int refcount; // 引用计数(用于内存回收)
void *ptr; // 指向实际数据的指针(这里指向SDS)
} robj;
这里的几个字段分别指的是填入的value数据类型,内部编码(不是UTF-8这种二进制编码过程,而是定义这种数据是怎么被组织存储在内存的),淘汰机制,实际数据的指针。 而这里的最重要的也是Redis高性能的一大原因就是它对不同的数据类型,不同的数据数量长度都采用了不同的内部编码。
我们一个一个来讲:
1、String
对于String类型,当存储的是整型时直接采用INT编码,直接以long学生存储;
对于其他字符串则是采用了:EMBSTR和RAW,具体来说:当字符串长度小于等于特定阈值是采用EMBSTR,大于时采用RAW。这两种编码都是由redisObject指向SDS实现的。
EMBSTR中redisObject与SDS是连续存储的:
RAW则是分开存储的:
EMBSTR实现简单,适用于数据量小;RAW则更加灵活适合存储大数据以及写操作(是的当进行append且需要扩容时EMBSTR固定分配空间显然就不合适了)
其中SDS(简单动态字符串)结构:
struct sdshdr {
int len; // 记录字符串已使用的长度(二进制数据的真实长度)
int free; // 记录未使用的空间
char buf[]; // 字节数组,用于保存实际数据
};
SDS是最简单的编码方式,提供了字符串长度,使得存储的String字符串不需要以“/0”作为结束标志,避免了不必要的结束标志冲突;而且预留了一定的多余的字节数组空间,防止每次扩容都需要再次进行全复制操作。
2、List
3.2版之前使用的是ZIPLIST和LINKEDLIST,看名字也能看出来一种是压缩链表类似于ArrayList是连续存储的,另一种是有链接的链表类似于LinkedList。
当链表长度小于512,每个元素长度不超过64字节时使用ZIPLIST,反之使用LINKEDLIST。ZIPLIST更紧凑在数据两小时使用可以节约内存,LINKEDLIST更加灵活,虽然多占用了内存但是提升了更新效率。
而3.2之后出现了QUICKLIST,结合了ZIPLIST和LINKEDLIST,平衡了灵活性和节约内存
如下图:是不是有点像MySQL里的B+树呢,每一个节点不是存储某个数据,而是存储的一个ZIPLIST。提高了更新效率同时节约了内存。
3、Set
Set是一种无序的集合,但是在Redis有两种编码方式,INTSET和HASHTABLE。
当Set集合里全是整形时,自然而然就会采用INSET编码,如图:
类似于一个强化版的数组(所以这个底层其实是有序的)多了数组长度的字段。
当集合中不只有整数时,就会自动转化为HASHTABLE编码,就是我们常知的HashMap的value为null的做法,如图:
RedisObject的数据指针指向的是一个value为空的dict。
4、Hash
知道了前面几种编码,HashMap的编码就很简单了,当HashMap里的元素大于512个或hashmap的键值大于64字节时采用HashTable编码,对就是上面Set的HashTable(alue不再是null)。反之,就采用一种节约空间的编码存储(毕竟Redis是很节约内存的)ziplist编码,如图:
对于hash的k和v看作一个entry整体放入压缩链表。
5、ZSet
ZSet就是在Set的基础上变为有顺序的集合(这个顺序不再是每个元素的大小关系那么简单,而是依照ZSet特有的score字段排序)简单来说,ZSet就是Hash将value字段变为特殊的score字段,且按照这个score字段排序。
当每个元素大小小于64字节,ZSet里元素数量小于128采用ziplist编码,与Hash的ziplist不同的是数组内元素的排序需要严格按照其后的score字段由小到大排序,如图:
当不满足ziplist编码条件就会采用SKIPLIST+HASHTABLE(跳表+HashTable),前面讲过ZSet就是有序的Set,在Set单一的HashTable编码的基础上,额外添加了跳表数据结构,来对score进行排序。我们都知道跳表就是普通全元素链表+多级排序链表,根据添加的多级排序链表来快速查询到指定大小元素,如图:
按分数增加,外再加一个hashtable指向此跳表,做到映射到分数的对象。
所以这种编码方式加快了查询特定分数的时间复杂度O(1)。
总结
通过深入分析Redis的五种主要数据类型的编码方式,我们可以清晰地看到Redis在内存存储设计上的精妙之处:
1. 灵活性与效率的平衡
Redis针对不同数据类型、不同数据规模采用了最优的编码策略。比如String类型根据数据长度选择EMBSTR/RAW编码,List类型从早期的ZIPLIST/LINKEDLIST演进到QUICKLIST,都体现了在内存使用效率和操作性能之间的智能权衡。
2. 数据驱动的编码转换
Redis的编码策略不是一成不变的,而是根据实际数据特征动态调整。当Set集合从纯整数变为混合类型时,会自动从INTSET转换为HASHTABLE;当Hash或ZSet的元素数量或大小超过阈值时,也会触发编码转换,这种自适应机制确保了始终使用最适合当前数据特征的存储方式。
3. 复合数据结构的巧妙运用
特别是ZSet的SKIPLIST+HASHTABLE组合设计令人印象深刻,既通过跳表维护了有序性,又通过哈希表实现了O(1)的查询效率,完美解决了有序集合的复杂需求。
4. 渐进式优化思想
从List编码的演进可以看出Redis团队的持续优化思路:ZIPLIST节约内存但更新效率低,LINKEDLIST更新快但内存占用大,而QUICKLIST通过分层设计实现了两者的优势结合。
这些精心的编码设计正是Redis能够实现高性能、低内存消耗的关键所在。作为开发者,理解这些底层机制不仅有助于我们更好地使用Redis,更能从中学习到优秀的数据结构设计思想,这些思想同样可以应用于我们日常的系统设计和开发工作中。
下一篇预告:我们将继续深入Redis的第三层——持久化机制,探讨Redis如何保证数据的安全性。