由于 redis 需要对每一个 key 产生不同的操作,所以Redis 必须让每个键都带有类型信息,使得程序可以检查键的类型,并为它选择合适的处理方式
为了解决以上问题, Redis 构建了自己的类型系统,这个系统的主要功能包括:
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):
/*对象类型 type*/
#define REDIS_STRING 0 // 字符串
#define REDIS_LIST 1 // 列表
#define REDIS_SET 2 // 集合
#define REDIS_ZSET 3 // 有序集
#define REDIS_HASH 4 // 哈希表
/*
对象编码
*/
#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 指针就指向这个双端链表;
另一方面,如果一个 redisObject 的 type 属性为 REDIS_HASH , encoding 属性为 REDIS_ENCODING_ZIPMAP ,那么这个对象就是一个 Redis 哈希表,它的值保存在一个 zipmap 里,而 ptr 指针就指向这个 zipmap .
对应关系:
一个 Key 的执行过程:
Demo: LPOP 的完整的执行过程
命令的返回值 OK 、 ERROR 、 WRONGTYPE 等常见的字符情况, Redis 在内部使用了一个 Flyweight 模式 :通过预分配一些常见的值
对象,并在多个数据结构之间共享这些对象,程序避免了重复分配的麻烦,也节约了一些 CPU 时间。(怎么样预分配,节约了 CPU 时间,单例或者静态模式?
)
Redis 预分配的值对象有如下这些:
因为命令的回复值直接返回给客户端,所以它们的值无须进行共享;另一方面,如果某个命令的输入值是一个小于 REDIS_SHARED_INTEGERS 的整数对象,那么当这个对象要被保存进数据库时, Redis 就会释放原来的值,并将值的指针指向共享对象。
Redis 的对象系统使用了引用计数技术来负责维持和销毁对象,它的运作机制如下:
字符串类型分别使用 REDIS_ENCODING_INT 和 REDIS_ENCODING_RAW 两种编码:
哈希表有两种实现方式,压缩列表(REDIS_ENCODING_ZIPLIST)和 字典(REDIS_ENCODING_HT)
创建空白哈希表时,程序默认使用 REDIS_ENCODING_ZIPLIST 编码,当以下任何一个条件被满足时,程序将编码从切换为 REDIS_ENCODING_HT :
使用 REDIS_ENCODING_ZIPLIST 和 REDIS_ENCODING_LINKEDLIST 这两种方式编码:
创建新列表时 Redis 默认使用 REDIS_ENCODING_ZIPLIST 编码,当以下任意一个条件被满足时,列表会被转换成 REDIS_ENCODING_LINKEDLIST 编码:
BLPOP 、 BRPOP 和 BRPOPLPUSH 三个命令都可能造成客户端被阻塞,阻塞原语并不是一定会造成客户端阻塞:
执行的过程和原理:
阻塞一个客户端需要执行以下步骤:
步骤 2 是将来解除阻塞的关键, server.db[i]->blocking_keys 是一个字典,字典的键是那些造成客户端阻塞的键,而字典的值是一个链表,链表里保存了所有因为这个键而被阻塞的客户端(被同一个键所阻塞的客户端可能不止一个):
在上图展示的 blocking_keys 例子中, client2 、 client5 和 client1 三个客户端就正被 key1 阻塞,而其他几个客户端也正在被别的两个 key 阻塞。
脱离阻塞的方式:
通过将新元素推入造成客户端阻塞的某个键中,可以让相应的客户端从阻塞状态中脱离出来(取消阻塞的客户端数量取决于推入元素的数量).
LPUSH 、 RPUSH 和 LINSERT 这三个添加新元素到列表的命令, 在底层都由一个 pushGenericCommand 的函数实现,这个函数的运作流程如下图
当向一个空键推入新元素时, pushGenericCommand 函数执行以下两件事:
readyList 结构的定义如下:
typedef struct readyList {
redisDb *db;
robj *key;
} readyList;
readyList 结构的 key 属性指向造成阻塞的键,而 db 则指向该键所在的数据库。
假设某个非阻塞客户端正在使用 0 号数据库,而这个数据库当前的 blocking_keys 属性的值如下:
如果此时执行 PUSH key3 value 命令,那么 pushGenericCommand 将创建一个 db 属性指向 0 号数据库、 key 属性指向 key3 键对象的 readyList 结构,并将它添加到服务器 server.ready_keys 属性的链表中:
它 使 用 REDIS_ENCODING_INTSET 和 REDIS_ENCODING_HT 两种方式编码
一个添加到集合的元素,决定了创建集合时所使用的编码:
如果一个集合使用 REDIS_ENCODING_INTSET 编码,那么当以下任何一个条件被满足时,这个 集合会被转换成 REDIS_ENCODING_HT 编码:
字典编码
当使用 REDIS_ENCODING_HT 编码时,集合将元素保存到字典的键里面,==而字典的值则统一设 为 NULL ==。(为啥??) 作为例子,以下图片展示了一个以 REDIS_ENCODING_HT 编码表示的集合,集合的成员为 elem1 、 elem2 和 elem3
使 用 REDIS_ENCODING_ZIPLIST 和 REDIS_ENCODING_SKIPLIST 两种方式编码。
如果第一个元素符合以下条件的话,就创建一个 REDIS_ENCODING_ZIPLIST 编码的有序集:
否则,程序就创建一个 REDIS_ENCODING_SKIPLIST 编码的有序集。如果元素在增加的过程中,不满足上面的任意一个条件,则会转化成 REDIS_ENCODING_SKIPLIST
SKIPLIST 编码的有序集
/*
* 有序集
*/
typedef struct zset {
// 字典
dict *dict;
// 跳跃表
zskiplist *zsl;
} zset;
zset 同时使用字典和跳跃表两个数据结构来保存有序集元素。 其中,元素的成员由一个 redisObject 结构表示,而元素的 score 则是一个 double 类型的浮点数,字典和跳跃表两个结构通过将指针共同指向这两个值来节约空间(不用每个元素都复制两份)
下图展示了一个 REDIS_ENCODING_SKIPLIST 编码的有序集: