Redis是基于上一篇文章所说的数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象。
Redis的对象系统还实现了基于引用技术技术的内存回收机制,当程序不在使用某个对象的时候,这个对象所占用的内存就会被自动释放。
对象的类型和编码
对象结构
typedef struct redisObject {
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
//
...
}robj;
类型
对象的type属性记录了对象的类型,值是五种基础对象之一。
对于Redis数据库保存对的键值对来说,键总是一个字符串对象,而值可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种。
对象 | 对象 type 属性的值 |
TYPE 命令的输出 |
---|---|---|
字符串对象 | REDIS_STRING |
"string" |
列表对象 | REDIS_LIST |
"list" |
哈希对象 | REDIS_HASH |
"hash" |
集合对象 | REDIS_SET |
"set" |
有序集合对象 | REDIS_ZSET |
"zset" |
使用Type命令可以返回数据库键对应的值对象的类型:
#键为字符串对象,值为字符串对象
redis> SET msg "hello world"
OK
redis> TYPE msg
string
#键为字符串对象,值为列表对象
redis>RPUSH numbers 1 3 5
(integer) 6
redis>TYPE numbers
list
·····
编码和底层实现
对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。
encoding属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现。
每种类型的对象都至少使用了两种不同的编码。
类型 | 编码 | 对象 |
---|---|---|
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 wordl"
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"
····
字符串对象
字符串对象的编码可以是int、raw或者embstr
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面。
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。
embstr编码是专门用于和保存短字符串的一种优化编码方式,这种编码和raw编码一样,都是用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构。
列表对象
列表对象的编码可以是ziplist或者linkedlist。
ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。
linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
linkdedlist编码的列表对象在底层的双端链表结构中包含了多个字符串对象,这种嵌套字符串对象的行为在哈希对象、集合对象和有序结合对象中都会出现,字符串对象是Redis五种类型的对象中唯一一种会被其它四中类型对象嵌套的对象。
哈希对象
哈希对象的编码可以是ziplist或者hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。
hashtable编码的哈希对象使用了字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:
- 字典的每个键都是一个字符串对象,对象中保存了键值对的键;
- 字典的每个值都是一个字符串对象,对象中保存了键值对的值。
集合对象
结合对象的编码可以是intset或者hashtable。
intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。
hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
有序集合对象
有序集合的编码可以是ziplist或者skiplist。
ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。
压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的位置,而分值较大的元素则被放置在靠近表尾的方向。
skiplist编码的有序结合对象使用zset结构作为底层实现,一个zset结构包含一个字典和一个跳跃表:
typedef struct zset{
zskiplist *zsl;
dict *dict;
}zset;
zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。
通过这个跳跃表,程序可以对有序集合进行范围型操作。、
除此之外,zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。
通过这个字典,程序可以用O(1)复杂度查找给定成员的分值。
zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。
编码与转换
在每一种对象中,都会存在编码和转换,个人的理解是在满足特定条件下,对象底层的编码方式会从这种转换为另一种。
举些例子:
在字符串对象中:
int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换成raw编码的字符串对象。
如果通过APPEND命令,向一个保存了整数值的字符串对象追加了一个字符串值,因为追加操作只能对字符串值执行,所以程序会先将之前保存的整数值转换成字符串值,然后再执行追加操作,操作的执行结果就是一个raw编码的保存了字符串值的字符串对象。
在哈希对象中:
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
- 哈希对象保存的键值对数量小于512个;
不能满足这两个条件的哈希对象需要使用hashtable编码;
还有关于每个对象API在上一篇文章讲过,根据每个对象的特性,用于保存相应的数据上。
Redis还有很多需要学习,关于单机和集群、事务、持久化等都还没了解到,但是底层了解之后就是用于实战,下一篇文章将会在实际项目中使用Redis