1,前言
Redis
中用自定义的数据结构以达到更好的效果。诸如:简单动态字符串、双端链表、字典、压缩列表、整数集合等等。
Redis
并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统。这个系统包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象。使用对象的好处之一就是可以根据使用场景的不同,选择相应的数据结构以优化使用效率。
Redis中相应的数据结构:
简单动态字符串
链表
字典
整数集合
压缩列表
2,对象
Redis
中的每个对象都由一个redisObject
结构表示,该结构中保存和数据有关的三个属性是:type
属性、encoding
属性、ptr
属性
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
//......
}
2.1,对象的类型与编码
Redis
使用对象来表示数据库中的键值对,每当创建一个键值对时,Redis
会创建两个对象,一个对象用作键值对的键(键对象),另一个用作键值对的值(值对象)
# 例如下列命令创建了两个对象,一个字符串对象用作键,另一个字符串对象用作值
redis> SET msg "hello wrold"
OK
2.1.1,类型
对象的type
属性记录了对象的类型,这个属性的值可以是下表中的任意一个:
类型常量 | 对象名称 |
---|---|
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
对于Redis
来说,键总是一个字符串对象,而值可以是几种对象之一,因此:
- 当我们称呼一个数据库键为”字符串键“时,我们指的是:这个数据库键对应的值为字符串对象
- 当我们称呼一个数据库键为”列表键“时,我们指的是:这个数据库键对应的值为列表对象
TYPE命令
我们可以使用TYPE
命令查看类型,当对一个数据库键执行TYPE
命令时,命令返回的结果是数据库键对应的值对象的类型,而不是键对象的类型。
# 键为字符串对象 值为字符串对象
redis> SET msg "hello world"
OK
redis> TYPE msg
string
# 键为字符串对象 值为字符串对象
redis> RPUSH numbers 1 3 5
(integer) 6
redis> TYPE numbers
list
# 键为字符串对象 值为哈希对象
redis> HMSET profile name Tom age 25 career Programmer
OK
redis> TYPE profile
hash
# 键为字符串对象 值为集合对象
redis> SADD fruit apple banana cherry
(integer)3
redis> TYPE fruit
set
# 键为字符串对象 值为有序集合对象
redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer)3
redis> TYPE price
zset
对象 | 对象type属性的值 | TYPE命令的输出 |
---|---|---|
字符串对象 | REDIS_STRING | ”string“ |
列表对象 | REDIS_LIST | ”list“ |
哈希对象 | REDS_HASH | ”hash“ |
集合对象 | REDIS_SET | ”set“ |
有序集合对象 | REDIS_ZSET | ”zset“ |
2.1.2,编码和底层实现
对象的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 world"
OK
redis> OBJECT ENCODING msg
"embstr"
Redis
根据不同的使用场景为一个对象设置不同的编码,从而优化对象在当前场景下的使用效率。
例如在链表对象包含的元素较少的时候,Redis
可以使用压缩列表来作为列表对象的底层实现。
- 压缩列表比双端链表更节约内存,并且当元素数量少的时候,在内存中以连续块的方式保存的压缩列表比起双端链表能更快的被载入缓存中
- 当元素越来越多时,
Redis
会使用功能更强、更适合保存大量元素的双端链表来保存数据
2.2,字符串对象
字符串对象的编码可以是int
、raw
或者embstr
int编码
如果一个字符串保存的值是整数值,并且这个整数值可以用long
类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr
属性里面(将void*
转换成long
),并将字符串对象的编码设置为REDIS_ENCODING_INT
例如:
redis> SET number 10086
OK
redis> OBJECT ENCODING number
"int"
对于一些浮点数(如3.14)的保存,程序会将这个浮点数转换为字符串值,然后再保存转换所得的字符串值
raw编码
如果字符串保存的是一个字符串值,并且这个值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将编码设置为REDIS_ENCODING_RAW
例如:
redis> SET story "Long,long ago there is lived a king......"
OK
redis> STRLEN story
(integer) 37
redis> OBJECT ENCODING story
“raw”
embstr编码
如果字符串对象保存的是一个字符串值,并且这个值的长度小于等于32字节,那么字符串对象会使用embstr
编码的方式保存字符串值
embstr
与raw
编码都是需要创建redisObject
与sdshdr
两个结构,但embstr
是专门用于保存短字符串的一种优化编码方式。embstr
在内存重分配的时候将创建出来的两个结构分配到同一块连续的内存空间(所以embstr
创建时只需要一次内存重分配,释放时也只需要一次)。
使用embstr
的优点:
- 创建时需要的内存分配次数从
raw
编码的两次降为一次,释放也是如此 - 由于
embstr
的两个结构都是存放在一块连续的内存空间中,所以能更好的利用缓存带来的优势
例如:
redis> SET msg "hello"
OK
redis> OBJECT ENCODING msg
"embstr"
编码的转换
int
编码的字符串对象和embstr
编码的字符串对象在条件满足的情况下,会被转换为raw
编码的字符串对象。例如:
redis> SET number 10086
OK
redis> OBJECT ENCODING number
"int"
redis> APPEND number " is a good number"
(integer) 23
redis> GET number
"10086 is a good number"
redis> OBJECT ENCODING number
"raw"
值得一提的是,因为Redis
并没有为embstr
编码的字符串对象编写任何相应的修改程序(只有int
和raw
编码的有),所以对embstr
编码的字符串对象执行任何修改指令,程序会将对象编码转换为raw
再进行修改。这也往往导致embstr
编码的字符串对象在执行修改指令后,总会变成一个raw
编码的字符串对象
2.3,列表对象
列表的编码可以是ziplist
或者linkedlist
编码
当列表对象同时满足以下两个条件时,列表对象使用ziplist
编码
- 列表对象保存的所有元素长度都小于64字节
- 列表对象保存的元素数量小于512个
其他不满足条件的需要使用linkedlist
编码(这两个条件的上限值是可以修改的)
创建一个列表对象numbers
redis> RPUSH numbers 1 "three" 5
(integer)3
ziplist编码
如果numbers
键使用的是ziplist
编码,那么值对象会是这样的:
linkedlist编码
如果使用linkedlist
编码的话,那么值对象会是这样的:
值得注意的是:这里的StringObject
是上面提及到的字符串对象,这里之所以这样画,是为了简化表示。字符串对象是Redis
五种类型的对象之中唯一一种会被其他类型对象嵌套的对象
以例子中的three
为例,其完整的表示应该为:
编码转换
只要不满足使用ziplist编码的任意一个条件,对象的编码转换操作就会被执行。原本保存的元素会被转移并保存到双端链表中。
redis> RPUSH blah "hello" "world" "again"
(integer)3
redis> OBJECT ENCODING blah
"ziplist"
#将一个大于64字节长的元素输入到列表对象中
redis> RPUSH blah "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"
(integer)4
redis>OBJECT ENCODING blah
"linkedlist"
2.4,哈希对象
哈希对象的编码可以是ziplist
或者hashtable
当哈希对象可以同时满足以下两个条件是,哈希对象使用ziplist
编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
- 哈希对象保存的键值对小于512个
假设有一个哈希对象使用ziplist
作为底层实现,当往这个哈希对象压入新的键值对时,程序会将保存了键的压缩列表节点推入到压缩列表中,随后将保存了值的压缩列表节点推入到压缩列表。所以保存了同一键值对的两个节点总是紧挨着,并且按键-值的顺序进行存放。
ziplist编码
创建一个列表对象profile
redis> HSET profile name "Tom"
(integer) 1
redis> HSET profile age 25
(integer) 1
redis> HSET profile career "Programmer"
(integer) 1
如果该列表对象此时使用的是ziplist
编码,那么哈希对象的示意图如下:
hashtable编码
hashtable
编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都是用一个字典键值对来保存(字典的键与值均是字符串对象)
如果上面的profile
其编码是hashtable
,那么示意图应为:
编码转换
当哈希对象不满足使用ziplist
编码的条件时,对象的编码转换操作就会被执行,原本保存在压缩列表的所有键值对都会被转移并保存到字典中,对象的编码也会变更。
2.5,集合对象
集合对象的编码可以是intset
或者hashatble
当同时满足以下条件时,使用intset编码
- 集合对象保存的元素都是整数值
- 集合对象保存的元素数量不超过512个
intset编码
intset
编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被包含在整数集合中。
redis> SADD numbers 1 3 5
(integer) 3
hashtable编码
hashtable
编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素;字典的值全部被设置为NULL
例如:
redis> SADD Dfruits "appl" "banana" "cherry"
(integer) 3
编码转换
当不满足使用intset
的使用条件时,会触发编码转换。原本保存在整数集合中的所有元素都会被转移并保存到字典中,并更改对象的编码为hashtable
2.6,有序集合对象
有序集合的编码可以是ziplist
或者skiplist
当同时满足以下两个条件时,对象使用ziplist编码:
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素成员的长度都小于64个字节
ziplist编码
ziplist
编码的有序集合对象底层使用压缩列表,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member
),第二个元素则保存元素的分值
压缩列表内的集合元素按分值从小到大进行排序,分值较小的存放在靠近表头的方向,分值较大的存放在靠近表尾的方向。
例如:
redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
假设price
使用压缩列表做底层实现,那么示意图应该是:
skiplist编码
skiplist
编码的有序集合对象使用zset
结构作为底层实现,一个zset
结构同时包含一个字典和一个跳跃表
typedef struct zset{
zskiplist *zsl;
dict *dict;
}zset;
假设price
使用的是skiplist
编码,那么示意图应该为:
编码转换
当有序集合对象不满足ziplist
编码的使用条件时,会触发编码转换;原本保存在压缩列表中的所有元素都会被转移并保存到zset
结构中,并更改对象的编码为skiplist