前一篇已经说过Redis的基本数据类型有五种:string、list、set、zset、hash;而这五种数据类型的底层实现又依赖于上一篇介绍过的六种基本数据结构。本篇就简单介绍下,五种基本数据类型(对象)是如何和基本数据结构相关联的。
首先,Redis的基本数据类型(对象)的结构体如下:
typedef struct redisObject {
unsigned type:4; // 对象类型
unsigned encoding:4; // 编码类型
unsigned lru:REDIS_LRU_BITS; /* lru time(relative to server.lruclock)对象上次被访问时间 */
int refcount; // 引用计数
void *ptr; // 指向底层实现的数据结构的指针
} robj;
redis是基于内存的键值对,其中键对象始终是字符串对象,值对象就是我们使用的五种数据类型。
type属性有五个取值,分别对应Redis的五种数据类型;使用type命令可以查看值对象的数据类型;
type常量 | 对象名称 |
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
encoding属性对应八种数据结构;使用OBJECT ENCODING命令可以查看值对象的编码;
encoding常量 | 编码常量对应的数据结构 |
REDIS_ENCODING_INT | long型数据结构 |
REDIS_ENCODING_EMBSTR | embstr编码的简答动态字符串 |
REDIS_ENCODING_RAW | 简单动态字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 双向链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典 |
Redis规定了上述五种数据结构和八种数据类型之间某些可以组合,进而出现下表,即不同数据类型编码的对象:
不同类型&编码的对象
序号 | 类型 | 编码 | 对象 |
1 | REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
2 | REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr编码的简单动态字符串实现的字符串对象 |
3 | REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 |
4 | REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象 |
5 | REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双向链表实现的列表对象 |
6 | REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
7 | REDIS_HASH | REDIS_ENCODING_HT | 使用字典实现的哈希对象 |
8 | REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象 |
9 | REDIS_SET | REDIS_ENCODING_HT | 使用字典实现的集合对象 |
10 | REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象 |
11 | REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象 |
注:embstr编码是专门由于保存短字符串的一种优化编码方式。 |
对不同的对象设置不止一种编码,同一种对象的不同编码在不同的场景下使用,极大提高了redis的灵活性和效率。比如当list类型包含数据不多时,使用连续空间的压缩列表保存,相比于双向链表,可以更快将数据载入缓存;当数据量很多时,连续空间的压缩列表优势渐渐消失,反而双向链表更有优势。
下面对于上面五种数据类型,八种数据结构,十一种组合使用场景进行介绍(小序号对应上表中1~11表项):
1、若字符串对象保存的是整数对象,且可使用long保存该整数,则redisObject使用该组合进行存储,存储时void * 指针改为long型。
2、若字符串对象保存的是短字符串(长度不超过39字节),则使用embstr编码方式保存该字符串对象。
3、若字符串对象保存的是长字符串(长度超过39字节),则使用raw编码方式保存该字符串对象。
说明:int类型对象和embstr类型对象在某些情况下会被转化为raw对象。如在8888整型对象后面APPEND一个字符串;或者将embstr字符串做增减。Redis不提供对embstr修改的api,也即embstr是只读型对象。字符串对象是唯一可以被其他四种数据类型(对象)嵌套使用的数据类型。
4、若列表对象保存的元素长度均小于64字节并且元素数量小于512个,则redisObject使用压缩列表编码。
5、若列表对象保存的元素不符合第4条规定时,使用双向链表编码。
6、若哈希对象保存的所有键值对的键和值长度都小于64字节,并且键值对数量小于512个,则使用压缩列表保存哈希对象。
7、若哈希对象保存的所有键值对不符合第6条规定时,则使用字典结构保存哈希对象。
说明:当用压缩列表保存时,要求空间是连续的,保存键值对时,先添加键对象到表尾,再添加值对象到表尾,即压缩列表使用相邻的两个节点保存哈希元素。使用字典保存时,字典的key就是哈希元素的键,字典的value就是哈希元素的值。
8、若集合对象保存的所有元素都是整数,且数量不超过512个时,则使用整数集合结构保存集合对象。
9、若集合对象保存的元素不符合第8条规定时,则使用字典保存集合对象。
说明:当使用字典保存集合对象时,字典中的key保存的是集合元素,而字典的value保存NULL。
10、若有序集合对象的所有成员元素长度小于64字节,并且总数量小于128个时,使用压缩列表编码有序集合对象。
11、若有序集合对象的所有成员元素不符合第10条规定时使用跳跃表编码有序集合对象。
说明:使用压缩列表作为底层实现时,redis使用相邻的两个节点分别存储成员名称和成员分值,并且按照分值从小到大排列。使用跳跃表时,每个有序集合包含一个跳跃表和一个字典:每个跳跃表节点保存一个有序集合元素,节点中的object保存元素成员,score保存分值,此部分实现了有序集合中和范围有关的操作,如ZRANK、ZRANGE等。字典结构的key保存了元素名称,value保存了每个元素的分值,此部分在O(1)时间复杂度内实现了查找确定分值。有序集合使用跳跃表和字典两个数据结构的出发点是高执行效率。虽然跳跃表和字典都保存了有序集合内元素的数据,但是实际上这些数据是通过指针共享的,并没有浪费内存。
以上,介绍完了redis的所有数据结构和基本数据类型,以及之间的底层实现关系,关于上述的编码区分阈值,均可在配置文件中修改。下面简单说下redis内的内存回收机制:
在每个对象中有个引用计数属性,创建对象初始化时为1,当对象被程序使用或使用完毕后,引用计数器进行增减,当减到0时便会将内存释放。
引用计数器除对内存的回收功能有作用之外,还可用于对象共享。当redis中有两个相同的对象时,会使用共享机制节约内存,而不是创建两个对象。此时只需将对象的引用计数器增加即可。redis服务初始化时,会创建0-9999所有整数值,当使用时只是增加引用计数而已。redis之所以只初始化共享0-9999是因为在共享时,需要先判断对象是否完全相同,而其他类型的对象,判断起来时间复杂度均高一些,只有整数判断时间复杂度为O(1)。
最后,提一下lru属性。此属性类似于操作系统中进程、分页存储等机制中的lru,即若淘汰算法为lru淘汰制,则当内存不足时,将lru最大值的对象空间释放。