Redis对象

对象

在Redis中,包含有5中对象,分别是字符串对象,列表对象,哈希对象,集合对象和有序集合对象。每种对象都对应于一种或多中数据结构。这些数据结构就是上一篇文章讲集中底层的数据结构。

通过这五种不同类型的对象,Redis在执行命令前,会根据对象的类型来判断一个对象是否可以执行给定的命令。并且使用这些对象,可以针对不同的使用场景来为对象设置不同的底层数据结构,以提供更好的效率。

此外,Redis的对象系统实现了基于引用计数的内存回收机制,当程序不在使用使用某个对象的时候,对象占用的内存会被自动释放掉,并且通过引用计数法实现了对象共享机制,就是让多个键共享一个对象来节约内存,例如Redis的字符串对象缓存了0-9999这么多个对象用来共享.

对象的类型和编码

Redis中使用一个叫RedisObject来表示的

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层数据结构的指针
void *ptr;
//引用计数
int refcount;
//空转时长
unsigned lru:22;
} robj;

type 就是上面提到的5种类型之一,encoding则表示此对象所使用的编码方式,也就是该对象使用的底层数据结构是什么。 refcount是用引用计数实现内存回收机制,当计数为0的时候回释放掉对象。空转时长lru则保存上次访问此对象的unix时间戳。

type 表示是什么对象

类型常量 对象名称 实现方式
REDIS_STRING 字符串对象 int实现,embstr编码的SDS,普通SDS
REDIS_HASH 哈希对象 压缩列表或字典
REDIS_LIST 列表对象 压缩列表或双端链表
REDIS_SET 集合对象 整数集合或字典
REDIS_ZSET 有序集合对象 压缩列表或跳跃表

字符串对象

对于字符串对象,redis中有三种编码方式,一种是int整数编码,一种是embstr编码的SDS实现,还有就是普通的SDS实现。 int整数实现很容易理解,但是embstr编码是什么呢。

embstr其实就是为了很短的字符串对象而存在的。这种编码方式和raw一样,都是使用redisObject结构和sdshdr结构来表示字符串对象,但是raw编码的字符串会调用两次内存分配函数来分别创建这两个对象。而embstr编码的只需要调用一次来申请一块连续的空间,空间中依次包含这两个结构。但是这个embstr的长度不能超过44.这个网上很多人都说embstr大小不能超过39,超过就会变成raw格式,但是我试了一下,并不是如此,在小于等于44的时候都是embstr类型。至于为什么,我认为首先redisObject大小为16字节,其次,在Redis中的内存分配器一次最多分配64字节的数据,这64字节的数据减去redisObject的16字节还有48字节,这48字节需要被sdshdr来分配,为了减少内存使用,在embstr编码下,free和len分别采用两个字节来表示,这个样子的话就是最大44字节。 当然了,这个是我认为的,有理解错误的地方请忽略。

而raw方式编码的字符串就是普通的SDS字符串了。

列表对象

列表对象可以是压缩列表编码 或者是 双端链表编码类型。 压缩列表主要是用在数据量比较少并且数据都很小的地方。

如果满足以下两个条件,列表会使用压缩列表来实现。否则用双端链表。

  • 列表对象保存的所有字符串元素都小于64字节
  • 列表中的元素个数小于512个

ziplist类型的列表:

linkedlist类型的列表:

哈希对象

哈希对象的底层实现可以是ziplist编码或者hashtable编码。

ziplist

在使用压缩列表作为哈希对象的底层实现的时候,每当有键值对插入的时候,会首先把键插入到压缩列表表尾,然后在将值插入到压缩列表表尾。因此同一键值对的两个节点总是紧紧挨在一起,保存键的节点在前,保存值的节点在后。

ziplist类型的哈希对象

hashtable

如果是使用hashtable来作为哈希对象的底层实现的话,哈希对象每个键值对都是用一个字典键值对来保存:

  • 字典的每个键都是一个字符串对象,对象中保存了键值对的键
  • 字典中每个值都是一个字符串对象,对象中保存了键值对的值。

既然有两种实现,那么什么时候会使用压缩列表,又是什么时候使用hashtable呢。当哈希对象同时满足以下两个条件的时候使用ziplist编码

  • 哈希对象的所有键值对的键和值的字符串长度都小于64字节
  • 哈希对象保存的键值对小于512个。

如果不满足以上两个条件,会转换成hashtable来实现。

集合对象

集合对象的编码可以是intset或者hashtable。
intset编码的集合对象底层使用整数集合来实现。集合中的所有元素都被保存在整数集合中。
hashtable编码的集合对象使用字典来实现。字典的每个键是一个字符串对象,字符串就是一个集合元素。而字典的值全部被设置为NULL。

intset编码的集合对象

hashtable编码的集合对象

编码转换

  • 所有的元素都是整数
  • 集合中的元素小于512个。

满足以上两种条件使用intset来实现。否则就会转换成hashtable编码。

有序集合对象

对象编码可以是ziplist或者skiplist

使用ziplist的时候,每个元素使用两个紧挨在一起的节点来表示,第一个节点保存元素成员,第二个保存分值。 采用从小到大的排序方式,第一个元素最小,越往后越大。

使用skiplist编码的有序集合对象使用zset结构作为底层实现。一个zset结构同时包含一个字典和一个跳跃表。

1
2
3
4
typedef struct zset{
zskiplist *zsl;
dict *dict;
}

zset结构中的zsl跳跃表按照分值从小到大保存了集合中的所有元素。每个跳跃表节点保存了一个结合元素。 除此之外,还使用了一个dict字典为有序集合创建了一个从成员到分值的映射。字典中的每个键值对都保存了一个集合元素:字典键保存了元素成员,值保存了元素成员的分值。通过这个字典可以使用O(1)时间就能找到给定成员的分值。

这里要说明一点,虽然zset同时使用跳跃表和字典来保存有序集合元素,但是两种数据结构都通过指针来共享相同的元素成员,所以不会产生重复保存成员和分值一说,因此内存浪费也就不多了。只是多了一个维护hashtable的内存而已。

编码转换

  • 有序集合保存的对象元素数量少于128个。
  • 有序集合保存的所有元素成员的长度都笑64字节。

满足以上两个条件的有序集合对象会使用ziplist编码,否则使用skiplist编码。

类型检查与命令多态

Redis中的命令基本上可以分为两类,一类是可以对任意类型的键执行,比如del,expire命令等。一种是只能对特定的键执行,比如set,hdel,rpush等命令。如果在对应类型的对象上执行了不属于此对象的命令,则会报错。

而这个类型检查命令就是根据redisObject结构的type属性来实现的。Redis会在执行一个特定类型的命令之前,会先检查数据库键对象的值对象是否为执行命令所需要的类型,如果是则执行命令,如果不是,会返回一个类型错误。这里检查是否是对应的类型其实就是检查redisObject结构中的type属性是否是对应的类型.

上面说过,在Redis中有5种对象,redisObject使用type属性类保证对应类型有命令。但是每种类型的对象还有不止一种编码方式,在这里还必须要针对不同的编码方式使用不同的操作函数。在这里Redis就会根据底层的编码不同,也就是redisObject中的encoding不同来调用不同的函数来处理。

下图展示了对键key执行LPOP命令的完整过程

内存回收

因为c语言并不具备自动内存回收功能,并且在redis中有时候会出现两个指针指向同一个对象的情况,比如在zskiplist实现的有序集合中,skiplist与hashtable共享成员对象。 这个样子导致在删除操作的时候并不能直接释放掉相应键值对的内存。所以redis实现了基于引用计数计数实现的内存回收机制。通过这一个机制,程序会在适当的时候释放对象并进行内存回收。

这一实现是根据redisObject中的refcount属性来实现的。每新创建一个对象,refcount就被初始化1,当对象被一个新程序所引用的时候,引用数会加1,每当对象不再被新程序引用时候,会减1,当引用计数变为0的时候,对象所占用的内存会被释放。

对象共享

对象共享会极大的节约空间,Redis在初始化服务器的时候会创建10000个字符串对象:0-9999用来共享。

Redis只提供了数字类型的字符串对象的共享实现,因为比较数字的时间是O(1),而其他如果要比较对象的话会很吃cpu和时间,因此不提供其他种类对象的共享。

对象的空转时长

redisObject结构中有一个属性为lru属性,该属性记录了该对象最后一次被访问的时间。 这个属性主要用于内存回收。 在服务器打开了maxmemory选项,并且服务器用于回收内存的算法为lru算法时候,如果服务器内存超过了maxmemory所设置的上限值,那么就会回收掉空转时长比较高的那部分键值对。 这里又涉及到一个 内存淘汰机制。 因为内存是有限的,因此会为Redis所使用的内存做一个限制,以免内存爆炸系统假死。因此设置了最大内存之后,如果又有新来的数据怎么办,这就是内存淘汰机制了。 而这个内存淘汰机制就是通过这个lru来实现的。 就是通过随机选取N个Key ,然后随便找到这个N个key中空转时长最长的key,然后释放掉内存。然后在进行添加。

还有一种算法是LFU算法。 这个算法还会根据最近访问的频率来进行排序,就是如果访问时间距离现在比较长,但是特别频繁,也不会选择释放这个对象。