首页 > 代码库 > Redis数据类型

Redis数据类型

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 、
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. 强制脱离:客户端强制终止和服务器的连接,或者服务器停机
 
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 等命令的操作对象
技术分享
 技术分享

 

Redis数据类型