Redis 的键值对中的 key 就是字符串对象,而 value 可以是字符串对象,也可以是集合数据类型的对象,比如 List 对象、Hash 对象、Set 对象和 Zset 对象。
Redis 是使用了一个「哈希表」保存所有键值对,哈希表的最大好处就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对。哈希表其实就是一个数组,数组中的元素叫做哈希桶。
Redis 的哈希桶是怎么保存键值对数据的呢?
哈希桶存放的是指向键值对数据的指针(dictEntry*),这样通过指针就能找到键值对数据,然后因为键值对的值可以保存字符串对象和集合数据类型的对象,所以键值对的数据结构中并不是直接保存值本身,而是保存了 void * key 和 void * value 指针,分别指向了实际的键对象和值对象,这样一来,即使值是集合数据,也可以通过 void * value 指针找到。
图中涉及到的数据结构的名字和用途:
- redisDB 结构,表示 Redis 数据库的结构,结构体里存放了指向了 dict 结构的指针;
- dict 结构,结构体里存放了 2 个哈希表,正常情况下都是用ht[0],ht[1]只有在 rehash 的时候才用
- ditctht 结构,表示哈希表的结构,结构里存放了哈希表数组,数组中的每个元素都是指向一个哈希表节点结构(dictEntry)的指针;
- dictEntry 结构,表示哈希表节点的结构,结构里存放了 **void * key 和 void * value 指针, key 指向的是 String 对象,而 value 则可以指向 String 对象,也可以指向集合类型的对象,比如 List 对象、Hash 对象、Set 对象和 Zset 对象。
特别说明下, *key 和 *value 指针指向的是 Redis 对象,Redis 中的每个对象都由 redisObject 结构表示,如下图:
对象结构里包含的成员变量:
- type,标识该对象是什么类型的对象(String 对象、 List 对象、Hash 对象、Set 对象和 Zset 对象);
- encoding,标识该对象使用了哪种底层的数据结构;
- ptr,指向底层数据结构的指针。
Redis 对象和数据结构的关系
通过简单动态字符串 (SDS) 、双端链表、字典、压缩列表、整数集合这五种不同类型的对象,Redis可以在执行命令之前,根据对象的类型来判断个对象是否可以执行给定的命令。使用对象的另一个好处是,我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率
除此之外,Redis的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外,Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。
最后,Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除。
对象的类型与编码
Redis使用对象来表示数据库中的键和值,每次当我们在Redis的数据库中新创建-个键值对时,我们至少会创建两个对象,一个对象用作键值对的键 (键对象) ,另个对象用作键值对的值 (值对象)
Redis中的每个对象都由一个redisObiect结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性
typedef struct redisObject {
/*对象的类型*/
unsigned type:4;
//具体的数据结构
unsigned encoding:4;
/* 24位,对象最后一次被命令程序访问的时间,与内存回收有关*/
/* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
unsigned lru:LRU_BITS;
/*引用计数。当refcount为0的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了*/
int refcount;
/*指向对象实际的数据结构*/
void *ptr;
} robj;
redisObject 属性:类型(type)
对象的type属性记录了对象的类型,这个属性的值可以是表8-1列出的常量的其中
| 类型常量 | 对象的名称 |
|---|---|
| REDIS_STRING | 字符串对象 |
| REDIS_LIST | 列表对象 |
| REDIS_HASH | 哈希对象 |
| REDIS_SET | 集合对象 |
| REDIS_ZSET | 有序集合对象 |
redisObject 属性:编码(encoding)
对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的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_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
| REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用 embstr 编码的简单动态字符串实现的字符串对象 |
| REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 |
| REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象 |
| REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的列表对象 |
| REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
| REDIS_HASH | REDIS_ENCODING_HT | 使用字典实现的哈希对象 |
| REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象 |
| REDIS_SET | REDIS_ENCODING_HT | 使用字典实现的集合对象 |
| REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象 |
| REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象 |
使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码
redis> SET msg "hello wrold"
OK
redis> OBJECT ENCODING msg
"embstr"
redis> SET story "long long long long long long ago ..."
OK
redis> OBJECT ENCODING story
"raw"
redis> SADD numbers 1 3 5
(integer) 3
redis> OBJECT ENCODING numbers
"intset"
redis> SADD numbers "seven"
(integer) 1
redis> OBJECT ENCODING numbers
"hashtable"
127.0.0.1:6381> SET story "redis mongoDB redis mongoDB redis mongoDB....."
OK
127.0.0.1:6381> object encoding story
"raw"
127.0.0.1:6381> STRLEN story
(integer) 146
字符串对象
SDS数据结构代码实现
redis5.0 之后的sds结构
struct __attribute__ ((__packed__)) sdshdr8 {
//当前字符数组的长度
uint8_t len; /* used */
//当前字符数组总共分配的内存大小
uint8_t alloc; /* excluding the header and null terminator */
//当前字符数组的属性、用来标识到底是 sdshdr8 还是 sdshdr16 等
unsigned char flags; /* 3 lsb of type, 5 unused bits */
//字符串真正的值
char buf[];
};
redis 3.2 之前的sds结构
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
unsigned int len;
// buf 中剩余可用空间的长度
unsigned int free;
// 数据空间
char buf[];
};
String对象
字符串对象的编码可以是int、raw或者embstr
- int:存储 8 个字节的长整型(long,
)。
- embstr:代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串),存储小于 44 个字节的字符串,只分配一次内存空间(因为 Redis Object 和 SDS 是连续的)。
- raw:存储大于 44 个字节的字符串,需要分配两次内存空间(分别为 Redis Object 和 SDS 分配空间)。
- redis 2.+ 是 32 字节
- redis 3.0-4.0 是 39 字节
- redis 5.0 是 44 字节
为什么从39改成44
/* object.c */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 //制定长度
为什么redis 3.2 之前的sds结构。embstr在存储小于39个字符都是embstr的,大于39个之后就是raw的了。
sdshdr = unsigned int * 2 = 4 * 2 = 8
embstr : redisObject(16)+sds{char(39个) + 8 + 1} =64
此时可以看出如果是39个字符就是64个字节,那么也就是说只要是小于等于39个字符的,分配的空间都是64个字节。超过64个字节就为raw。
为什么redis 3.2 之后的sds结构。embstr在存储小于等于44个字符都是embstr的,大于44个之后就是raw的了。
在 3.2 以后的版本中,SDS 又有多种结构(sds.h文件中):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表
通过查看redis 3.2 后中的sds的结构,经过分析可以看到sds使用了uint8_t代替int,占用的内存空间更小了,下面计算一下sds的头部信息占用的空间大小:sdsdr8 = uint8_t (1个字节)* 2 + char(1个字节) = 3个字节
由于对sds的头部信息结构做了调整,所以在原来39的基础上又可以多存储了5个字节,为44个字节。
REDIS_ENCODING_INT
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面 (将void*转换成long) ,并将字符串对象的编码设置为int。
127.0.0.1:6381> set number 10086
OK
127.0.0.1:6381> object encoding number
"int"
REDIS_ENCODING_RAW 和 REDIS_ENCODING_EMBSTR
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于44字节,那么字符串对象将使用一个简单动态字符串 (SDS)来保存这个字符串值,并将对象的编码设置为raw。
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于44字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。
127.0.0.1:6381> set name "qwertyuiopasdfghjklzxcvbnm123456789012345678"
OK
127.0.0.1:6381> strlen name
(integer) 44
127.0.0.1:6381> object encoding name
"embstr"
127.0.0.1:6381> set name "qwertyuiopasdfghjklzxcvbnm1234567890123456789"
OK
127.0.0.1:6381> strlen name
(integer) 45
127.0.0.1:6381> object encoding name
"raw"
raw
embstr
embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码样,都使用redisObiect结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObiect结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObiect和sdshdr两个结构,如下图所示
embstr编码的字符串对象在执行命令时,产生的效果和raw编码的字符串对象执行命令时产生的效果是相同的,但使用embstr编码的字符串对象来保存短字符串值有以下好处:
- embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次。
- 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数
- 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势。
类型转换
可以用long double类型表示的浮点数在Redis中也是作为字符串值来保存的。如果我们要保存一个浮点数到字符串对象里面,那么程席会先将这个浮点数转换成字符串值,然后再保存转换所得的字符串值
举个例子,执行以下代码将创建一个包含3.14的字符串表示3.14(String)的字符串对象:
127.0.0.1:6381> set pi 3.14
OK
127.0.0.1:6381> object encoding pi
"embstr"
127.0.0.1:6381> incrbyfloat pi 2.0
"5.14"
127.0.0.1:6381> object encoding pi
"embstr"
在有需要的时候,程序会将保存在字符串对象里面的字符串值转换回浮点数值,执行某些操作,然后再将执行操作所得的浮点数值转换回字符串值,并继续保存在字符串对象里面。
列表对象
列表对象的编码可以是ziplist或者linkedlist。
ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点 (entry) 保存了一个列表元素。举个例子,如果我们执行以下RPUSH命令,那么服务器将创建一个列表对象作为numbers键的值:
redis> RPUSH numbers 1 "three" 5
(integer) 3
如果numbers键的值对象使用的是ziplist编码,如下所示。
另一方面,linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点 (node) 都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
举个例子,如果前面所说的numbers键创建的列表对象使用的不是ziplist编码,而是linkedlist编码,那么numbers键的值对象如下图所示。
注意,linkedlist编码的列表对象在底层的双端链表结构中包含了多个字符串对象这种嵌套字符串对象的行为在稍后介绍的哈希对象、集合对象和有序集合对象中都会出现,字符串对象是Redis五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象。
注意
为了简化字符串对象的表示,我们在上面图中使用了一个带有StringObject字样的格子来表示一个字符串对象,而StringObiect字符串值"three"的字符串对象的内存如下图所示
编码转换
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码
- 列表对象保存的所有字符串元素的长度都小于64字节
- 列表对象保存的元素数量小于512个,不能满足这两个条件的列表对象需要使用linkedlist编码。
注意
以上两个条件的上限值是可以修改的,具体请看配置文件中关于list-max-ziplist-value选项和list-max-ziplist-entries选项的说明
对于使用ziplist编码的列表对象来说,当使用ziplist编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行,原本保存在压缩列表里的所有列表元素都会被转移并保存到双端链表里面,对象的编码也会从ziplist变为linkedlist.
以下代码展示了列表对象因为保存了长度太大的元素而进行编码转换的情况:
#所有元素的长度都小于64字节
redis> RPUSH blah "hello" "world" "again"
(integer)3
redis> OBJECT ENCODING blah
"ziplist"
#将一个65字节长的元素推入列表对象中
redis> RPUSH blah "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"
(integer) 4
#编码已改变
redis> OBJECT ENCODING blah
"linkedlist"
除此之外,以下代码展示了列表对象因为保存的元素数量过多而进行编码转换的情况
#列表对象包含512个元素
redis> EVAL "for i=1, 512 do redis.call('RPUSH', KEYS[1],i)end" 1 "integers"
(nil)
redis> LLEN integers
(integer) 512
redis> OBJECT ENCODING integers
"ziplist"
#再向列表对象推入一个新元素,使得对象保存的元素数量达到513个
redis> RPUSH integers 513
(integer) 513
#编码已改变
redis> OBJECT ENCODING integers
"linkedlist"
哈希对象
哈希对象的编码可以是ziplist或者hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,因此:
- 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
举个例子,如果我们执行以下HSET命令,那么服务器将创建一个列表对象作为profile键的值:
redis> HSET profile name "Tom"
(integer) 1
redis> HSET profile age 25
(integer) 1
redis> HSET profile career "Programmer"
(integer) 1
如果profile键的值对象使用的是ziplist编码,那么这个值对象将会是下图所示
另一方面,hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存
- 字典的每个键都是一个字符串对象,对象中保存了键值对的键:
- 字典的每个值都是一个字符串对象,对象中保存了键值对的值
举个例子,如果前面profle键创建的不是ziplist编码的哈希对象,而是hashtable编码的哈希对象,那么这个哈希对象应该会是下图所示
编码转换
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
- 哈希对象保存的键值对数量小于512个,不能满足这两个条件的哈希对象需要使用hashtable编码
注意
这两个条件的上限值是可以修改的,具体请看配置文件中关于hash-max-ziplist-value选项和hash-max-ziplist-entries选项的说明
对于使用ziplist编码的列表对象来说,当使用ziplist编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行,原本保存在压缩列表里的所有键值对都会被转移并保存到字典里面,对象的编码也会从ziplist变为hashtable。
以下代码展示了哈希对象因为键值对的键长度太大而引起编码转换的情况
# 哈希对象只包含一个键和值都不超过64个字节的键值对
redis> HSET book name "Mastering C++ in 21 days"
(integer) 1
redis> OBJECT ENCODING book
"ziplist"
#向哈希对象添加一个新的键值对,键的长度为66字节
redis> HSET book long_long_long_long_long_long_long_long_long_long_long_description "content"
(integer) 1
#编码已改变
redis> OBJECT ENCODING book
"hashtable"
除了键的长度太大会引起编码转换之外,值的长度太大也会引起编码转换,以下代码展示了这种情况的一个示例:
#哈希对象只包含一个键和值都不超过64个字节的键值对
redis> HSET blah greeting "hello world"
(integer) 1
redis> OBJECT ENCODING blah
"ziplist"
#向哈希对象添加一个新的键值对,值的长度为68字节
redis> HSET blah story "many string ... many string ... many string ... many string ... many"
(integer) 1
#编码已改变
redis> OBJECT ENCODING blah
"hashtable"
最后,以下代码展示了哈希对象因为包含的键值对数量过多而引起编码转换的情况
#创建一个包含512个键值对的哈希对象
redis> EVAL "for i=1, 512 do redis.call('HSET', KEYS[1], i, i)end" 1 "numbers"
(nil)
redis> HLEN numbers
(integer) 512
redis> OBJECT ENCODING numbers
"ziplist"
#再向哈希对象添加一个新的键值对,使得键值对的数量变成513个
redis> HMSET numbers "key" "value"
OK
redis> HLEN numbers
(integer) 513
#编码改变
redis> OBJECT ENCODING numbers
"hashtable"
SET对象
集合对象的编码可以是intset或者hashtable。
intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面
举个例子
redis> SADD numbers 1 3 5
(integer) 3
以上代码将创建一个如下图所示的intset编码集合对象
另一方面,hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
举例,以下代码将创建一个如下图所示的hashtable编码集合对象:
redis> SAD Dfruits "apple" "banana" "cherry"
(integer)3
编码的转换
当集合对象可以同时满足以下两个条件时,对象使用intset编码
- 集合对象保存的所有元素都是整数值
- 集合对象保存的元素数量不超过512个
不能满足这两个条件的集合对象需要使用hashtable编码
注意
第二个条件的上限值是可以修改的,具体请看配置文件中关于set-max-intset-entries选项的说明
对于使用intset编码的集合对象来说,当使用intset编码所需的两个条件的任意一个不能被满足时,就会执行对象的编码转换操作,原本保存在整数集合中的所有元素都会被转移并保存到字典里面,并且对象的编码也会从intset变为hashtable.
举个例子,以下代码创建了一个只包含整数元素的集合对象,该对象的编码为intset:
redis> SADD numbers 1 3 5
(integer) 3
redis> OBJECT ENCODING numbers
"intset"
不过,只要我们向这个只包含整数元素的集合对象添加一个字符串元素,集合对象的编码转移操作就会被执行:
redis> SADD numbers "seven"
(integer) 1
redis> OBJECT ENCODING numbers
"hashtable"
除此之外,如果我们创建一个包含512个整数元素的集合对象,那么对象的编码应该会是intset:
redis> EVAL "for i=1, 512 do redis.call('SADD', KEYS[1], i) end" 1 integers
(nil)
redis> SCARD integers
(integer) 512
redis> OBJECT ENCODING integers
"intset"
但是,只要我们再向集合添加一个新的整数元素,使得这个集合的元素数量变成513,那么对象的编码转换操作就会被执行:
redis> SADD integers 10086
(integer) 1
redis> SCARD integers
(integer) 513
redis> OBJECT ENCODING integers
"hashtable"
ZSET对象
有序集合的编码可以是ziplist或者skiplist。
ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。
压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。
举个例子,如果我们执行以下ZADD命令,那么服务器将创建一个有序集合对象作为price键的值:
redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
如果price键的值对象使用的是ziplist编码
skiplist编码的有序集合对象使用zset结构作为底层实现,一个字典和一个跳跃表:
typedef struct zset {
zskiplist *zsl;
dict *dict;
} zset;
zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素: 跳跃表节点的obiect属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的
除此之外,zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素: 字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以用O (1) 复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的,而很多其他有序集合命令都在实现的内部用到了这一特性。
有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数。值得一提的是,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存
为什么有序集合需要同时使用跳跃表和字典来实现?
在理论上,有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现但无论单独使用字典还是跳跃表,在性能上对比起同时使用字典和跳跃表都会有所降低。
举个例子,如果我们只使用字典来实现有序集合,那么虽然以O (1) 复杂度查找成员的分值这一特性会被保留,但是,因为字典以无序的方式来保存集合元素,所以每次在执行范围型操作一-比如ZRANK、ZRANGE等命令时,程序都需要对字典保存的所有元素进行排序,完成这种排序需要至少O (NIogN)时间复杂度,以及额外的O (N) 内存空间 (因为要创建一个数组来保存排序后的元素)
另一方面,如果我们只使用跳跃表来实现有序集合,那么跳跃表执行范围型操作的所有优点都会被保留,但因为没有了字典,所以根据成员查找分值这一操作的复杂度将从O (1) 上升为O (logN) 。因为以上原因,为了让有序集合的查找和范围型操作都尽可能快地执行,Redis选择了同时使用字典和跳跃表两种数据结构来实现有序集合
编码的转换
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码
- 有序集合保存的元素数量小于128个;
- 有序集合保存的所有元素成员的长度都小于64字节:
不能满足以上两个条件的有序集合对象将使用skiplist编码
注意
以上两个条件的上限值是可以修改的,具体请看配置文件中关于zset-max-ziplist-entries选项和zset-max-ziplist-value选项的说明
对于使用ziplist编码的有序集合对象来说,当使用ziplist编码所需的两个条件中的任意一个不能被满足时,就会执行对象的编码转换操作,原本保存在压缩列表里的所有集合元素都会被转移并保存到zset结构里面,对象的编码也会从ziplist变为skiplist。
以下代码展示了有序集合对象因为包含了过多元素而引发编码转换的情况
#对象包含了128个元素
redis> EVAL "for i=1, 128 do redis.call('ZADD', KEYS[1], i, i) end" 1 numbers
(nil)
redis> ZCARD numbers
(integer) 128
redis> OBJECT ENCODING numbers
"ziplist"
#再添加一个新元素
redis> ZADD numbers 3.14 pi
(integer) 1
#对象包含的元素数量变为129个
redis> ZCARD numbers
(integer) 129
#编码已改变
redis> OBJECT ENCODING numbers
"skiplist"
以下代码则展示了有序集合对象因为元素的成员过长而引发编码转换的情况:
#向有序集合添加一个成员只有三字节长的元素
redis> ZADD blah 1.0 www
(integer) 1
redis> OBJECT ENCODING blah
"ziplist"
#向有序集合添加一个成员为66字节长的元素
redis> ZADD blah 2.0 ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
(integer) 1
#编码已改变
redis> OBJECT ENCODING blah
"skiplist"
内存回收
因为C语言并不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
每个对象的引用计数信息由redisObject结构的refcount属性记录:
typedef struct redisObject {
// ...
//引用计数
int refcount;
// ...
} robj;
对象的引用计数信息会随着对象的使用状态而不断变化:
- 在创建一个新对象时,引用计数的值会被初始化为1;
- 当对象被一个新程序使用时,它的引用计数值会被增一;
- 当对象不再被一个程序使用时,它的引用计数值会被减一;
- 当对象的引用计数值变为0时,对象所占用的内存会被释放。
下表列出了修改对象引用计数的API,这些API分别用于增加、减少、重置对象的引用计数。
| 函数 | 作用 |
|---|---|
| incrRefCount | 将对象的引用计数值增一 |
| decrRefCount | 将对象的引用计数值减一,当对象的引用计数值等于 0 时,释放对象 |
| resetRefCount | 将对象的引用计数值设置为 0,但并不释放对象,这个函数通常在需要重新设置对象的引用计数值时使用 |
对象的整个生命周期可以划分为创建对象、操作对象、释放对象三个阶段。作为例子,以下代码展示了一个字符串对象从创建到释放的整个过程
//创建一个字符串对象s,对象的引用计数为1
robj *s = createStringObject(...)
//对象s执行各种操作...
//将对象s的引用计数减一,使得对象的引用计数变为0
//导致对象s被释放
decrRefCount(s)