《redis设计与实现》——对象

本节介绍五种类型的对象、对象对应的编码和相应的底层实现

一、对象的类型和编码

  • Redis 使用对象来表示数据库中的键和值。当我们在Redis中新创建一个键值对时,我们至少创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。

  • 每一个对象都由一个redisObject结构表示:

  • typedef struct redisObject {
        // 类型
        unsigned type:4;
        
        // 编码
        unsigned encoding:4;
        
        // 指向底层实现数据结构的指针
        void *ptr;
        
        // ...
        
    } robj;
    

五种对象类型以及TYPE命令输出

对象 对象type属性的值 TYPE命令的输出
字符串对象 REDIS_STRING "string"
列表对象 REDIS_LIST "list"
哈希对象 REDIS_HASH "hash"
集合对象 REDIS_SET set
有序集合对象 REDIS_ZSET "zset"

对象的编码

encoding属性记录了对象使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现。

编码常量 编码对应的底层数据结构
REDIS_ENCODING_INT long类型的整数
REDIS_ENCODING_EMBSTR enbstr编码的简单动态字符串
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_ENBSTR 使用embstr编码的简单动态字符串实现的字符串对象
REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串的对象
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用双端链表实现的列表对象
REDIS_HSAH 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命令可以查看一个数据库的键的值对象的编码。

> OBJECT ENCODING key_name

二、字符串对象

字符串对象的编码可以是int、raw或者enbstr。

如果一个字符串对象保存到的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void* 转换成long)并将字符串对象的编码设置为int。

如果字符串对象保存的是一个字符串值,并且字符串长度大于39字节,那么字符串对象使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。

如果字符串对象保存的是一个字符串值,并且这个字符串的长度小于39字节,那么字符串对象将使用embstr编码的方式来保存这个字符串的值。

使用embstr编码的字符串对象保存短字符串好处:

  • 创建字符串对象所需内存分配数从raw编码的两次降低为一次。释放对象也只需调用一次内存释放函数。
  • 所有数据都保存在一块连续的内存里面,能够更好地利用缓存带来的优势。

编码转换

  • 对int编码的字符串执行一些命令使这个对象报保存的不再是整数值,而是一个字符串值,那么字符串对象的编码将从int变成raw。
  • embstr编码的字符串对象是只读的。当我们对embstr编码的字符串对象执行任何修改命令时,程序会先将对象的编码从embstr转换成raw,然后在执行修改命令。这也是为什么int编码的字符串对象进行编码转换只能转换成raw编码的字符串对象。

三、列表对象

列表对象的编码可以是ziplist或者linkedlist。

linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。

这种嵌套字符串对象的行为在后面介绍的哈希对象、集合对象和有序集合对象中都会出现,字符串对象是redis五种类型的对象唯一一种会被其他四种对象嵌套的对象。

编码转换

同时满足以下两个条件,列表对象使用ziplist编码:

  • 列表对象保存的所有字符串元素长度都小于64字节;
  • 列表对象保存的元素数量小于512个;
  • 不能满足这两个条件的列表对象需要使用linkedlist编码。

四、哈希对象

哈希对象的编码可以是ziplist和hashtable。

ziplist编码的哈希对象

  • 先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。两个节点紧挨在一起,键节点在前,值节点在后。
  • 尾插法。

hashtable编码的哈希对象使用字典作为底层实现

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

编码转换:

满足以下两个条件,哈希对象使用ziplist编码:

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
  • 哈希对象保存的键值对数量小于512个;
  • 不能满足这两个条件的哈希对象需要使用hashtable编码。

五、集合对象

集合对象的编码可以是intset或者hashtable。

编码转换

满足以下两个条件,对象使用intset编码:

  • 集合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量不超过512个;
  • 不能满足这两个条件的集合对象需要使用hashtable编码。

六、有序集合对象

有序集合的编码可以是ziplist或者skiplist。

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

typedef struct zset {
    zskiplist *zsl;
    dict *dict;
} zset;

为什么有序集合需要同时使用跳跃表和字典来实现?

  • 使用跳跃表:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的。
  • 使用字典:程序可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的。
  • 须知:在实际中,字典和跳跃表会共享元素的成员和分值,所以并不会造成任何数据重复,也不会因此浪费任何内存。

编码转换

同时满足以下两个条件,对象使用ziplist编码:

  • 有序集合保存的元素数量小于128个;
  • 有序集合保存的所有元素成员的长度都小于64字节。
  • 不满足以两个条件的有序集合对象都将使用skiplist编码。

七、类型检查与命令多态

Redis用于操作键的命令基本可以分为两种类型:

  • 命令可以对任何类型的键执行。比如DEL命令、EXPIRE命令、RENAME命令、TYPE命令、OBJECT命令等。

  • 另一种命令只能对特定类型的键执行,比如说:

    SET, GET, APPEND, STRLEN等命令只能对字符串键执行;
    ...
    

类型检查

在执行一个类型特定的命令之前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令。

类型检查:redisObject.type == target_type ?

多态命令的实现

Redis会根据值对象的编码方式,选择正确的命令实现代码来执行命令。

比如:只要执行LLEN命名的是列表键,那么无论值对象使用的是ziplist还是linkedlist编码,命令都可以正常实行。

DEL、EXPIRE等命令和LLEN等命令的区别在于,前者是基于类型的多态——一个命令可以同时处理不同类型的键,而后者是基于编码的多态——一个命令可以同时处理多种不同编码。

八、内存回收

因为C语言并不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制。

typedef struct redisObject {
    // ...
    // 引用计数
    // ...
} robj;

九、对象共享

除了用于实现引用计数内存回收机制之外,对象的引用计数属性还带有对象共享的作用。

实现机制

  • 将数据库键的值指针指向一个现有的值对象;
  • 将被共享的值对象的引用计数增一。

共享对象机制对于节约内训非常有帮助,数据库中保存的相同值越多,对象共享机制就越能节约越多的内存。

为什么Redis不共享包含字符串的对象?

  • 一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同所需的复杂度就会越高,消耗CPU时间也会越多。
  • 如果共享对象是保存整数值的字符串对象,那么验证操作的复杂度为O(1);
  • 如果共享对象是保存字符串值的字符串对象,那么验证操作的复杂度为O(N);
  • 受到CPU时间的限制,Redis只对包含整数值的字符串对象进行共享。
  • 目前来书,Redis在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。

十、对象的空转时长

除了前面介绍过的type、encoding、ptr和refcount四个属性之外,redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一i此被命令程序访问的时间:

typedef struct redisObject {
    // ...
    unsigned lru:22;
    // ...
}robj;

空转时长就是当前时间减去键的值对象的lru时间计算得出的。

你可能感兴趣的:(《redis设计与实现》——对象)