0. Redis对象
Redis并没有直接使用6种基础数据结构来直接实现键值对数据库,而是基于这些数据结构再次实现了5种对象,每种对象都可以根据不同的使用场景选取不同的基础数据结构作为底层编码方式,例如 链表对象,可以选用双端链表 linked-list 或者 压缩列表ziplist 两种数据结构。
这样做的好处是:针对不同的使用场景,可以更灵活的选取底层编码数据结构,以提高操作效率,以及达到节省内存的目的。
0.1 对象类型的实现:
Redis中的每个对象都由一个 redisObject
结构体表示:
typedef struct redisObject {
unsigned type:4; //对象类型(5种):REDIS_STRING, REDIS_LIST, REDIS_HASH, REDIS_SET, REDIS_ZSET
unsigned encoding:4; //对象使用的编码方式(8中):REDIS_ENCODING_INT, REDIS_ENCODING_EMBSTR, REDIS_ENCODING_RAW, REDIS_ENCODING_HT, REDIS_ENCODING_LINKEDLIST, REDIS_ENCODING_ZIPLIST, REDIS_ENCODING_INTSET, REDIS_ENCODING_SKIPLIST
void *ptr; //指向底层实现数据结构的指针
} robj;
使用 “TYPE” 命令查看键对象的类型:
redis> SET msg "hello world"
OK
redis> TYPE msg
string
redis> RPUSH numbers 1 3 5
(integer) 6
redis> TYPE numbers
list
0.2 每种类型的对象可以使用的编码方式:
使用 OBJECT ENCODING
命令查看键对象所使用的底层数据结构:
redis> SET msg "hello world"
OK
redis> OBJECT ENCODING msg
"embstr"
1. 字符串对象:
字符串对象的编码可以是 int
、raw
、embstr
。
1.1 使用 int 实现字符串对象:
如果一个字符串对象保存的是整数值,并且这个整数值可以用long整型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换为long),并将字符串对象的编码设置为int。
举例:
执行如下命令向字符串对象中添加元素:
redis> SET number 10086
OK
redis> OBJECT ENCODING number
"int"
对象的存储结构:
1.2 使用 raw 实现字符串对象:
如果字符串对象保存的是一个字符串值,并且这个字符串的长度大于 39个字节,那么字符串对象将使用一个SDS来保存这个字符串值,并将对象的编码设置为raw。
举例:
执行如下命令向字符串对象中添加元素:
redis> SET story "Long, long, long ago there lived a king ..."
OK
redis> STRLEN story
(integer) 43
redis> OBJECT ENCODING story
"raw"
对象的存储结构:
1.3 使用 embstr 实现字符串对象:
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于 39个字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。
使用embstr编码的优点:
- embstr将创建字符串所需的内存分配次数从raw编码的两次降低为一次;
- 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数;
- 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好的利用缓存带来的优势。
举例:
执行如下命令向字符串对象中添加元素:
redis> SET msg "hello"
OK
redis> OBJECT ENCODING msg
"embstr"
对象的存储结构:
1.4 字符串对象的编码格式的转换:
(1)embstr —> raw :
embstr编码的字符串对象是只读的(Redis没有为embstr实现任何修改的程序),所以当embstr编码的字符串独享执行修改操作的命令时,程序会先将 embstr转换成raw编码,然后再执行修改命令;
(2)int —> raw :
对于使用int编码实现的字符串对象,如果修改为一个新的int值,则是不需要进行编码转换的;
但如果重新将值修改为字符串 或者 执行 “APPEND” 命令,(由于 APPEND追加操作只能对字符串值执行,) 程序会先将之前保存的整数值 10086 转换为 字符串值“10086”,然后再执行追加操作。操作的执行结果就是一个raw编码的、保存了字符串值的字符串对象。
2. 列表对象:
列表对象的编码方式可以是 ziplist
或者 linkedlist
。
2.1 使用 ziplist 实现列表对象:
举例:
执行如下命令向 ziplist 中添加元素:
redis> RPUSH numbers 1 "three" 5
(integer) 3
对象在 ziplist 中的存储结构:
2.2 使用 linked list 实现列表对象:
执行如下命令向 linked list 中添加元素:
redis> RPUSH numbers 1 "three" 5
(integer) 3
对象在 linked list 中的存储结构:
注意linkedlist中的每个节点都是一个字符串对象,完整的节点字符串对象的结构是(使用embstr实现的字符串对象):
2.3 列表对象的编码格式的转换:
当列表对象可以 同时 满足以下两个条件时,列表对象使用 ziplist 编码:
- 列表对象保存的所有字符串元素的长度都小于 64字节;
- 列表对象保存的元素数量小于 512个。
否则,如果不能满足以上两个条件,则列表对象需使用 linkedlist 编码。
以上的转换阈值可以在 redis.conf 配置文件中进行修改:
list-max-ziplist-entries 512
list-max-ziplist-value 64
3. 哈希对象:
哈希对象的编码可以是 ziplist
或者 hashtable
。
3.1 使用 ziplist 实现哈希对象:
使用 ziplist 编码实现哈希对象,当插入一个新的键值对时,Redis会先将 “键” 节点插入到压缩列表的表尾,然后再将 “值” 节点插入到压缩列表的表尾。这样,保存在压缩列表中的哈希对象的键值对结构特征为:
- 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
举例:
执行如下命令向 ziplist 中添加元素:
redis> HSET profile name "TOM"
(integer) 1
redis> HSET profile age 25
(integer) 1
redis> HSET profile career "Programmer"
(integer) 1
对象在 ziplist 中的存储结构:
3.2 使用 hashtable 实现哈希对象:
另一种情况是使用 hashtable 编码方式实现 哈希对象,
此时字典中的每个键值对中的键和值都是字符串对象。
举例:
执行如下命令向 hashtable中添加元素:
redis> HSET profile name "TOM"
(integer) 1
redis> HSET profile age 25
(integer) 1
redis> HSET profile career "Programmer"
(integer) 1
对象在 hashtable 中的存储结构:
3.3 哈希对象的编码格式的转换:
当哈希对象同时满足以下两个条件时,哈希对象使用 ziplist 编码:
- 哈希对象保存的所有价值对的键和值的字符串长度都小于 64字节;
- 哈希对象保存的键值对数量小于 512个。
如果不满足以上两个条件中的任意一个,则哈希对象使用 hashtable 的编码方式存储。
以上的转换阈值可以在 redis.conf 配置文件中进行修改:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
4. 集合对象:
集合对象的编码方式可以是 intset
或者 hashtable
。
4.1 使用 intset 实现集合对象:
当集合对象中的所有元素均为 整数 时,采用 intset编码。
举例:
执行如下命令向集合中添加元素:
redis> SADD numbers 1 3 5
(integer) 3
对象在 intset 中的存储结构:
4.2 使用 hashtable 实现集合对象:
当集合对象中的元素包含字符串时,采用 hashtable 作为编码方式,此时 hashtable中的key保存集合对象元素的值,hashtable的value为空。
举例:
执行如下命令向 集合中添加元素:
redis> SADD fruites cherry apple banana
对象在 hashtable 中的存储结构:
4.3 集合对象的编码格式的转换:
当集合对象同时满足 以下两个条件时,对象使用 intset 编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过 512个。
否则如果不满足任意一个条件,则使用 hashtable 编码。
以上的转换阈值可以在 redis.conf 配置文件中进行修改:
set-max-intset-entries 512
5. 有序集合对象:
有序集合的编码方式为 ziplist
或者 skiplist + dict
。
5.1 使用 ziplist 实现有序集合对象:
当有序集合使用 ziplist 作为底层编码实现时,集合中的每个元素使用两个紧挨在一起的压缩列表节点来保存:第一个节点保存元素成员(value),第二个节点保存元素分值(score)。
压缩列表中的集合元素按分值从小到大进行排序,分值较小的元素排在靠近表头的方向,分值较大的元素则排在靠近表尾的方向。
举例:
执行命令向有序集合中添加元素:
redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
对象在ziplist中的存储结构:
5.2 使用 zskiplist + dict 实现有序集合对象:
同时使用两种数据结构的目的是:
-
跳跃列表可以保证按照 O(1) 的时间复杂度 进行 范围型操作(ZRANK、ZRANGE 等按照 score分值范围进行查找);
如果只使用字典,则范围型操作需要对所有元素(哈希存储)进行排序,时间复杂度将退化为 O(logN); -
字典 可以保证 按照 O(1) 的时间复杂度 根据成员(value) 查找 分值(score);(字典的key存储有序集合对象的值,字典的value存储有序集合对象的score分值)
如果只使用skiplist跳跃列表,按照成员value进行查找时需要遍历整个列表,时间复杂度将退化为O(N)。
使用 zskiplist+dict 时的存储效率:
虽然zset对象同时使用 跳跃列表和字典来保存 有序集合的元素,但这两种数据结构都会通过 “指针” 来共享 相同元素的 成员和分值,所以同时使用 跳跃列表和字典 来保存集合元素 不会产生任何重复成员或分值,也不会因此浪费额外的内存。 (原始数据只有一份,跳跃列表和字典这两个数据结构通过各自指针同时指向一块内存)
举例:
执行命令向有序集合中添加元素:
redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
对象在 zskiplist+dict 中的存储结构:
5.3 有序集合对象的编码格式的转换:
当有序集合的对象同时满足以下两个条件时,对象使用ziplist压缩列表的方式编码:
- 有序集合保存的元素个数小于 128个;
- 有序集合保存的所有元素的长度 都小于 64字节。
如果不能满足两个条件中的任何一个,则需要使用 zskiplist + dict 的方式进行编码。
以上的转换阈值可以在 redis.conf 配置文件中进行修改:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
9. Redis对象的 引用计数、内存回收、对象共享、空转时长:
typedef struct redisObject {
type;
encoding;
*ptr;
int refcount; //引用计数
unsigned lru:32; //空转时长:记录对象最后一次被命令程序访问的时间
} redisObject;
在创建一个对象 或者 对象被一个新程序使用时,它的引用计数会加1;
在对象不再被一个程序使用时,它的引用计数会被减1;
当对象的引用计数变为0时,对象所占用的内存将会被释放。
另一种可能导致对象的引用计数增加的操作是:对象共享
新建的键值对中的“值”对象与其他键对应的值对象相同,则可以进行对象共享,以节省内存,如:
redis> set key1 100
OK
redis> set key2 100
OK
新建的两个键值对,值对象相同都是100,则键key1 与 key2 共享同一个对象,此时值对象 “100” 的引用计数加1。
注意:Redis不会共享包含字符串的对象,因为校验两个字符串是否相等需要对字符串进行遍历,这会导致程序的升高。所以Redis只对包含整数值的字符串对象进行共享。