对象处理机制
Redis 构建了自己的类型系统,这个系统的主要功能包括:
• redisObject 对象。
• 基于redisObject 对象的类型检查。
• 基于redisObject 对象的显式多态函数。
• 对redisObject 进行分配、共享和销毁的机制。
(redisObject 实际上是只是一个结构类型。)
redisObject 的定义位于redis.h :
/*
* Redis 对象
*/
typedef struct redisObject {
// 类型
unsigned type:4;
// 对齐位
unsigned notused:2;
// 编码方式
unsigned encoding:4;
// LRU 时间(相对于server.lruclock)
unsigned lru:22;
// 引用计数
int refcount;
// 指向对象的值
void *ptr;
} robj;
type 、encoding 和ptr 是最重要的三个属性。
type 记录了对象所保存的值的类型,它的值可能是以下常量的其中一个(定义位于redis.h):
/*
* 对象类型
*/
#define REDIS_STRING 0 // 字符串
#define REDIS_LIST 1 // 列表
#define REDIS_SET 2 // 集合
#define REDIS_ZSET 3 // 有序集
#define REDIS_HASH 4 // 哈希表
encoding 记录了对象所保存的值的编码,它的值可能是以下常量的其中一个(定义位于redis.h):
/*
* 对象编码
*/
#define REDIS_ENCODING_RAW 0 // 编码为字符串
#define REDIS_ENCODING_INT 1 // 编码为整数
#define REDIS_ENCODING_HT 2 // 编码为哈希表
#define REDIS_ENCODING_ZIPMAP 3 // 编码为zipmap
#define REDIS_ENCODING_LINKEDLIST 4 // 编码为双端链表
#define REDIS_ENCODING_ZIPLIST 5 // 编码为压缩列表
#define REDIS_ENCODING_INTSET 6 // 编码为整数集合
#define REDIS_ENCODING_SKIPLIST 7 // 编码为跳跃表
ptr 是一个指针,指向实际保存值的数据结构,这个数据结构由type 属性和encoding 属性决定。
举个例子, 如果一个redisObject 的type 属性为REDIS_LIST , encoding 属性为REDIS_ENCODING_LINKEDLIST ,那么这个对象就是一个Redis 列表,它的值保存在一个双端链表内,而ptr 指针就指向这个双端链表;
命令的类型检查和多态
当执行一个处理数据类型的命令时,Redis 执行以下步骤:
1. 根据给定key ,在数据库字典中查找和它像对应的redisObject ,如果没找到,就返回NULL 。
2. 检查redisObject 的type 属性和执行命令所需的类型是否相符,如果不相符,返回类型错误。
3. 根据redisObject 的encoding 属性所指定的编码,选择合适的操作函数来处理底层的数据结构。
4. 返回数据结构的操作结果作为命令的返回值。
对象共享
有一些对象在Redis 中非常常见,比如命令的返回值OK 、ERROR 、WRONGTYPE 等字符,另外,一些小范围的整数,比如个位、十位、百位的整数都非常常见。
为了利用这种常见情况,Redis 在内部使用了一个Flyweight 模式:通过预分配一些常见的值对象,并在多个数据结构之间共享这些对象,程序避免了重复分配的麻烦,也节约了一些CPU时间。
Redis 预分配的值对象有如下这些:
• 各种命令的返回值,比如执行成功时返回的OK ,执行错误时返回的ERROR ,类型错误时返回的RONGTYPE ,命令入队事务时返回的QUEUED ,等等。
• 包括0 在内, 小于redis.h/REDIS_SHARED_INTEGERS 的所有整数(REDIS_SHARED_INTEGERS 的默认值为10000)
Note: 共享对象只能被带指针的数据结构使用。
需要提醒的一点是,共享对象只能被字典和双端链表这类能带有指针的数据结构使用。
像整数集合和压缩列表这些只能保存字符串、整数等字面值的内存数据结构,就不能使用共享
对象。
引用计数以及对象的销毁
Redis 的对象系统使用了引用计数技术来负责维持和销毁对象,它的
运作机制如下:
• 每个redisObject 结构都带有一个refcount 属性,指示这个对象被引用了多少次。
• 当新创建一个对象时,它的refcount 属性被设置为1 。
• 当对一个对象进行共享时,Redis 将这个对象的refcount 增一。
• 当使用完一个对象之后,或者取消对共享对象的引用之后,程序将对象的refcount 减一。
• 当对象的refcount 降至0 时,这个redisObject 结构,以及它所引用的数据结构的内存,都会被释放。
字符串
字符串编码
字符串类型分别使用REDIS_ENCODING_INT 和REDIS_ENCODING_RAW 两种编码:
• REDIS_ENCODING_INT 使用long 类型来保存long 类型值。
• REDIS_ENCODING_RAW 则使用sdshdr 结构来保存sds (也即是char* )、long long 、double 和long double 类型值。
换句话来说,在Redis 中,只有能表示为long 类型的值,才会以整数的形式保存,其他类型的整数、小数和字符串,都是用sdshdr 结构来保存。
编码的选择
新创建的字符串默认使用REDIS_ENCODING_RAW 编码,在将字符串作为键或者值保存进数据库时,程序会尝试将字符串转为REDIS_ENCODING_INT 编码。
哈希表
REDIS_HASH (哈希表) 是HSET 、HLEN 等命令的操作对象, 它使用REDIS_ENCODING_ZIPLIST 和REDIS_ENCODING_HT 两种编码方式:
字典编码的哈希表
当哈希表使用字典编码时,程序将哈希表的键(key)保存为字典的键,将哈希表的值(value)保存为字典的值。
哈希表所使用的字典的键和值都是字符串对象。下图展示了一个包含三个键值对的哈希表:
压缩列表编码的哈希表
当使用REDIS_ENCODING_ZIPLIST 编码哈希表时,程序通过将键和值一同推入压缩列表,从而形成保存哈希表所需的键-值对结构:
新添加的key-value 对会被添加到压缩列表的表尾。
当进行查找/删除或更新操作时,程序先定位到键的位置,然后再通过对键的位置来定位值的位置。
编码的选择
创建空白哈希表时,程序默认使用REDIS_ENCODING_ZIPLIST 编码,当以下任何一个条件被满足时,程序将编码从切换为REDIS_ENCODING_HT :
• 哈希表中某个键或某个值的长度大于server.hash_max_ziplist_value (默认值为64)。
• 压缩列表中的节点数量大于server.hash_max_ziplist_entries (默认值为512 )。
列表
REDIS_LIST (列表) 是LPUSH 、LRANGE 等命令的操作对象, 它使用REDIS_ENCODING_ZIPLIST 和REDIS_ENCODING_LINKEDLIST 这两种方式编码:
编码的选择
创建新列表时Redis 默认使用REDIS_ENCODING_ZIPLIST 编码,当以下任意一个条件被满足时,列表会被转换成REDIS_ENCODING_LINKEDLIST 编码:
• 试图往列表新添加一个字符串值, 且这个字符串的长度超过server.list_max_ziplist_value (默认值为64 )。
• ziplist 包含的节点超过server.list_max_ziplist_entries (默认值为512 )。
阻塞
BLPOP 、BRPOP 和BRPOPLPUSH 三个命令都可能造成客户端被阻塞,以下将这些命令统称为列表的阻塞原语。
阻塞原语并不是一定会造成客户端阻塞:
• 只有当这些命令被用于空列表时,它们才会阻塞客户端。
• 如果被处理的列表不为空的话,它们就执行无阻塞版本的LPOP 、RPOP 或RPOPLPUSH命令。