Redis对象类型
Redis对象类型
Redis基于基础的数据结构创建的对象:
- 字符串对象、
- 列表对象、
- 哈希对象、
- 集合对象
- 有序集合对象。
对象回收:Redis对象系统实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;Redis通过引用计数技术实现了对象共享机制,在适当的条件下通过让多个数据库键共享同一个内存对象来节约内存;
一、RedisObject
在server.h文件中,给出了RedisObject的结构体定义:
typedef struct redisObject { unsigned type:4;//类型 unsigned encoding:4;//编码 unsigned lru:LRU_BITS; //访问时间lru int refcount;// 引用计数refcount void *ptr;//指向底层实现数据结构的指针 } robj;
- 类型type:
Redis的对象有五种类型,分别是string、hash、list、set和zset,type属性就是用来标识着五种数据类型。type占用4个bit位,其取值和类型对应如下:
#define OBJ_STRING 0 #define OBJ_LIST 1 #define OBJ_SET 2 #define OBJ_ZSET 3 #define OBJ_HASH 4
- 编码类型encoding:
Redis对象的编码方式由encoding参数指定,也就是表示ptr指向的数据以何种数据结构作为底层实现。该字段也占用4个bit位。其取值和对应类型对应如下:
#define OBJ_ENCODING_RAW 0 /* Raw representation */ #define OBJ_ENCODING_INT 1 /* Encoded as integer */ #define OBJ_ENCODING_HT 2 /* Encoded as hash table */ #define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ #define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */ #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */ #define OBJ_ENCODING_INTSET 6 /* Encoded as intset */ #define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ #define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */ #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
上述这几种编码类型与其底层实现如下表:
编码类型 | 底层实现 |
OBJ_ENCODING_RAW | 简单动态字符串sds |
OBJ_ENCODING_INT | long类型的整数 |
OBJ_ENCODING_HT | 字典dict |
OBJ_ENCODING_LINKEDLIST | 双端队列sdlist |
OBJ_ENCODING_ZIPLIST | 压缩列表ziplist |
OBJ_ENCODING_INTSET | 整数集合intset |
OBJ_ENCODING_SKIPLIST | 跳跃表skiplist和字典dict |
OBJ_ENCODING_EMBSTR | EMBSTR编码的简单动态字符串sds |
OBJ_ENCODING_QUICKLIST | 由双端链表和压缩列表构成的快速列表 |
Redis的每一种对象类型可以对应不同的编码方式,这就极大地提升了Redis的灵活性和效率。Redis可以根据不同的使用场景,来选择合适的编码方式,五种对象类型对应的底层编码方式如下表所示:
对象类型 | 编码方式 |
OBJ_STRING 字符串类型 |
OBJ_ENCODING_RAW ,OBJ_ENCODING_INT ,OBJ_ENCODING_EMBSTR 简单动态字符串sds;long类型的整数;EMBSTR编码的简单动态字符串sds |
OBJ_LIST 列表类型 |
OBJ_ENCODING_LINKEDLIST ,OBJ_ENCODING_ZIPLIST ,OBJ_ENCODING_QUICKLIST 双端队列sdlist;压缩列表ziplist;由双端链表和压缩列表构成的快速列表 |
OBJ_SET 集合类型 |
OBJ_ENCODING_INTSET ,OBJ_ENCODING_HT 整数集合intset、字典dict |
OBJ_ZSET 有序集合类型 |
OBJ_ENCODING_ZIPLIST ,OBJ_ENCODING_SKIPLIST 压缩列表ziplist、跳跃表skiplist和字典dict |
OBJ_HASH 哈希类型 |
OBJ_ENCODING_ZIPLIST ,OBJ_ENCODING_HT 压缩列表ziplist、字典dict |
- 访问时间lru
lru表示该对象最后一次被访问的时间,其占用24个bit位。保存该值的目的是为了计算该对象的空转时长,便于后续根据空转时长来决定是否释放该键,回收内存。键的空转时长还有另外一项作用:如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。
- 引用计数refcount
C语言不具备自动内存回收机制,所以Redis对每一个对象设定了引用计数refcount字段,程序通过该字段的信息,在适当的时候自动释放内存进行内存回收。此功能与C++的智能指针相似。
- 当创建一个对象时,其引用计数初始化为1;
- 当这个对象被一个新程序使用时,其引用计数加1;
- 当这个对象不再被一个程序使用时,其引用计数减1;
- 当引用计数为0时,释放该对象,回收内存。
二、t_String 字符串类型
字符串是Redis中最为常见的数据存储类型,其底层实现是简单动态字符串sds,因此,该字符串类型是二进制安全的,这就意味着它可以接受任何格式的数据。另外,Redis规定,字符串类型最多可以容纳的数据长度为512M。Redis提供了下列函数,来检测字符串键的大小。
static int checkStringLength(client *c, long long size) { // 超出了512M,就直接报错 if (size > 512*1024*1024) { addReplyError(c,"string exceeds maximum allowed size (512MB)"); return C_ERR; } return C_OK; }
字符串对象的编码可以是int、raw或者embstr。
- 如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int。
- 如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。
- 如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。embstr编码是专门用于保存短字符串的一种优化编码方式,embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构。
三、t_list 列表对象类型
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:
- 列表对象保存的所有字符串元素的长度都小于64字节;
- 列表对象保存的元素数量小于512个;
不能满足这两个条件的列表对象需要使用linkedlist编码。
以上两个条件的上限值是可以修改的,配置文件中关于list-max-ziplist-value选项和list-max-ziplist-entries
- ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。
- 另一方面,linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
四、t_hash 哈希对象类型
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
- 哈希对象保存的键值对数量小于512个;
不能满足这两个条件的哈希对象需要使用hashtable编码。
这两个条件的上限值是可以修改的,配置文件中关于hash-max-ziplist-value选项和hash-max-ziplist-entrie
- ziplist编码的哈希对象使用压缩列表作为底层实现
每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,因此:
- 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
- hashtable编码的哈希对象使用字典作为底层实现
- 字典的每个键都是一个字符串对象,对象中保存了键值对的键;
- 字典的每个值都是一个字符串对象,对象中保存了键值对的值。
五、t_set 集合对象类型
集合对象的编码可以是intset或者hashtable。
当集合对象可以同时满足以下两个条件时,对象使用intset编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过512个。
不能满足这两个条件的集合对象需要使用hashtable编码。第二个条件的上限值是可以修改的,配置文件中关于set-max-intset-entries选项的说明
- intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。
- 另一方面,hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
六、t_zset 集合对象类型
- ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。
压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。
- zset结构中的zsl跳跃表
zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素。每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作。
除此之外,zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的,而很多其他有序集合命令都在实现的内部用到了这一特性。
在理论上,有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现,但无论单独使用字典还是跳跃表,在性能上对比起同时使用字典和跳跃表都会有所降低。举个例子,如果我们只使用字典来实现有序集合,那么虽然以O(1)复杂度查找成员的分值这一特性会被保留,但是,因为字典以无序的方式来保存集合元素,所以每次在执行范围型操作——比如ZRANK、ZRANGE等命令时,程序都需要对字典保存的所有元素进行排序,完成这种排序需要至少O(NlogN)时间复杂度,以及额外的O(N)内存空间(因为要创建一个数组来保存排序后的元素)。
另一方面,如果我们只使用跳跃表来实现有序集合,那么跳跃表执行范围型操作的所有优点都会被保留,但因为没有了字典,所以根据成员查找分值这一操作的复杂度将从O(1)上升为O(logN)。因为以上原因,为了让有序集合的查找和范围型操作都尽可能快地执行,Redis选择了同时使用字典和跳跃表两种数据结构来实现有序集合。
七、类型检查与命令多态
Redis中用于操作键的命令基本上可以分为两种类型。
- 其中一种命令可以对任何类型的键执行,比如说DEL命令、EXPIRE命令、RENAME命令、TYPE命令、OBJECT命令等;
- 而另一种命令只能对特定类型的键执行,比如说:
❑SET、GET、APPEND、STRLEN等命令只能对字符串键执行;
❑HDEL、HSET、HGET、HLEN等命令只能对哈希键执行;
❑RPUSH、LPOP、LINSERT、LLEN等命令只能对列表键执行;
❑SADD、SPOP、SINTER、SCARD等命令只能对集合键执行;
❑ZADD、ZCARD、ZRANK、ZSCORE等命令只能对有序集合键执行;