Redis数据类型
- 字符串
- 哈希表
- 列表
- 集合
- 有序集
对象处理机制
对键能执行的命令各部相同,但有些又是通用的。
Redis 必须让每个键都带有类型信息,使得程序可以检查键的类型,并为它选择合适的处理方式
因为各类型的底层实现(Redis 称为编码,encoding),各不相同,因此程序必须根据键所采取的编码进行不同的操作。
比如说集合类型就可以由字典和整数集合两种不同的数据结构实现。
当用户执行ZADD命令时,应该不必关心结合使用的是什么编码,只要能将新元素添加到集合就可以了。
因此,操作数据类型的命令除了要对键的类型进行检查之外,还需要根据数据类型的不同编码进行多态处理
Redis构建了自己的类型系统
- redisObject 对象
- 基于 redisObject 对象的类型检查
- 基于 redisObject 对象的显式多态函数
- 对 redisObject 进行分配、共享和销毁的机制
redisObject 数据结构
redisObject 是 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_STRING 0 // 字符串
- REDIS_LIST 1 // 列表
- REDIS_SET 2 // 集合
- REDIS_ZSET 3 // 有序集
- REDIS_HASH 4 // 哈希表
- encoding(编码)
- REDIS_ENCODING_RAW 0 // 编码为字符串
- REDIS_ENCODING_INT 1 // 编码为整数
- REDIS_ENCODING_HT 2 // 编码为字典
- REDIS_ENCODING_ZIPMAP 3 // 编码为 zipmap
- REDIS_ENCODING_LINKEDLIST 4 // 编码为双端链表
- REDIS_ENCODING_ZIPLIST 5 // 编码为压缩列表
- REDIS_ENCODING_INTSET 6 // 编码为整数集合
- REDIS_ENCODING_SKIPLIST 7 // 编码为跳跃表
- ptr
- 指向实际保存值得数据结构
- 这个数据结构由type属性 和encoding 属性决定
- 例子
- redisObject 的 type属性为REDIS_LIST,encoding属性为REDIS_ENCODING_LINKEDLIST
- 那么这个对象就是一个Redis列表,它的值保存在一个双端链表内,而ptr指针就指向这个双端链表
- 指向实际保存值得数据结构
命令的类型检查和多态
当执行一个处理数据类型的命令时,Redis执行以下步骤:
- 根据给定 key ,在数据库字典中查找和它像对应的 redisObject ,如果没找到,就返回NULL 。
- 检查 redisObject 的 type 属性和执行命令所需的类型是否相符,如果不相符,返回类型错误。
- 根据 redisObject 的 encoding 属性所指定的编码,选择合适的操作函数来处理底层的数据结构。
- 返回数据结构的操作结果作为命令的返回值。
对键key执行LPOP命令的完整过程:
对象共享
Redis 在内部使用了一个 Flyweight 模式 :通过预分配一些常见的值对象,
并在多个数据结构之间共享这些对象,程序避免了重复分配的麻烦,也节约了一些CPU时间。
Redis 预分配的值对象有如下这些:
- 各种命令的返回值,比如执行成功时返回的 OK ,执行错误时返回的 ERROR ,类型错误时返回的 WRONGTYPE ,命令入队事务时返回的 QUEUED ,等等。
- 包括0在内,小于redis.h/REDIS_SHARED_INTEGERS的所有整数
-
- (REDIS_SHARED_INTEGERS的默认值为10000)
共享对象只能被字典和双端链表这类能带有指针的数据结构使用。
像整数集合和压缩列表这些只能保存字符串、整数等字面值的内存数据结构,就不能使用共享对象。
引用计数以及对象的销毁
C语言本身没有自动释放内存的相关机制
以及对象被引用了多少次?
Redis使用引用计数来负责维持和销毁对象
- 每个redisObject 结构都带有一个 refcount 属性,指示这个对象被引用了多少次。
- 当新创建一个对象时,它的 refcount 属性被设置为 1 。
- 当对一个对象进行共享时,Redis 将这个对象的 refcount 增一。
- 当使用完一个对象之后,或者取消对共享对象的引用之后,程序将对象的 refcount 减一。
- 当对象的 refcount 降至 0 时,这个 redisObject 结构,以及它所引用的数据结构的内存,都会被释放。
小结
- Redis 使用自己实现的对象机制来实现类型判断、命令多态和基于引用计数的垃圾回收。
- 一种 Redis 类型的键可以有多种底层实现。
- Redis 会预分配一些常用的数据对象,并通过共享这些对象来减少内存占用,和避免频繁地为小对象分配内存。
字符串
REDIS_STRING (字符串)是 Redis 使用得最为广泛的数据类型,它除了是 SET 、 GET 等命令的操作对象之外,
数据库中的所有键,以及执行命令时提供给Redis的参数,都是用这种类型保存的。
字符串类型分别使用REDIS_ENCODING_INT和REDIS_ENCODING_RAW两种编码:
• REDIS_ENCODING_INT 使用 long 类型来保存 long 类型值。
• REDIS_ENCODING_RAW 则使用 sdshdr 结构来保存 sds (也即是 char* )、 long long 、
• REDIS_ENCODING_RAW 则使用 sdshdr 结构来保存 sds (也即是 char* )、 long long 、
double 和 long double 类型值。
在Redis中,只有能表示为long 类型的值,才会以整数的形式保存其他类型的整数,小数,字符串都是用sdshdr结构来保存的
默认编码REDIS_ENCODING_RAW即使用sdshdr保存数据
哈希表
当哈希表使用字典编码时,程序将哈希表的键( key)保存为字典的键,将哈希表的值( value)保存为字典的值。
哈希表所使用的字典的键和值都是字符串对象。
包含三个键值对的哈希表:
默认使用ziplist,压缩列表作为哈希表的编码
列表
REDIS_LIST(列 表)是LPUSH ,LRANGE 等命令的操作对象
阻塞的条件
BLPOP,BRPOP,BRPOPLPUSH
三个命令都可能造成客户端被阻塞,将这些命令统称为列表的阻塞原语
阻塞原语并不是一定造成客户端阻塞:
- 只有当这些命令被用于空列表时,它们才会阻塞客户端
- 如果被处理的列表不为空的话,它们就执行无阻塞版本的 LPOP 、 RPOP 或 RPOPLPUSH 命令。
BLPOP决定是否对客户端进行阻塞过程:
阻塞
当一个阻塞原语的处理目标为空键时,执行该阻塞原语的客户端就会被阻塞
阻塞一个客户端需要执行以下步骤:
1. 将客户端的状态设为“正在阻塞” ,并记录阻塞这个客户端的各个键,以及阻塞的最长时限( timeout)等数据。
2. 将客户端的信息记录到 server.db[i]->blocking_keys 中(其中 i 为客户端所使用的数据库号码)。
3. 继续维持客户端和服务器之间的网络连接,但不再向客户端传送任何信息,造成客户端阻塞。
解除阻塞
server.db[i]->blocking_keys 是一个字典,字典的键是那些造成客户端阻塞的键,
而字典的值是一个链表,链表里保存了所有因为这个键而被阻塞的客户端(被同一个键所阻塞的客户端可能不止一个)
当客户端被阻塞之后,脱离阻塞状态有以下三种方法:
1. 被动脱离:有其他客户端为造成阻塞的键推入了新元素。
2. 主动脱离:到达执行阻塞原语时设定的最大阻塞时间。
3. 强制脱离:客户端强制终止和服务器的连接,或者服务器停机
2. 主动脱离:到达执行阻塞原语时设定的最大阻塞时间。
3. 强制脱离:客户端强制终止和服务器的连接,或者服务器停机
lpush rpush linsert
推入新元素
内部均由pushGenericCommand 去做
pushGenericCommand 函数执行以下两件事:
- 检查这个键是否存在于前面提到的 server.db[i]->blocking_keys 字典里,
- 如果是的话,那么说明有至少一个客户端因为这个 key 而被阻塞,
- 程序会为这个键创建一个redis.h/readyList 结构,并将它添加到 server.ready_keys链表中。
- 即将readylist 添加到服务器
- 将给定的值添加到列表键中。
虽然key3已经不再是空键,但到目前为止,被key3阻塞的客户端还没有任何一个被解除阻塞状态。
调用handleClientsBlockedOnLists,执行:
def handleClientsBlockedOnLists(): # 执行直到 ready_keys 为空 while server.ready_keys != NULL: # 弹出链表中的第一个 readyList rl = server.ready_keys.pop_first_node() # 遍历所有因为这个键而被阻塞的客户端 for client in all_client_blocking_by_key(rl.key, rl.db): # 只要还有客户端被这个键阻塞,就一直从键中弹出元素 # 如果被阻塞客户端执行的是 BLPOP ,那么对键执行 LPOP # 如果执行的是 BRPOP ,那么对键执行 RPOP element = rl.key.pop_element() if element == NULL: # 键为空,跳出 for 循环 # 余下的未解除阻塞的客户端只能等待下次新元素的进入了 break else: # 清除客户端的阻塞信息 server.blocking_keys.remove_blocking_info(client) # 将元素返回给客户端,脱离阻塞状态 client.reply_list_item(element)
先阻塞先服务FBFS策略,这点从上面伪代码也是可以看出的,根据从前开始取列表
阻塞因超时而取消
每次Redis服务器常规操作函数( server cron job)执行时,程序都会检查所有连接到服务器
的客户端查看那些处于"正在阻塞"状态的客户端的最大阻塞时限是否已经过期,
集合
REDIS_SET 集合是SADD,SRANDMEMBER等命令的操作对象
第一个添加到集合的元素,决定了创建集合时所使用的编码
- 如果第一个元素可以表示为 long long 类型值(也即是,它是一个整数),那么集合的初始编码为 REDIS_ENCODING_INTSET
- 否则,集合的初始编码为 REDIS_ENCODING_HT
sinter,sinterstore,求并交集
sdiff,sdiffstore ,求集合差算法
有序集
REDIS_ZSET (有 序 集)是 ZADD ,ZCOUNT 等命令的操作对象