前面分析了Redis底层的几种基本数据结构,但是这些数据结构在redis中并不是直接使用的,而是封装成redis的数据类型(redisObject)来提供给系统使用。
Redis的键值对可以保存不同类型的值,因此需要对键值的类型进行检查并根据不同类型进行多态处理。 为了基于类型的操作更加方便,Redis创建了自己的类型系统,主要包括:字符串、哈希表、列表、集合 和有序集五种类型,这些类型都是基于对前面的基本数据结构的封装实现(Redis内部称为编码,encoding)的,且同一种数据类型可能有多种底层实现。 因此在对redis的数据进行操作时需要检查数据类型,并且根据其不同的底层实现执行不同的多态处理。这也就要求Redis的每个键都带有类型信息,是的程序可以检查键的类型,并为它选择合适的处理方式。
为了解决以上问题,Redis创建了自己的类型系统,这个系统的主要功能包括:
* redisObject对象
*基于redisObject对象的类型检查
*基于redisObject对象的显示多态函数
*对redisObject进行分配,共享和销毁的机制
redisObject是Redis类型系统的核心,数据库中的每个键、值、以及Redis本身处理的参数。都表示为这种数据类型。
Note:C并不是面向对象语言,这里使用将redisObject称为对象是为了讲述的方便,同时通过OOP术语,也便于理解,redisObject实际上只是一个结构类型。
redisObject定义在redis.h中:
/* A redis object, that is a type able to hold a string / list / set */
/* The actual Redis Object */
//redis对象包括了五种类型:字符串 列表 集合 有序集 哈希表,而每种类型
//有自己的底层实现的数据结构(即编码方式)(一种类型可能有多种底层实现方式)
#define REDIS_LRU_BITS 24
#define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 1 /* LRU clock resolution in seconds */
typedef struct redisObject {
unsigned type:4;//类型
unsigned encoding:4;//编码方式
unsigned lru:REDIS_LRU_BITS;//LRU时间(相对于server.lrulock) /* lru time (relative to server.lruclock) */
int refcount;//引用计数
void *ptr;//指向对象的值
} robj;
注:redisObject结构的ptr指向实际保存值的数据结构,这个数据结构由type属性和encoding属性决定。 例如: 如果一个
redisObjec的type属性为REDIS_LIST,encoding属性为REDIS_ENCODING_LINKEDLIST,那么这个对象的类型就是一个Redis列表,他保存的值在一个双链表内,而ptr指针就指向这个双端链表。
<span style="background-color: rgb(51, 204, 255);"></span>
/* Object types */
//对象类型
#define REDIS_STRING 0 //字符串
#define REDIS_LIST 1 //列表
#define REDIS_SET 2 //集合
#define REDIS_ZSET 3 //有序集
#define REDIS_HASH 4 //哈希表
/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */
//enconding记录了对象所保存值的编码
#define REDIS_ENCODING_RAW 0 //编码为字符串 /* Raw representation */
#define REDIS_ENCODING_INT 1 //编码为整数 /* Encoded as integer */
#define REDIS_ENCODING_HT 2 //编码为zipmap /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 //编码为双端链表/* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 //编码为压缩列表/* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6 //编码为整数集合 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 //编码为跳跃表/* Encoded as skiplist */
RedisObject、Redis的所有数据类型 以及Redis类型所有的编码方式(底层实现)三者之间的关系如下图:
Note:REDIS_ENCODING_ZIPMAP从redis2.6开始,已经不再是任何数据类型的底层结构。
命令的类型检查和多态:
当执行一个处理数据类型的命令时,Redis执行以下步骤:
1、根据给定的key,在数据库字典中查找和它对应的redisObject,如果没有找到则返回NULL
2、检查redisObject的type的属性和执行命令所需的类型是否相符,不符,则返回类型错误
3、根据redisObject的encoding属性所指定的编码,选择合适的操作函数来处理底层数据结构
4、返回数据结构的操作结果作为返回值。
例:一个对key执行LPOP命令的完整过程如下:
对象共享:
一些对象在Redis中非常常见,如命令返回值:OK ERROR WRONGTYPE等字符,以及一些小范围的整数。对于这些常见对象,Redis在内部使用了Flyweight模式: 预分配一些常见值的对象,程序避免重复分配麻烦,也节约了一些CPU时间。
Redis预分配的值对象有:
* 各种命令的返回值:OK ERROR WRONGTYPE 等。
*包括0在内的,小于redis.h/REDIS_SHARED_INTEGERS(10000)的所有整数值。
因为命令的反复值都是直接返回给客户端,其值不需要共享; 而对于较小的整数值,如果某命令的输入值是一个小于REDIS_SHARED_INTEGERS的整数对象,那么当这个对象被保存进数据库时,Redis就会释放原来的值,并将值的指针指向共享对象。
例如: 三个列表,他们都带有指向共享对象数组中某个值对象的指向:
则三个列表的值分别为:
列表A:[20120101,300,10086]
列表B:[81,1234567890,999]
列表C:[100,0,-25,123]
需要注意的是:共享对象只能被带指针的数据结构使用。 如字典,双端链表都带有指针,而像整数集合和压缩列表这些只能保存字符串、整数等字面值的内存数据结构就不能使用共享对象。
引用计数及对象的销毁:
当将redisObject用作数据库的键或值而不是作为存储参数时,其生命周期是非常长的,而C语言本身没有释放内存的机制,如果只依靠程序员的记忆来对对象进行追踪和销毁基本是不可能的。 另外,对于共享对象,可能被多次引用,因此关于被引用了多少次也是需要考虑的问题。
Redis对象系统使用了引用计数来负责维持和销毁对象:
每个对象结构都有一个ref引用计数,当计数递减为0时,这个redisObject结构以及它所引用的底层数据结构的内存都会被释放。
小结:
*Redis使用自己的实现的对象机制来实现类型判断、命令多态和基于引用计数的垃圾回收
*一种Redis类型可以有多种底层实现
* Redis会预分配一些常用的数据对象,并通过共享这些对象来减少内存占用,和避免频繁的为小对象分配内存。