本节介绍五种类型的对象、对象对应的编码和相应的底层实现。
一、对象的类型和编码
Redis 使用对象来表示数据库中的键和值。当我们在Redis中新创建一个键值对时,我们至少创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。
每一个对象都由一个redisObject结构表示:
typedef struct redisObject { // 类型 unsigned type:4; // 编码 unsigned encoding:4; // 指向底层实现数据结构的指针 void *ptr; // ... } robj;
五种对象类型以及TYPE命令输出
对象 | 对象type属性的值 | TYPE命令的输出 |
---|---|---|
字符串对象 | REDIS_STRING | "string" |
列表对象 | REDIS_LIST | "list" |
哈希对象 | REDIS_HASH | "hash" |
集合对象 | REDIS_SET | set |
有序集合对象 | REDIS_ZSET | "zset" |
对象的编码
encoding属性记录了对象使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现。
编码常量 | 编码对应的底层数据结构 |
---|---|
REDIS_ENCODING_INT | long类型的整数 |
REDIS_ENCODING_EMBSTR | enbstr编码的简单动态字符串 |
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_ENBSTR | 使用embstr编码的简单动态字符串实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串的对象 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的列表对象 |
REDIS_HSAH | 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命令可以查看一个数据库的键的值对象的编码。
> OBJECT ENCODING key_name
二、字符串对象
字符串对象的编码可以是int、raw或者enbstr。
如果一个字符串对象保存到的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void* 转换成long)并将字符串对象的编码设置为int。
如果字符串对象保存的是一个字符串值,并且字符串长度大于39字节,那么字符串对象使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。
如果字符串对象保存的是一个字符串值,并且这个字符串的长度小于39字节,那么字符串对象将使用embstr编码的方式来保存这个字符串的值。
使用embstr编码的字符串对象保存短字符串好处:
- 创建字符串对象所需内存分配数从raw编码的两次降低为一次。释放对象也只需调用一次内存释放函数。
- 所有数据都保存在一块连续的内存里面,能够更好地利用缓存带来的优势。
编码转换:
- 对int编码的字符串执行一些命令使这个对象报保存的不再是整数值,而是一个字符串值,那么字符串对象的编码将从int变成raw。
- embstr编码的字符串对象是只读的。当我们对embstr编码的字符串对象执行任何修改命令时,程序会先将对象的编码从embstr转换成raw,然后在执行修改命令。这也是为什么int编码的字符串对象进行编码转换只能转换成raw编码的字符串对象。
三、列表对象
列表对象的编码可以是ziplist或者linkedlist。
linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
这种嵌套字符串对象的行为在后面介绍的哈希对象、集合对象和有序集合对象中都会出现,字符串对象是redis五种类型的对象唯一一种会被其他四种对象嵌套的对象。
编码转换:
同时满足以下两个条件,列表对象使用ziplist编码:
- 列表对象保存的所有字符串元素长度都小于64字节;
- 列表对象保存的元素数量小于512个;
- 不能满足这两个条件的列表对象需要使用linkedlist编码。
四、哈希对象
哈希对象的编码可以是ziplist和hashtable。
ziplist编码的哈希对象:
- 先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。两个节点紧挨在一起,键节点在前,值节点在后。
- 尾插法。
hashtable编码的哈希对象使用字典作为底层实现:
- 字典的每个键都是一个字符串对象,对象中保存了键值对的键;
- 字典的每个值都是一个字符串对象,对象中保存了键值对的值;
编码转换:
满足以下两个条件,哈希对象使用ziplist编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
- 哈希对象保存的键值对数量小于512个;
- 不能满足这两个条件的哈希对象需要使用hashtable编码。
五、集合对象
集合对象的编码可以是intset或者hashtable。
编码转换:
满足以下两个条件,对象使用intset编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过512个;
- 不能满足这两个条件的集合对象需要使用hashtable编码。
六、有序集合对象
有序集合的编码可以是ziplist或者skiplist。
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
typedef struct zset {
zskiplist *zsl;
dict *dict;
} zset;
为什么有序集合需要同时使用跳跃表和字典来实现?
- 使用跳跃表:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的。
- 使用字典:程序可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的。
- 须知:在实际中,字典和跳跃表会共享元素的成员和分值,所以并不会造成任何数据重复,也不会因此浪费任何内存。
编码转换:
同时满足以下两个条件,对象使用ziplist编码:
- 有序集合保存的元素数量小于128个;
- 有序集合保存的所有元素成员的长度都小于64字节。
- 不满足以两个条件的有序集合对象都将使用skiplist编码。
七、类型检查与命令多态
Redis用于操作键的命令基本可以分为两种类型:
命令可以对任何类型的键执行。比如
DEL
命令、EXPIRE
命令、RENAME
命令、TYPE
命令、OBJECT
命令等。-
另一种命令只能对特定类型的键执行,比如说:
SET, GET, APPEND, STRLEN等命令只能对字符串键执行; ...
类型检查
在执行一个类型特定的命令之前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令。
类型检查:redisObject.type == target_type ?
多态命令的实现
Redis会根据值对象的编码方式,选择正确的命令实现代码来执行命令。
比如:只要执行LLEN命名的是列表键,那么无论值对象使用的是ziplist还是linkedlist编码,命令都可以正常实行。
DEL、EXPIRE等命令和LLEN等命令的区别在于,前者是基于类型的多态——一个命令可以同时处理不同类型的键,而后者是基于编码的多态——一个命令可以同时处理多种不同编码。
八、内存回收
因为C语言并不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制。
typedef struct redisObject {
// ...
// 引用计数
// ...
} robj;
九、对象共享
除了用于实现引用计数内存回收机制之外,对象的引用计数属性还带有对象共享的作用。
实现机制:
- 将数据库键的值指针指向一个现有的值对象;
- 将被共享的值对象的引用计数增一。
共享对象机制对于节约内训非常有帮助,数据库中保存的相同值越多,对象共享机制就越能节约越多的内存。
为什么Redis不共享包含字符串的对象?
- 一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同所需的复杂度就会越高,消耗CPU时间也会越多。
- 如果共享对象是保存整数值的字符串对象,那么验证操作的复杂度为O(1);
- 如果共享对象是保存字符串值的字符串对象,那么验证操作的复杂度为O(N);
- 受到CPU时间的限制,Redis只对包含整数值的字符串对象进行共享。
- 目前来书,Redis在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
十、对象的空转时长
除了前面介绍过的type、encoding、ptr和refcount四个属性之外,redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一i此被命令程序访问的时间:
typedef struct redisObject {
// ...
unsigned lru:22;
// ...
}robj;
空转时长就是当前时间减去键的值对象的lru时间计算得出的。