一、对象
- Redis中有六种主要数据结构:
简单动态字符串
、双端链表
、字典
、跳跃表
、整数集合
、压缩列表
; - Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,每种对象都用到了至少一种数据结构;
- Redis使用
encoding
属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,这样极大地提高了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率; - 五种类型的对象:字符串对象(REDIS_STRING)、列表对象(REDIS_LIST)、哈希对象(REDIS_HASH)、集合对象(REDIS_SET)、有序集合对象(REDIS_ZSET);
1.1 对象结构
typedef struct redisObject{
// 对象类型
unsigned type:4;
// 对象编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// 引用计数
int refcount;
// 对象空转时长
unsigned lru:32;
// ...
}robj;
type
:记录了对象的类型,取值有五种:REDIS_STRING
:字符串对象;REDIS_LIST
:列表对象;REDIS_HASH
:哈希对象;REDIS_SET
:集合对象;REDIS_ZSET
:有序集合对象;
encoding
:记录了对象所使用的编码,表示对象使用了什么数据结构作为对象的底层实现;ptr
:指向底层数据结构的指针;refcount
:对象引用计数,用于内存回收;lru
:对象空转时长,记录对象最后一次被访问的时间;
1.2 对象类型
- Redis的键总是一个字符串对象;
- Redis的值可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象;
- 对Redis的键执行
type
命令,查看的不是键对象的类型,而是对应的值对象的类型;
127.0.0.1:6379> set key1 hello
OK
127.0.0.1:6379> type key1
string
127.0.0.1:6379> lpush key2 1 2 3 4
(integer) 4
127.0.0.1:6379> type key2
list
127.0.0.1:6379> sadd key3 1 2 3
(integer) 3
127.0.0.1:6379> type key3
set
127.0.0.1:6379> hset key4 name tom age 24
(integer) 2
127.0.0.1:6379> type key4
hash
127.0.0.1:6379> zadd key5 24 tom 25 jack
(integer) 2
127.0.0.1:6379> type key5
zset
1.3 对象命令
type
命令查看的是对象类型,读取对象的type
属性,返回值是:string
、list
、set
、hash
、zset
;object encoding
命令查看对象的编码,即对象的底层实现,读取对象的encoding
属性,object refcount
命令查看对象的引用计数,读取对象的refcount
属性;object idletime
命令查看对象的空转时间,是当前时间减去对象的lru
时间计算得来的;该命令不会修改对象的lru
属性;
1.4 内存回收
C语言不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个基于引用计数技术实现内存回收机制;
- 创建一个对象时,引用计数值会被初始化为1;
- 当一个对象被新程序使用时,引用计数会被增1;
- 当一个对象不再被一个程序使用时,引用计数值会被减1;
- 当对象的引用计数值为0时,对象所占用的内存会被释放;
1.5 对象共享
- Redis使用共享对象机制节约内存使用;
- 对象的引用计数可以表示对象的共享量;
- Redis在初始化服务器时,会创建一万个字符串对象,包含了从0到9999的所有整数值,当服务器用到这些值时,直接使用共享对象;
- 为什么不共享包含字符串的对象?因为只有在共享对象和目标对象完全相同的情况下,程序才会将共享对象作为键的值对象,而一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同所需的复杂度越高,消耗的CPU时间也会越长;
1.6 对象空转时长
- 对象的
lru
属性记录对象最后一次被程序访问的时间; - 如果服务器打开了
maxmemory
选项,那么当服务器占用的内存达到最大内存时会进行内存回收,如果选择的回收算法是volatile-lru
或allkeys-lru
,则空转时间较高的键会优先被服务器回收;
1.7 类型检查命令多态
类型检查
Redis会根据值对象的类型
(type
属性)来判断键
是否能够执行指定的命令。
Redis中用于操作键的命令基本上可以分为两种类型:
- 一种可以对任何类型的键执行,如
DEL命令
、EXPIRE命令
、TYPE命令
、OBJECT命令
等 - 另一种只能对特定类型的键执行:比如
SET
只能对字符串键执行;
注意:当我们称呼一个数据库键为字符串键时,我们指的是这个数据库键对应的值为字符串对象;
在执行一个特定类型的命令之前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令,执行类型检查是通过redisObject
结构的type
属性来实现的;
命令多态
一个类型的对象会有多种不同的底层实现方式,Redis会根据值对象的编码方式(encoding
属性),选择正确的命令实现代码来执行命令。
- 基于类型的多态:一个命令可以同时处理多种不同类型的键(对象类型不同);
- 基于编码的多态:一个命令可以同时处理用多种不同编码实现的对象(对象类型相同,实现的数据结构不同);
二、字符串对象
- 字符串对象的
type
是:string
; - 字符串对象的编码可以是:
int
、embstr
、raw
,对应的encoding
属性是:REDIS_ENCODING_INT
、REDIS_ENCODING_EMBSTR
、REDIS_ENCODING_RAW
; - 字符串对象是Redis五种类型的对象中唯一一个会被其他四种对象嵌套的对象,即其他四种类型的对象在保存节点值时,有时会使用字符串对象;
2.1 存储结构
int编码
- 如果一个字符串对象保存的是整数,并且这个整数可以用long类型表示,则使用
int
编码,ptr
指针指向long类型整数;
embstr编码
- embstr是专门保存短字符串的一种优化编码方式;
- embstr将
redisObject
结构和sdshdr
结构连续分配在一起,使用一块连续的内存空间分配这两块结构:
使用连续内存空间的好处:
- 创建字符串对象时,所需的内存分配次数从raw编码的两次降低为1次;
- 释放字符串对象时,从raw编码的调用两次内存释放函数降低为调用1次;
raw编码
raw
编码时,使用一个简单动态字符串来保存字符串值,ptr
指针指向一个sds
结构;
2.2 编码选择与转换
- 编码选择:
- 字符串对象保存的是整数时并且该整数可以用
long
类型表示时,使用int
编码; - 字符串对象保存的值长度小于等于
39字节
时,选用短字符串优化编码embstr
方式; - 字符串对象保存的值长度大于
39字节
时,选用raw
编码方式; - 可以用
long double
类型表示的浮点数,在Redis中也是作为字符串值来保存的; - 长度太大没办法用
long
保存的整数,也使用字符串值保存;
127.0.0.1:6379> set k1 3
OK
127.0.0.1:6379> object encoding k1
"int"
127.0.0.1:6379> set k2 3.14
OK
127.0.0.1:6379> object encoding k2
"embstr"
- 编码转换
int
编码的值:执行某些操作,使保存的不再是整数值时(如append操作),字符串编码会从int
变为raw
;embstr
编码的字符串对象没有相应的修改程序,是只读的,当对embstr编码的字符串执行任何修改命令时,程序会先将对象的编码从embstr
转换为raw
,然后再执行修改命令;
127.0.0.1:6379> set k3 hello
OK
127.0.0.1:6379> object encoding k3
"embstr"
127.0.0.1:6379> append k3 thanks
(integer) 11
127.0.0.1:6379> object encoding k3
"raw"
注意:
- 对于
embstr
和raw
编码的字符串对象,不能执行incrby
、decrby
等数字操作;
三、列表对象
- 列表对象的
type
是list
; - 列表对象的编码是
ziplist
或者linkedlist
,对应的encoding
属性是REDIS_ENCODING_ZIPLIST
或REDIS_ENCODING_LINKEDLIST
;对应的底层数据结构是压缩列表
或双端链表
;
3.1 存储结构
ziplist编码
- 当列表中元素个数较少,或元素长度较短时,使用压缩列表作为底层存储结构;
- 压缩列表的好处是节约内存;
- 每个压缩列表节点
entry
保存一个列表元素;
linkedlist编码
- linkedlist编码的列表对象在底层使用双端链表存储对象,存储的是字符串对象;
- 存储结构其中字符串对象是使用的简化的字符串对象表示法,完整的字符串对象参考上小节;
3.2 编码转换
当列表对象同时满足以下两个条件时,列表对象使用ziplist
编码:
- 列表对象保存的元素数量小于512个(可以由配置文件的
list-max-ziplist-entries
设置); - 列表对象保存的所有字符串元素的长度都小于64B(可以由配置文件的
list-max-ziplist-value
设置);
ziplist
编码所需的两个条件中任意一个不满足时,对象就会执行编码转换操作,原本保存在压缩列表里的所有元素都会被转移并保存到双端链表里,对象的编码从ziplist
转换为linkedlist
;
四、哈希对象
- 哈希对象的
type
是hash
; - 哈希对象的编码可以是
ziplist
或hashtable
,对应的encoding
属性是REDIS_ENCODING_ZIPLIST
或REDIS_ENCODING_HT
,对应的底层数据结构是压缩列表
或字典
;
4.1 存储结构
ziplist编码
- 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后添加的键值对会在现有键值对后依次添加;
hashtable编码
- hash编码的底层结构使用
字典
; - 字典的每一个键都是一个字符串对象,对象中保存了键值对的键;
- 字典的每一个值都是一个字符串对象,对象中保存了键值对的值;
4.2 编码转换
当哈希对象可以同时满足以下两个条件时,使用ziplist
编码:
- 哈希对象保存的键值对数量小于512个(可以通过配置文件的
hash-max-ziplist-entries
设置); - 哈希对象保存的所有键值对的键和值的字符串长度都小于64B(可以通过配置文件的
hash-max-ziplist-value
设置);
当ziplist
编码所需的两个条件的任意一个不能被满足时,对象会进行编码转换,编码从ziplist
转换为hashtable
;
五、集合对象Set
- 集合对象的
type
是set
; - 集合对象的编码可以是
intset
或者hashtable
,对应的encoding
属性值是REDIS_ENCODING_INTSET
、REDIS_ENCODING_HT
,对应的底层数据结构是整数集合
和字典
;
5.1 存储结构
intset编码
intset
编码的集合对象,底层使用整数集合实现;- 整数集合的底层实现是数组,这个数组以
有序
、不重复
的方式保存集合元素;
hashtable编码
hashtable
编码的集合对象,底层使用字典实现;- 字典的每个键都是一个字符串对象,对象保存集合中的一个元素;
- 字典的每个值都是NULL;
hashtable
编码的简略实现如下图所示,这里省略了字典的内部实现:
5.2 编码转换
当集合对象同时满足以下两个条件时,使用intset
编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过512个(可以在配置文件的
set-max-intset-entries
设置)
当使用intset
编码所需的两个条件的任意一个不能被满足时,就会执行对象的编码转换操作,原本保存在整数集合里的所有元素都会被转移并保存到字典里面,并且对象的编码也会从intset
变为hashtable
;
六、有序集合对象ZSet
- 有序集合对象的
type
是zset
; - 有序集合的编码可以是
ziplist
或者skiplist
,对应的encoding
属性是REDIS_ENCODING_ZIPLIST
或REDIS_ENCODING_SKIPLIST
,对应的底层数据结构是压缩列表
或跳跃表与字典
;
6.1 存储结构
ziplist编码
ziplist
编码的有序集合,底层使用压缩列表
存储;- 每个集合元素使用两个紧挨着的压缩列表节点保存,第一个节点保存元素的成员,第二个节点保存元素的分值;
- 压缩列表内的集合元素按分值从小到大排序
skiplist编码
skiplist
编码的有序集合对象使用zset
结构作为底层实现,一个zset
结构同时包含一个字典
和一个跳跃表
:
typedef struct zset{
// 跳跃表
zskiplist *zsl;
// 字典
dict * dict;
}zset;
- 跳跃表节点的
object
属性保存了元素的成员,跳跃表节点的score
属性保存了元素的分值; - dict字典为有序集合创建了一个从成员到分值的映射,字典的键保存了元素的成员,字段的值保存了元素的分值;
- 使用
跳跃表
可以快速的执行范围型操作,使用字典
可以快速的查找成员的分值;
为什么有序集合要同时使用跳跃表和字典来实现?
- 字典结构,查找成员分值的时间复杂度是O(1),但是字典是以无序的方式保存集合元素的,在进行范围型操作(比如
zrang
,zrange
)时,需要对字典保存的所有元素进行排序,时间复杂度至少O(NlogN); - 跳跃表结构,存储的数据是有序的,执行范围型操作的速度会很快,但是根据成员查找分值时,不如字典的速度快;
- 所以Redis选择了同时使用字典和跳跃表两种结构来实现有序集合,在不同类型的操作时,可以充分利用两种数据结构的优点;
- 从理论上讲,有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现,但是无论单独使用字典还是跳跃表,在性能上对比同时使用字典和跳跃表都会有所降低;
6.2 编码转换
当有序集合同时满足以下两个条件时,对象使用ziplist
编码:
- 有序集合保存的元素数量小于128(可以通过配置文件的
zset-max-ziplist-entries
设置); - 有序集合保存的所有元素成员的长度都小于64字节(可以通过配置文件的
zset-max-ziplist-value
设置);
当使用ziplist
编码所需的两个条件中的任意一个不能被满足时,程序就会执行编码转换操作,将原本存储在压缩列表里的所有集合元素转移到zset
里面,并将对象的编码从ziplist
改为skiplist
;