redis的键可以保存不同类型的值,那么在对键进行操作时就必须直到键的类型和其上可以执行的操作,所以Redis 必须让每个键都带有类型信息,使得程序可以检查键的类型,并为它选择合适的处理方式。另外,在前面介绍各个底层数据结构时有提到,Redis 的每一种数据类型,比如字符串、列表、有序集,它们都拥有不只一种底层实现(Redis 内部称之编码,encoding),这说明,每当对某种数据类型的键进行操作时,程序都必须根据键所采取的编码,进行不同的操作。
为了解决以上问题,Redis 构建了自己的类型系统,这个系统的主要功能包括:
• redisObject 对象。
• 基于 redisObject 对象的类型检查。
• 基于 redisObject 对象的显式多态函数。
• 对 redisObject 进行分配、共享和销毁的机制
redisObject结构
下面来看一下结构定义:
/*
353 * Redis 对象
354 */
355 typedef struct redisObject {
356
357 // 类型
358 unsigned type:4;
359
360 // 不使用(对齐位)
361 unsigned notused:2;
362
363 // 编码方式
364 unsigned encoding:4;
365
366 // LRU 时间(相对于 server.lruclock)
367 unsigned lru:22;
368
369 // 引用计数
370 int refcount;
371
372 // 指向对象的值
373 void *ptr;
374
375 } robj;
4位的`type`表示对象的逻辑类型,它的值可以是以下常量中的一个:
# define REDIS_STRING 0 // 字符串
# define REDIS_LIST 1 // 列表
# define REDIS_SET 2 // 集合
# define REDIS_ZSET 3 // 有序集
# define REDIS_HASH 4 // 哈希表
`encoding`表示逻辑类型的物理编码方式,它的值如下:
# 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 // 编码为跳跃表
`lru`用于LRU算法用于当内存超限时清除内存中的对象。`refcount`是对象的持有计数,ptr指向实际保存值的数据结构
从Redis服务器端接受到客户端请求后,解析请求包的参数,就会将所有参数转换为string类型的robj对象,robj在层层解析中传递,最终通过逻辑类型的实现进行相应的处理
redisObject 、Redis 所有数据类型、以及 Redis 所有编码方式(底层实现)三者之间的关系:
字符串(string)
字符串类型在Redis可能是DB中的key或者value或者其他逻辑类型中的值,如list中的元素,set中的元素,hash类型中的键或者值。在作为DB中的key时,是将string类型的实际字符串(sds)作为key,而不是robj对象
字符串编码
字符串类型分别使用 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 编码(节省内存)
这样robj的ptr指针是void *,可以直接将ptr=(void *)long_obj,这样做的好处就是代表整数的string不用额外的内存保存值,因为long跟指针的占用字节数不管32位还是64位系统都是一样的
哈希表(hash)
哈希表使用REDIS_ENCODING_ZIPLIST 和 REDIS_ENCODING_HT 两种编码方式
压缩列表编码的哈希表
当使用 REDIS_ENCODING_ZIPLIST 编码哈希表时,程序通过将键和值一同推入压缩列表,从而形成保存哈希表所需的键 -值对结构:
新添加的key-value会被添加到ziplist的末尾
编码选择
创建空白哈希表时,程序默认使用 REDIS_ENCODING_ZIPLIST 编码,当以下任何一个条件被满足时,程序将编码从切换为 REDIS_ENCODING_HT :
• 哈希表中某个键或某个值的长度大于 server.hash_max_ziplist_value (默认值为 64)
• 压缩列表中的节点数量大于 server.hash_max_ziplist_entries (默认值为 512 )
列表(list)
使用Lists结构,我们可以轻松地实现最新消息排行等功能。Lists的另一个应用就是消息队列,可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行,它 使 用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 )
集合(set)
Sets 就是一个集合,集合的概念就是一堆不重复值的组合。利用Redis提供的Sets数据结构,可以存储一些集合性的数据,比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。它 使 用REDIS_ENCODING_INTSET 和 REDIS_ENCODING_HT 两种方式编码:
编码的选择
第一个添加到集合的元素,决定了创建集合时所使用的编码:
• 如果第一个元素可以表示为 long long 类型值(也即是,它是一个整数),那么集合的初始编码为 REDIS_ENCODING_INTSET
• 否则,集合的初始编码为 REDIS_ENCODING_HT
编码切换
如果一个集合使用 REDIS_ENCODING_INTSET 编码,那么当以下任何一个条件被满足时,这个集合会被转换成 REDIS_ENCODING_HT 编码:
• intset 保存的整数值个数超过 server.set_max_intset_entries (默认值为 512 )
• 试图往集合里添加一个新元素,并且这个元素不能被表示为 long long 类型(也即是,它不是一个整数)
有序集(zset)
和Sets相比,Sorted Sets增加了一个权重参数score,使得集合中的元素能够按score进行有序排列,比如一个存储全班同学成绩的Sorted Sets,其集合value可以是同学的学号,而score就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。它 使 用REDIS_ENCODING_ZIPLIST 和 REDIS_ENCODING_SKIPLIST 两种方式编码:
编码选择
在通过 ZADD 命令添加第一个元素到空 key 时,程序通过检查输入的第一个元素来决定该创建什么编码的有序集。
如果第一个元素符合以下条件的话,就创建一个 REDIS_ENCODING_ZIPLIST 编码的有序集:
• 服务器属性 server.zset_max_ziplist_entries 的值大于 0 (默认为 128 )
• 元素的 member 长度小于服务器属性 server.zset_max_ziplist_value 的值(默认为 64)
否则,程序就创建一个 REDIS_ENCODING_SKIPLIST 编码的有序集
编码转换
对于一个 REDIS_ENCODING_ZIPLIST 编码的有序集,只要满足以下任一条件,就将它转换为
REDIS_ENCODING_SKIPLIST 编码:
• ziplist 所保存的元素数量超过服务器属性 server.zset_max_ziplist_entries 的值(默认值为 128 )
• 新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值(默认值为 64 )
ziplist编码的有序集
当使用 REDIS_ENCODING_ZIPLIST 编码时,有序集将元素保存到 ziplist 数据结构里面。
其中,每个有序集元素以两个相邻的 ziplist 节点表示,第一个节点保存元素的 member 域,第二个元素保存元素的 score 域。多个元素之间按 score 值从小到大排序,如果两个元素的 score 相同,那么按字典序对 member进行对比,决定那个元素排在前面,那个元素排在后面。
skiplist编码的有序集
当使用 REDIS_ENCODING_SKIPLIST 编码时,有序集元素由 redis.h/zset 结构来保存:
/*
* 有序集
*/
typedef struct zset {
// 字典
dict *dict;
// 跳跃表
zskiplist *zsl;
} zset;
zset 同时使用字典和跳跃表两个数据结构来保存有序集元素。
其中,元素的成员由一个 redisObject 结构表示,而元素的 score 则是一个 double 类型的浮点数,字典和跳跃表两个结构通过将指针共同指向这两个值来节约空间(不用每个元素都复制两份)
下图展示了一个 REDIS_ENCODING_SKIPLIST 编码的有序集:
• 检查给定 member 是否存在于有序集(被很多底层函数使用);
• 取出 member 对应的 score 值(实现 ZSCORE 命令)。
另一方面,通过使用跳跃表,可以让有序集支持以下两种操作:
• 在 O(log N ) 期望时间、O(N ) 最坏时间内根据 score 对 member 进行定位(被很多底层
函数使用);
• 范围性查找和处理操作,这是(高效地)实现 ZRANGE 、ZRANK 和 ZINTERSTORE
等命令的关键。
通过同时使用字典和跳跃表,有序集可以高效地实现按成员查找和按顺序查找两种操作