天天看点

Redisbook学习笔记(3)数据类型之对象处理机制

在Redis 的命令中,用于对键(key)进行处理的命令占了很大一部分,而对于键所保存的值的

类型(后简称“键的类型” ),键能执行的命令又各不相同。

比如说,LPUSH 和LLEN 只能用于列表键,而SADD 和SRANDMEMBER 只能用于集合

键,等等。

另外一些命令,比如DEL 、TTL 和TYPE ,可以用于任何类型的键,但是,要正确实现这些

命令,必须为不同类型的键设置不同的处理方式:比如说,删除一个列表键和删除一个字符串

键的操作过程就不太一样。

以上的描述说明,Redis 必须让每个键都带有类型信息,使得程序可以检查键的类型,并为它

选择合适的处理方式。

另外,在前面介绍各个底层数据结构时有提到,Redis 的每一种数据类型,比如字符串、列表、

有序集,它们都拥有不只一种底层实现(Redis 内部称之为编码,encoding),这说明,每当对

某种数据类型的键进行操作时,程序都必须根据键所采取的编码,进行不同的操作。

比如说,集合类型就可以由字典和整数集合两种不同的数据结构实现,但是,当用户执行

ZADD 命令时,他/她应该不必关心集合使用的是什么编码,只要Redis 能按照ZADD 命令的

指示,将新元素添加到集合就可以了。

这说明,操作数据类型的命令除了要对键的类型进行检查之外,还需要根据数据类型的不同编

码进行多态处理。

为了解决以上问题,Redis 构建了自己的类型系统,这个系统的主要功能包括:

redisObject 对象。

基于redisObject 对象的类型检查。

基于redisObject 对象的显式多态函数。

对redisObject 进行分配、共享和销毁的机制。

以下小节将分别介绍类型系统的这几个方面。

Note: 因为C 并不是面向对象语言,这里将redisObject 称呼为对象一是为了讲述的方便,

二是希望通过模仿OOP 的常用术语,让这里的内容更容易被理解,redisObject 实际上是只

是一个结构类型。

redisObject 数据结构,以及Redis 的数据类型

redisObject 是Redis 类型系统的核心,数据库中的每个键、值,以及Redis 本身处理的参数,

都表示为这种数据类型。

redisObject 的定义位于redis.h :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

<code>/*</code>

<code>* Redis 对象</code>

<code>*/</code>

<code>typedef</code> <code>struct</code> <code>redisObject {</code>

<code>// 类型</code>

<code>unsigned type:4;</code>

<code>// 对齐位</code>

<code>unsigned notused:2;</code>

<code>// 编码方式</code>

<code>unsigned encoding:4;</code>

<code>// LRU 时间(相对于server.lruclock)</code>

<code>unsigned lru:22;</code>

<code>// 引用计数</code>

<code>int</code> <code>refcount;</code>

<code>// 指向对象的值</code>

<code>void</code> <code>*ptr;</code>

<code>} robj;</code>

type 、encoding 和ptr 是最重要的三个属性。

type 记录了对象所保存的值的类型,它的值可能是以下常量的其中一个(定义位于redis.h):

<code>* 对象类型</code>

<code>#define REDIS_STRING 0 // 字符串</code>

<code>#define REDIS_LIST 1 // 列表</code>

<code>#define REDIS_SET 2 // 集合</code>

<code>#define REDIS_ZSET 3 // 有序集</code>

<code>#define REDIS_HASH 4 // 哈希表</code>

encoding 记录了对象所保存的值的编码,它的值可能是以下常量的其中一个(定义位于

redis.h):

<code>* 对象编码</code>

<code>#define REDIS_ENCODING_RAW 0 // 编码为字符串</code>

<code>#define REDIS_ENCODING_INT 1 // 编码为整数</code>

<code>#define REDIS_ENCODING_HT 2 // 编码为哈希表</code>

<code>#define REDIS_ENCODING_ZIPMAP 3 // 编码为zipmap</code>

<code>#define REDIS_ENCODING_LINKEDLIST 4 // 编码为双端链表</code>

<code>#define REDIS_ENCODING_ZIPLIST 5 // 编码为压缩列表</code>

<code>#define REDIS_ENCODING_INTSET 6 // 编码为整数集合</code>

<code>#define REDIS_ENCODING_SKIPLIST 7 // 编码为跳跃表</code>

ptr 是一个指针,指向实际保存值的数据结构,这个数据结构由type 属性和encoding 属性决

定。

举个例子, 如果一个redisObject 的type 属性为REDIS_LIST , encoding 属性为

REDIS_ENCODING_LINKEDLIST ,那么这个对象就是一个Redis 列表,它的值保存在一个双

端链表内,而ptr 指针就指向这个双端链表;

另一方面, 如果一个redisObject 的type 属性为REDIS_HASH , encoding 属性为

REDIS_ENCODING_ZIPMAP ,那么这个对象就是一个Redis 哈希表,它的值保存在一个zipmap

里,而ptr 指针就指向这个zipmap ;诸如此类。

下图展示了redisObject 、Redis 所有数据类型、以及Redis 所有编码方式(底层实现)三者

之间的关系:

<a href="http://s3.51cto.com/wyfs02/M02/12/17/wKiom1L42DPwvSEaAAF7ivDMBUg512.jpg" target="_blank"></a>

这个图展示了Redis 各种数据类型,以及它们的编码方式。

Note: REDIS_ENCODING_ZIPMAP 没有出现在图中,因为从Redis 2.6 开始,它不再是任何数

据类型的底层结构。

命令的类型检查和多态

有了redisObject 结构的存在,在执行处理数据类型的命令时,进行类型检查和对编码进行多

态操作就简单得多了。

当执行一个处理数据类型的命令时,Redis 执行以下步骤:

1. 根据给定key ,在数据库字典中查找和它像对应的redisObject ,如果没找到,就返回

NULL 。

2. 检查redisObject 的type 属性和执行命令所需的类型是否相符,如果不相符,返回类

型错误。

3. 根据redisObject 的encoding 属性所指定的编码,选择合适的操作函数来处理底层的

数据结构。

4. 返回数据结构的操作结果作为命令的返回值。

作为例子,以下展示了对键key 执行LPOP 命令的完整过程:

<a href="http://s3.51cto.com/wyfs02/M00/12/17/wKiom1L42Imy4-ZLAAGu7U_h9E8702.jpg" target="_blank"></a>

对象共享

有一些对象在Redis 中非常常见,比如命令的返回值OK 、ERROR 、WRONGTYPE 等字符,另外,

一些小范围的整数,比如个位、十位、百位的整数都非常常见。

为了利用这种常见情况,Redis 在内部使用了一个Flyweight 模式:通过预分配一些常见的值

对象,并在多个数据结构之间共享这些对象,程序避免了重复分配的麻烦,也节约了一些CPU

时间。

Redis 预分配的值对象有如下这些:

各种命令的返回值,比如执行成功时返回的OK ,执行错误时返回的ERROR ,类型错误时

返回的WRONGTYPE ,命令入队事务时返回的QUEUED ,等等。

包括0 在内, 小于redis.h/REDIS_SHARED_INTEGERS 的所有整数

(REDIS_SHARED_INTEGERS 的默认值为10000)

因为命令的回复值直接返回给客户端,所以它们的值无须进行共享;另一方面,如果某个命令

的输入值是一个小于REDIS_SHARED_INTEGERS 的整数对象,那么当这个对象要被保存进数据

库时,Redis 就会释放原来的值,并将值的指针指向共享对象。

作为例子,下图展示了三个列表,它们都带有指向共享对象数组中某个值对象的指针:

<a href="http://s3.51cto.com/wyfs02/M02/12/17/wKioL1L42JjRFnk3AAB-yp0St_A115.jpg" target="_blank"></a>

三个列表的值分别为:

列表A :[20130101, 300, 10086] ,

列表B :[81, 12345678910, 999] ,

列表C :[100, 0, -25, 123] 。

Note: 共享对象只能被带指针的数据结构使用。

需要提醒的一点是,共享对象只能被字典和双端链表这类能带有指针的数据结构使用。

像整数集合和压缩列表这些只能保存字符串、整数等字面值的内存数据结构,就不能使用共享

对象。

引用计数以及对象的销毁

当将redisObject 用作数据库的键或者值,而不是用来储存参数时,对象的生命期是非常长

的,因为C 语言本身没有自动释放内存的相关机制,如果只依靠程序员的记忆来对对象进行追

踪和销毁,基本是不太可能的。

另一方面,正如前面提到的,一个共享对象可能被多个数据结构所引用,这时像是“这个对象被

引用了多少次? ”之类的问题就会出现。

为了解决以上两个问题,Redis 的对象系统使用了引用计数技术来负责维持和销毁对象,它的

运作机制如下:

每个redisObject 结构都带有一个refcount 属性,指示这个对象被引用了多少次。

当新创建一个对象时,它的refcount 属性被设置为1 。

当对一个对象进行共享时,Redis 将这个对象的refcount 增一。

当使用完一个对象之后,或者取消对共享对象的引用之后,程序将对象的refcount 减

一。

当对象的refcount 降至0 时,这个redisObject 结构,以及它所引用的数据结构的内

存,都会被释放。

小结

Redis 使用自己实现的对象机制来实现类型判断、命令多态和基于引用计数的垃圾回收。

一种Redis 类型的键可以有多种底层实现。

Redis 会预分配一些常用的数据对象,并通过共享这些对象来减少内存占用,和避免频繁

地为小对象分配内存。

本文转自shayang8851CTO博客,原文链接:http://blog.51cto.com/janephp/1357861,如需转载请自行联系原作者