概述

上一篇文章,总结了Redis用到的所有主要数据结构,但是Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象。

每个对象都用到了底层的至少一种数据结构。通过这五种对象,Redis在执行之前可以根据对象类型判断对象是否可以执行给定的命令。使用对象还有一个好处就是可以针对不同场景设置不同的数据结构实现,优化对象在不同场景下的使用效率。

Redis系统还实现了基于引用计数技术的内存回收机制;还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过多个数据库键共享同一个对象来节约内存。

Redis的对象带有访问时间记录,该信息可用于计算数据库键的空转时长。

接下来介绍Redis对象系统的特性以及具体的每种对象如何实现。

对象的类型与编码

Redis中每个对象由一个redisObject结构表示:

1
2
3
4
5
6
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
void *ptr;
...
}robj;

类型

对象的type属性记录了对象的类型。

类型常量 对象名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

当我们称一个数据库键是“字符串键”时,我们指的是“这个数据库键所对应的值为字符串对象”。TYPE命令的实现方式也与此类似,当我们对一个数据库键执行TYPE命令时,返回的结果为数据库对应的值对象的类型。

编码和底层实现

ptr 指向对象的底层实现数据结构,而这个数据结构由 encoding 表示。

编码常量 编码对应的底层数据结构
REDIS_ENCODING_INT long类型的整数
REDIS_ENCODING_EMNSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典

对象与数据结构的对应关系

字符串对象

字符串的数据结构选择可参考上图,主要介绍embstr。embstr是专门用于保存短字符串的一种优化编码方式。这种编码也使用redisObject和sdshdr来表示字符串对象,不同之处在于raw编码会调用两次内存分配函数来分别创建redisObject和sdshdr结构,而embstr通过调用一次内存分配函数来分配一个连续的空间,包含redisObject和sdshdr两个结构。

使用embstr的好处:

  • 只需一次内存分配(原因是sdshdr较短,可一次分配在一起)
  • 释放时一次释放
  • 所有数据都保存在一个连续空间,能更好的利用缓存(缓存命中率高)

使用long double类型表示的浮点数也是用字符串值来保存的。使用时先将字符串转换为浮点数值,执行操作后在转换为字符串值并继续保存在字符串对象里面。

embstr实际上是只读的,所以如果我们对embstr执行任何修改命令时,程序会先将embstr转换为raw,再进行操作。

列表对象

列表对象可以是ziplist或者linkedlist。
当列表对象保存的所有字符串元素的长度都小于64字节 且 列表对象保存的元素数量小于512个时,使用ziplist编码。
(这两个值可以在配置文件中修改)

在使用ziplist时,当这两个条件有一个不再满足,就会对对象进行编码转换操作。将编码类型转换为linkedlist。

哈希对象

哈希对象的编码可以是ziplist或者hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现,此时每当添加新键值对时,程序现将键压入表尾,再将值压入表尾。

集合对象

集合对象的编码可以是intset或者hashtable。
当集合对象所保存的所有元素都是整数值 且 集合元素数量不超过512个时使用intset,否则使用hashtable。

有序集合对象

有序集合的编码可以是ziplist或者skiplist。
使用ziplist时,每个集合元素用两个挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。按照分值从小到大进行排序。
使用skiplist时,底层结构用zset实现:

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

zset 中持有一个跳跃表 zsl 和一个字典 dict 。

zsl 按照分值大小从小到大保存所有的集合元素,每个跳跃表节点都保存了一个集合元素:object属性保存元素的成员,score属性保存分值。通过跳跃表,可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的。

此外,zset还持有一个 dict 字典为有序集合创建一个从成员到分值的映射。通过这个字典,程序可以用O(1)复杂度查找给定成员的分值。ZSCORE命令就是根据这一特性实现的。

虽然zset同时使用 zsl 和 dict 来保存集合元素,但这两种数据结构都会通过之阵来共享相同元素的成员和分值,所以不会浪费额外的内存。

当有序集合对象保存元素的数量小于128个 且 所有元素成员的程度都小于64字节时,使用ziplist。

对象使用哪种数据结构,基本上取决于保存元素的数量以及每个元素的大小。有些特殊的还取决于元素的类型。这些属性的上限值可以在配置文件中修改。

类型检查与多态

Redis中用于操作键的命令基本上分为两种类型,一种可以对任何类型的键执行,例如DEL、EXPIRE、RENAME、TYPE、OBJECT等命令,另一种命令只能对特定类型的键执行。

在执行一个类型特定命令之前,服务器会先检查输入的键对象是否为执行命令所需的类型,如果是,则执行;否则就拒绝执行并返回一个类型错误。

Redis还会根据值对象的编码方式选择正确的命令实现代码来执行命令。例如列表对象有ziplist和linkedlist两种编码可用,那么我们就使用对应编码的API完成操作。

内存回收

每个对象的引用计数信息有redisObject结构的 refCount 属性记录:

  • 当创建一个新对象时,引用计数的值会被初始化为1;
  • 当对象被一个新程序使用时,它的引用计数会增一;
  • 当对象不再被一个新程序使用时,它的引用计数值会减一;
  • 当对象的引用计数值变为0时,对象所占用的内存会被释放。

对象共享

目前来说,Redis会在初始化服务器时,创建一万个字符串对象。这些对象包含了从0到9999的所有整数值,当服务器要用到这些整数时,服务器就会使用这些共享对象而不是创建新的对象。

对象的空转时长

redisObject中还有一个属性 lru 来记录对象最后一次被命令程序访问的时间,用当前时间减去这个值,就是对象的空转时间。当服务器开启maxmermoy选项时,那么内存超过时,空转时间长的对象会优先被释放。