Redis有5种基本数据类型:string 字符串、hash 哈希、list 列表、set 集合、zset 有序集合。
String 字符串
字符串对象的编码有3种:INT、RAW和EMBSTR。
- 当字符串对象保存的是整数值,且long类型能够表示,那么使用INT编码。字符串对象会将整数值保存在字符串对象结构的ptr属性。
- 如果字符串对象保存的是一个字符串值,且字符串值长度大于44字节,那么使用RAW编码。字符串对象会使用SDS来保存这个字符串值。
- 如果字符串对象保存的是一个字符串值,且字符串值长度小于等于44字节,那么使用EMBSTR编码。这种编码也是使用SDS来保存字符串值,但是不同之处在于redisObject和SDS存放在一块连续的空间。
什么是EMBSTR编码?
EMBSTR编码是专门用于保存短字符串的一种优化编码方式。
这种编码跟RAW一样都是使用SDS来保存字符串值,但RAW编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而EMBSTR编码则调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构。
使用EMBSTR编码的字符串对象来保存短字符串值有如下好处:
- EMBSTR编码将创建字符串对象所需的内存分配次数从RAW编码的两次减少为一次。
- EMBSTR编码只需要调用一次内存释放函数就可以释放字符串对象。而RAW编码需要调用两次。
- EMBSTR编码的字符串对象能够减少内存碎片的产生。EMSTR能够存储的字符串长度最大为44字节,加上SDS的1字节的len、1字节的alloc、1字节的flags,再加上字符串最后加上的空字符1字节,那么SDS的最大占用字节为48。然后redisObject占用16字节,那么EMBSTR编码的字符串对象最大为64字节。Redis使用的jemalloc内存分配器,以2的n次方来分配内存,因此可以减少内存碎片的产生。
编码的转换
INT编码的字符串对象和EMBSTR编码的字符串对象在条件满足的情况,会被转换成RAW编码的字符串对象。
对于INT编码的字符串对象来说,如果我们向对象执行了一些命令,使得这个对象保存到的不再是整数值,而是一个字符串值,那么字符串对象的编码将从int变成raw。
另外,因为Redis没有为EMBSTR编码的字符串对象编写任何相应的修改程序(只有int编码的字符串对象和raw编码的字符串对象有这些程序),所以EMBSTR编码的字符串对象实际上是只读的。当我们对EMBSTR编码的字符串对象执行任何修改命令时,程序先将对象的编码从EMBSTR转换成RAW,然后再执行修改命令。因为这个原因,EMBSTR编码的字符串对象在执行修改命令之后,总会变成一个RAW编码的字符串对象。
list 列表
在Redis 3.2版本之前,Redis采用LinkedList或ZipList来实现List。当列表对象保存的所有字符串元素的长度都小于64字节且列表对象保存的元素数量小于512个,则使用ziplist编码,否则使用linkedlist编码。
在Redis 3.2版本之后,Redis则采用QuickList来实现List。
hash 哈希
哈希对象的编码可以是ziplist或者hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入哈希对象时,程序会先讲保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。因此:
- 保存了同一键值对的两个节点总是紧挨到一起,保存键的节点在前,保存值的节点在后。
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象的键值对会被放到压缩列表的表尾方向。
hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:
- 字典中的每个键都是一个字符串对象,对象中保存了键值对的键。
- 字典中的每个值都是一个字符串对象,对象中保存了键值对的值。
编码转换
当哈希对象满足以下两个条件,哈希对象使用ziplist编码,否则使用hashtable编码。
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节。
- 哈希对象保存的键值对数量小于512个。
set 集合
集合对象的编码可以是intset或hashtable。
intset编码的集合对象使用整数集合IntSet作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。
hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
编码的转换
当集合对象同时满足以下条件时,对象使用intset编码,否则使用hashtable编码。
- 集合对象保存的所有元素都是整数值。
- 集合对象保存的元素数量不超过512个。
zset 有序集合
有序集合的编码可以是ziplist或skiplist。
ziplist编码的有序集合使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。
压缩列表中的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置到靠近表尾的方向。
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和跳跃表:
c
typedef struct zset {
zskiplist *zsl;
dict *dict;
}
zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围性操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的。
zet结构的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的。
有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数。值得一提的是,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或分值,也不会因此而浪费额外的内存。
为什么有序集合需要同时使用跳跃表和字典来实现?
在理论上,有序集合可以单独使用字典或者跳跃表来实现,但是相比于同时使用跳跃表和字典,性能会有所降低。
例如,如果我们只使用字典来实现集合,那么虽然查找成员的分值的复杂度为O(1)。但是,字典以无序的方式来保存集合元素,所以每次在执行范围性操作(ZRANK或ZRANGE),程序都要对字典保存的所有元素进行排序。完成这种排序需要至少O(NLogN)时间复杂度,以及额外的O(N)内存空间。
若我们只使用跳跃表来实现有序集合,那么跳跃表执行范围性操作的所有优点都会被保留。但因为没有了字典,所以根据成员查找分值这一操作的时间复杂度将从O(1)上升到O(logN)。
因此,为了让有序集合的查找和范围型操作都尽可能快地执行,Redis选择了同时使用字典和跳跃表两种数据结构来实现有序集合。
编码的转换
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码,否则使用skiplist编码。
- 有序集合保存的元素数量小于128个。
- 有序集合保存的所有元素成员的长度都小于64字节。