Redis中有那些对象

redis中有简单动态字符串(SDS),双端链表,字典,压缩列表,整数集合等数据结构,但是redis并没有直接采用这些数据结构来构成键值对数据库,而是基于这些数据结构创建了一个对象系统。包含了字符串对象列表对象哈希对象集合对象有序集合对象。而且redis对象系统还实现了基于引用计数器的内存回收机制,并且通过引用计数技术实现了对象共享。

redis中每个对象都是由一个redisObject结构表示,该结构中和保存数据有关的三个属性如下:

1
2
3
4
5
6
7
8
9
10
typedef struct redisObject {
// 类型
unsigned type;

// 编码
unsigned encoding;

// 指向底层实现数据结构的指针
void *ptr;
}

type

这个属性记录了对象的类型,该类型可以是下表中的一个。

image-20230322183207835

每一个数据的键,总是一个字符串对象,而值可以是上表中的任意一个。

在redis中,使用TYPE命令可以显示一个键对应值的type

encoding

记录了对象所使用的编码。即记录该对象使用什么数据结构作为底层的实现。可以是下表中的任意一个。

image-20230322183455762

每种类型的对象至少使用了两种不同的编码,如下表所示

image-20230322183620698

在redis中,使用OBJECT ENCODING命令可以查看一个键对应值对象的编码。

通过encoding属性设置编码,而不是固定一个对象的编码,为redis带来了极大的灵活性。因为redis可以根据不同场景选择不同的编码,以此改变底层数据结构。

字符串对象

如果字符串对象存储的是整数,并且这个整数可以用long表示,那么整数值将保存在字符串对象结构的ptr属性里面。void* 转换为long。并将字符串编码设置为int。

如果字符串对象保存的是一个字符串值,并且该值长度大于32字节,那么就是用简单动态字符串保存,如果小于32,则使用embstr编码保存。

embstr

embstar专门用来保存短字符串的一种优化编码。embstr和raw都是使用redisObject和sdshdr结构来表示字符串,但是raw需要两次内存分配来创建,而embstr只需要一次内存分配,并且是连续的地址。释放空间也一样,分别是1次和2次。

long double类型表示的浮点数也是作为字符串来保存的。

编码的转换

int类型的编码和embstr在适当条件下会被转化为raw编码。而且,redis没有为embstr编写任何修改程序,我们对次做的任何修改都会使其转化为raw编码。

列表对象

列表对象的编码可以是ziplist(压缩列表)或者linkedlist(双端链表)。

当列表对象满足所保存的所有字符串元素都小于64字节,且数量小于512个时,采用ziplist编码,否则采用linkedlist编码。

哈希对象

哈希对象的编码可以是ziplist或者hashtable。

ziplist编码的哈希对象,每当有新的值加入,程序会先将保存了键的压缩列表节点加入压缩列表的表尾,然后是将保存值的节点加入表尾。所以说,保存了键和值的节点总是挨在一起,键在前,值在后。而且先添加的键值对在表头,后添加的在表尾。

具体结构如下图:

image-20230322192610777

hashtable编码的哈希对象,使用字典作为底层实现。哈希对象每一个键值对都使用一个字典键值对来保存。

字典的键是一个字符串对象,保存了键值对的键。字典的每个值也是一个字符串对象,保存了键值对的值。

image-20230322192653785

当哈希对象的所有键值对的键以及值的长度都小于64且键值对数量小于512时,使用ziplist编码,否则使用hashtable编码。

集合对象

集合对象的编码可以是intset或者hashtable。

intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里。

具体结构如下:

image-20230322194532294

hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为null。hashtable编码如果要取值,则只用取出来键即可。

执行完下面语句的结构如下图

1
SAD Dfruits "apple" "banana" "cherry"

image-20230322194543206

当集合对象保存的都是整数,且数量不超过512个时,采用intset编码,否则采用hashtable编码。

有序集合对象

有序集合编码可以是ziplist或者skiplist。

ziplist编码的有序集合对象使用压缩列表作为底层,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(number),第二个元素保存元素的分值(score)。

压缩列表内元素按照分值大小从小到大排序,分值较小的元素被放置在靠近表头的方向,较大的放在表尾。

skiplist编码的有序集合对象使用zset结构作为底层实现。一个zset包含了一个字典和一个跳跃表,具体结构如下:

1
2
3
4
5
6
7
typedef struct zset {
// 跳跃表
zskilplist *zsl;

// 字典
dict *dict;
} zset;

zset结构中的zsl跳跃表按分值从小到大保存了所有的集合元素,跳跃表的每个节点保存了一个集合元素:跳跃表节点的object属性保存了元素成员,而跳跃表节点的score保存了分值。通过跳跃表可以对集合实现范围操作。

zset中的字典为有序集合创建了一个从成员到分值的映射,字典中的每一个键值对都保存了一个集合元素,键是元素成员,值的成员对应的分值(score)。

zset会同时使用跳跃表和字典保存集合元素,但这两种结构会通过指针共享相同元素的成员和分值,所以不会浪费内存。

为什么同时使用跳跃表和字典?

如果单纯使用字典,能保留在o(1)的时间复杂度内查找到某成员,但是字典是无序的,对于范围查找则效率不高,而跳跃表因为有序所欲进行范围查找很快。如果单纯使用跳跃表,那么就无法完成在o(1)的时间复杂度内查找到某成员。

一个具体的例子:

image-20230322202217608

image-20230322202233639

当有序集合元素数量小于128个,同时所有元素成员长度小于64字节时,使用ziplist编码,否则使用skiplist编码。

参考

《Redis设计与实现》