参考资料:
相关文章:
写在开头:本文为个人学习笔记,内容比较随意,夹杂个人理解,如有错误,欢迎指正。
目录
如果要了解redis的数据结构,可以从两个不同的层面来讨论它:
- 第一个层面,是从使用者的角度,这一层面也是Redis暴露给外部的调用接口,比如:string,list,hash,set,sorted set。
- 第二个层面,是从内部实现的角度,属于更底层的实现,比如:dict,sds,ziplist,quicklist,skiplist,intset。
在之前的文章中我们介绍了Redis的几种底层数据结构,本文我们介绍下基础数据类型是如何关联到这些数据结构上的。
一、redisObject
1、redisObject的作用
在redis的命令中,用于对键进行处理的命令占了很大一部分,而对于键所保存的值的类型(键的类型),键能执行的命令又各不相同。如: LPUSH 和 LLEN 只能用于列表键, 而 SADD 和 SRANDMEMBER 只能用于集合键, 等等; 另外一些命令, 比如 DEL、 TTL 和 TYPE, 可以用于任何类型的键;但是要正确实现这些命令, 必须为不同类型的键设置不同的处理方式: 比如说, 删除一个列表键和删除一个字符串键的操作过程就不太一样。
以上的描述说明, Redis 必须让每个键都带有类型信息, 使得程序可以检查键的类型, 并为它选择合适的处理方式. 比如说, 集合类型就可以由字典和整数集合两种不同的数据结构实现, 但是, 当用户执行 ZADD 命令时, 他/她应该不必关心集合使用的是什么编码, 只要 Redis 能按照 ZADD 命令的指示, 将新元素添加到集合就可以了。
这说明, 操作数据类型的命令除了要对键的类型进行检查之外, 还需要根据数据类型的不同编码进行多态处理.
为了解决以上问题, Redis 构建了自己的类型系统, 这个系统的主要功能包括:
- redisObject 对象.
- 基于 redisObject 对象的类型检查.
- 基于 redisObject 对象的显式多态函数.
- 对 redisObject 进行分配、共享和销毁的机制
2、redisObject数据结构
/* 对象类型 */
#define OBJ_STRING 0 // 字符串
#define OBJ_LIST 1 // 列表
#define OBJ_SET 2 // 集合
#define OBJ_ZSET 3 // 有序集
#define OBJ_HASH 4 // 哈希表
/* 对象编码 */
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* 注意:版本2.6后不再使用. */
#define OBJ_ENCODING_LINKEDLIST 4 /* 注意:不再使用了,旧版本2.x中String的底层之一. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
#define LRU_BITS 24
/* Redis 对象 */
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码方式
unsigned encoding:4;
// LRU - 24位, 记录最末一次访问时间(相对于lru_clock)
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向底层数据结构实例
void *ptr;
} robj;
redisObject 几个最重要的属性如下:
type:记录了对象所保存的值的类型,其值为下面几个常量之一
/* 对象类型 */
#define OBJ_STRING 0 // 字符串
#define OBJ_LIST 1 // 列表
#define OBJ_SET 2 // 集合
#define OBJ_ZSET 3 // 有序集
#define OBJ_HASH 4 // 哈希表
encoding:记录了对象所保存的值的编码,其值为下面几个常量之一
/* 对象编码 */
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* 注意:版本2.6后不再使用. */
#define OBJ_ENCODING_LINKEDLIST 4 /* 注意:不再使用了,旧版本2.x中String的底层之一. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
ptr:一个指针,指向实际保存值的数据结构,这个数据结构由type和encoding属性决定。举个例子, 如果一个redisObject 的type 属性为OBJ_LIST , encoding 属性为OBJ_ENCODING_QUICKLIST ,那么这个对象就是一个Redis 列表(List),它的值保存在一个QuickList的数据结构内,而ptr 指针就指向quicklist的对象。
在内存中的结构如下图:
lru: 记录了对象最后一次被命令程序访问的时间。
基于该属性,Redis引入了一个叫做空转时长的概念,当前时间减去键的值对象的lru时间,就是该键的空转时长。Object idletime命令可以打印出给定键的空转时长
如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。
refcount:记录当前对象被引用了多少次,对象被创建时记录为1,当对一个对象进行共享时,redis将这个对象的refcount加一。当使用完一个对象后,或者消除对一个对象的引用之后,程序将对象的refcount减一。当对象的refcount降至0 时,这个RedisObject结构,以及它引用的数据结构的内存都会被释放。
关于引用计数,这里涉及到了对象共享,redis一般会把一些常见的值放到一个共享对象中,这样可使程序避免了重复分配的麻烦,也节约了一些CPU时间,redis预分配的值对象如下:
(1)各种命令的返回值,比如成功时返回的OK,错误时返回的ERROR,命令入队事务时返回的QUEUE,等等。(注意只有常用字符串,因为字符串的比较时间复杂度接近O(n))
(2)包括0 在内,小于REDIS_SHARED_INTEGERS的所有整数(REDIS_SHARED_INTEGERS的默认值是10000)
命令的类型检查和多态
当执行一个处理数据类型命令的时候,redis执行以下步骤
- 根据给定的key,在数据库字典中查找和他相对应的redisObject,如果没找到,就返回NULL;
- 检查redisObject的type属性和执行命令所需的类型是否相符,如果不相符,返回类型错误;
- 根据redisObject的encoding属性所指定的编码,选择合适的操作函数来处理底层的数据结构;
- 返回数据结构的操作结果作为命令的返回值。
二、redis对象与底层结构对应关系
经过上文和《Redis:底层数据结构》的介绍,我们对Redis的对象机制和底层数据结构有了一些了解,下面我们来介绍下Redis中基本数据类型的实现方式(即基于何种基础结构实现)。
Redis提供的基础数据类型如下:
数据类型 | 可以存储的值 | 操作 | 应用场景 |
STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 | 做简单的键值对缓存 |
LIST | 列表 | 从两端压入或者弹出元素 对单个或者多个元素进行修剪, 只保留一个范围内的元素 | 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的数据 |
SET | 无序集合 | 添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素 | 交集、并集、差集的操作,比如交集,可以把两个人的粉丝列表整一个交集 |
HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在 | 结构化的数据,比如一个对象 |
ZSET | 有序集合 | 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名 | 去重但可以排序,如获取排名前几名的用户 |
1、String
字符串是Redis最基本的数据类型,不仅所有key都是字符串类型,其它几种数据类型构成的元素也是字符串。注意字符串的长度不能超过512M。
String类型可以存储数值和字符串,当存储字符串时,看过前文的朋友大概可以猜出时基于SDS实现的,而当存储的值是整数值时,String会判断其大小是否可以由long表示,可以的话直接保存,否则按字符串的模式保存。Redis通过3种不同的编码实现String的不同实现,这34种编码反方式分别为int,embstr和raw。
三种实现方式
- int 编码:保存的是可以用 long 类型表示的整数值(浮点数类型也是作为字符串保存的,在需要的时候再将其转换成浮点数类型)
- embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)
- raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)
我们看到虽然同样是基于SDS结构实现的String字符串,却分为了embst和raw两种类型,从上图中我们可以看出embst存储的值比raw小,且embst的内存是直接和redisObject连接在一起的,而raw则是通过指针进行的连接,这是因为embstr编码是专门用来保存短字符串的一种优化编码。
embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。
编码的转换
(1)数值转字符串:当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw。
(2)字符串修改:对于 embstr 编码,由于 Redis 没有对其编写任何的修改程序(embstr 是只读的),在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。
二、List
list 列表,它是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表结构。
列表对象的编码目前有三种,quicklist、zipmap、linkedlist,不过后两种并没有被使用,因此list类型的编码其实被固定为了quicklist,quicklist在前文中已经做了详细介绍,这里不多赘述。
三、 Hash
哈希对象的编码可以是 ziplist 或者 hashtable;对应的底层实现有两种, 一种是ziplist, 一种是dict。
两种实现
- 编码为ziplist时的内存分布:
当使用ziplist作为底层实现时,新增的键值对是保存到压缩列表的表尾。
- 编码为hashtable时的内存分布:
当使用dict作为底层实现时,哈希对象中的每个键值对都使用一个字典键值对(键与值均是以sds的形式存储的)。
其实使用dict作为Hash的实现已经足够了,就像我们java中的HashMap,但是Redis为了能够节省内存,将ziplist也提供为了hash的实现之一,就是为了能够在元素个数少、元素长度小的场景下尽量的压缩内存占用,达到节省空间的目的。
编码转换
当同时满足下面两个条件时,使用ziplist(压缩列表)编码:
1、列表保存元素个数小于512个
2、每个元素长度小于64字节
不能满足这两个条件的时候使用 hashtable 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。
四、Set
实现方式
集合对象的底层结构有2种,分别是intset和dict。 显然当使用intset作为底层实现的数据结构时, 集合中存储的只能是数值数据, 且必须是整数; 而当使用dict作为集合对象的底层实现时, 是将数据全部存储于dict的键中, 值字段闲置不用。
- intset(注意只能存储整数):
- dict(注意这里和Hash种的区别,Hash中key和value都有值,这里只有key有值):
编码转换
当集合同时满足以下两个条件时,使用 intset 编码:
1、集合对象中所有元素都是整数
2、集合对象所有元素数量不超过512
不能满足这两个条件的就使用 hashtable 编码。第二个条件可以通过配置文件的 set-max-intset-entries 进行配置。
五、Zset
zset作为有序集合,使用score对元素进行排序。有序集合的底层实现依然有两种, 一种是使用ziplist作为底层实现, 另外一种比较特殊, 底层使用了两种数据结构: dict与zskiplist。
实现方式
- 使用ziplist时:
ziplist可以有效的节省内存,当数据量不大的时候使用ziplist是个不错的选择,参考上文在hash中使用ziplist作为底层实现的设置,分别使用2个相邻的节点作为key和value,在有序集合中redis将他们调整为了data和score,同时做好去重和排序的工作。
使用dict+zskiplist时:
我们从上图中可以看出,这里将dict中的table数组作为了zskiplistNode节点的指针,将两种结构结合了起来。
这么做的原因在于:
(1)假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序。
(2)假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作的复杂度变为了O(logN)。
将两者结合起来后,技能利用到dict的快速查找能力,又能利用到zskiplist自身有序的特点。
编码转换
当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:
1、保存的元素数量小于128;
2、保存的所有元素长度都小于64字节。
不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。