之前我们提到过,redis可以存储键五种数据结构,这五种数据结构分别是STRING(字符串),LIST(列表),SET(集合),HASH(散列),ZSET(有序集合)。下面我们会对这五种数据结构进行介绍。不过在介绍着五种数据类型之前,先简单说一下redis的key键。Redis key值是二进制安全的,这意味着可以用任何二进制序列作为key值,从形如”foo”的简单字符串到一个JPEG文件的内容都可以。空字符串也是有效key值。
String是redis最简单的key-value结构类型,value值不仅可以是String,也可以是数字(当数字类型是Long的时候,encoding就是整型,其他都存储在sdshdr当做字符串)。你可以存放任何种类的字符串,包括二进制数据,例如你可以在一个键下保存一副jpeg图片。值的长度不能超过1GB。使用String类型,可以完全实现目前Memcached的功能,并且效率更高。还可以享受Redis的定时持久化(可以选择RDB模式或者AOP模式),操作日志及Replication等功能。
redis中的字符串并没有直接使用C语言传统的字符串来表示,而是构建了一种简单动态字符串的类型,简称SDS,并将其作为redis默认的字符串来表示。在redis中,C字符串只是用来做字符串的字面量,当用来作为可以修改的字符串的时候使用的是SDS。
SDS的结构为三部分,char buf【】字节数组,用来保存字符串;int len用来记录buf数组中已经使用字节的数;int free记录buf数组中未使用的字节数。我们用SDS和C字符串来做一下比较。我们知道,C字符串的长度是N+1,最后一个元素总是空字符‘\n’。因为C字符串并没有记录字符串的长度,所以为了获取一个字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计算,直到遇到空字符,在C字符串中只能有一个空字符,它会以遇到的第一个空字符来判断这个字符是否结束。由于C字符串底层永远是一个N+1的数组,所以,每次对字符串进行增加或者减少的操作的时候,程序总要对保存这个字符串的数组进行一次内存重分配的操作。如果不这么做,在进行字符串拼接或者增加其长度的时候,有可能会导致缓冲区溢出,当减少其长度的时候,可能会导致内存泄漏。但是SDS为了避免这些,做了调整。SDS中有专门记录字符长度的len值和记录空闲长度的free值。程序只需直接访问SDS的len值就可以知道字符串的长度,当程序修改对SDS进行修改的时候,会先检测SDS字符串的空间是否满足修改所需要的要求,如果不满足,会自动的将SDS的空间扩展到执行修改所需要的大小。但是一旦涉及到扩展空间,就会出现内存重新分配的操作,redis的SDS为了避免这样,通过未使用空间,实现了空间预分配和惰性空间释放两种优化策略。
所谓空间预分配,当对SDS进行修改的时候,并且需要对SDS空间进行扩展的时候,程序不仅会为SDS分配修改所需要的空间,还会为SDS分配额外的未使用空间。其分配策略是如下定义的:如果对SDS修改后的长度小于1MB,那么程序分配和len属性同样大小的未使用空间;如果对SDS修改后的长度大于等于1MB,那么程序会分配1MB的未使用空间。通过空间预分配策略,redis可以减少连续执行字符串增长操作所需要的内存重分配次数。
注意:无论是哪种情况,修改后的buf数组的实际长度最后都应该加上一字节。(可以想想为什么)
所谓惰性空间释放,就是当需要缩短SDS保存的字符串的时候,程序并不立即使用内存重新分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录下来,并等来将来使用。当然,当我们真正需要释放SDS的未使用空间的时候,可以通过提供的api释放。
注意:redis并不是用SDS的buf数组来保存字符的,而是来保存一系列二进制数据。
要说清楚redis的列表数据类型,一般意义上讲,列表就是有序元素的序列。但是现在有很多地方对list的使用并不恰当,比如Python Lists就名不副实,名为Linked list,其实是数组,用数组实现lIst和用Linked List实现list,在属性方面大不相同。
Redis List基于Linked list实现,Redis对链表(Linked list)的支持使得它在键值存储的世界中独树一帜,这意味着即使在一个list中有数百万个元素,在头部添加一个元素和在尾部添加一个元素,其时间复杂度也是常数级别的。在十个元素的list的头部添加一个元素和在十万个元素的list头部添加一个元素的速度相同。那么有优点就有缺点,我们知道如果用数组实现list,我们可以通过索引快速的访问list中的元素,但是如果用Linked list实现list就没有那么快了。Redis Lists用linked list实现的原因是:对于数据库系统来说,至关重要的特性是:能非常快的在很大的列表上添加元素。另一个重要因素是,正如你将要看到的:Redis lists能在常数时间取得常数长度。
Redis对外暴露的list数据类型,它底层实现所依赖的内部数据结构就是quicklist。在quicklist.c文件头部的注释中,是这样解释quicklist的:A doubly linked list of ziplists,一个ziplists的双向列表。双向链表是由多个节点(Node)组成的,quicklist中的每个节点都是一个ziplist,我们知道双向链表虽然利于在它的两端进行push和pop,但是由于它在每个节点上除了要保存数据之外还要额外的保存两个指针,其次双向链表的各个节点都是相互独立的内存块,地址不连续,节点多了容易产生内存碎片,所以它的内存消耗比较大。然而ziplist由于是一个连续的内存,所以存储效率很高,但是它不利于修改操作,每次数据变动都会引起一次内存的realloc,特别是在ziplist长度很长的时候,一次数据变动,一次realloc可能就会大批量数据,进一步降低性能。redis将这两点结合起来寻找一个平衡点。我们来分析一下,每个quicklist节点上的ziplist越短,内存碎片就越多,就有可能在内存中行成很多不能被利用的小碎片,从而降低存储效率。我们极端的假设一下,每个quicklist节点上的ziplist只有一个数据项,这样就变成一个普通的双向链表了。反过来说,如果每个quicklist节点上的ziplist越长,则为ziplist分配大块连续内存空间的难度就越大,如果这些空间没有被利用,就会在内存中形成很多小块的空闲空间,但却找不到一块足够的空闲空间分配给ziplist,这样同样会降低存储效率。这种情况的极端是整个quicklist只有一个节点,所有的数据项都分配在这仅有的一个节点的ziplist里面。这其实蜕化成一个ziplist了。可见,一个quicklist节点上的ziplist要保持一个合理的长度。那到底多长合理呢?这可能取决于具体应用场景。实际上,Redis提供了一个配置参数 list-max-ziplist-size ,就是为了让使用者可以来根据自己的情况进行调整。它可以取正值,也可以取负值。
当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。
当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,每个值含义如下:
-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)
-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
另外,list的设计目标是能够用来存储很长的数据列表的。当列表很长的时候,最容易被访问的很可能是两端的数据,中间的数据被访问的频率比较低(访问起来性能也很低)。如果应用场景符合这个特点,那么list还提供了一个选项,能够把中间的数据节点进行
压缩,从而进一步节省内存空间。Redis的配置参数 list-compress-depth 就是用来完成这个设置的。
list-compress-depth 0
这个参数表示一个quicklist两端不被压缩的节点个数。注:这里的节点个数是指quicklist双向链表的节点个数,而不是指ziplist里面的数据项个数。实际上,一个quicklist节点上的ziplist,如果被压缩,
就是整体被压缩的。
参数 list-compress-depth 的取值含义如下:
0: 是个特殊值,表示都不压缩。这是Redis的默认值。
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。
依此类推...
由于0是个特殊值,很容易看出quicklist的头节点和尾节点总是不被压缩的,以便于在表的两端进行快速存取。具体的情况可以自行查资料,看源码。
Redis的集合和列表都可以存储多个字符串,它们的不同之处在于,列表可以存储多个相同的字符串,二集合则通过使用散列的来保证自己存储的每个字符串都是各不相同的(这些散列只有键,但没有与键相关联的值)。REDIS_ENCODING_ZIPLIST即ZIPLIST,是一种双端列表,且通过特殊的格式定义,压缩内存适用,以时间换空间。
Redis支持Value为Hash表,其逻辑类型为REDIS_HASH,REDIS_HASH可以有两种encoding方式: REDIS_ENCODING_ZIPLIST 和 REDIS_ENCODING_HT。REDIS_ENCODING_HT即前文提到的字典的实现,REDIS_ENCODING_ZIPLIST即ZIPLIST,是一种双端列表,且通过特殊的格式定义,压缩内存适用,以时间换空间。
zset是set的升级版本,它在set的基础上增加了一个权重参数score,使得集合中的元素能够按照score进行有序排列。这一参数在添加和修改元素的时候可以指定,每次指定之后,zset会自动重新的按新的值调整顺序。比如一个存储全班同学成绩的zset,其集合 value 可以是同学的学号,而 score 就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。有序集合和散列一样,都用于存储键值对:有序集合的键被称为成员(member),每个成员都是独一无二的;而有序集合的值则被称为分值(score),分值必须为浮点数。有序集合是Redis里面唯一一个既可以根据成员访问元素(这一点和散列一样),又可以根据分值以及分值的排列顺序来访问元素的结构。
Redis中的sorted set,是在skiplist, dict和ziplist基础上构建起来的。我们这里说一下skiplist。Redis里面使用skiplist是为了实现sorted set这种对外的数据结构。skiplist本质上也是一种查找结构,即根据给定的key快速的查找到它所在的位置(或者对应的value)。skiplist。顾名思义,首先它是一个list,是在有序链表的基础上发展起来的。在这样的链表中,我们要查找某个数据,就需要从头开始逐个进行比较,直到找到包含数据的那个节点,或者找到第一个比给定数据大的节点为止(没找到),也就是说时间复杂度为O(n)。同样,当我们要插入新数据的时候,也要经历相同的查找过程,从而确定插入的位置。
现在,我们在每相邻的两个节点中间增加一个指针,让指针指向下下个节点,如下图:
我们可以看到我们在旧的的列表上增加了一层新的列表,这层新的列表是原来旧的列表数量的一半,当我们想要查找数据的时候, 会先沿着这条新的链表进行查找,当碰到比待查找数据大的节点时,再回到原来的链表中进行查找。我们可以看到,由于新增加的指针,我们不用与链表中的每个节点进行比较了,只需要比较原来一半的节点数。
同样的我们可以在新增加的链表上再增加一层新的链表,这样查找的速率又会为原来的一半,如下图:
......
可以想象,当链表足够长的时候,这种多层链表的查找方式能让我们跳过很多下层节点,大大加快查找的速度。
skiplist正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。删除数据也有同样的问题。
skiplist为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level),这是一个随机数,至于这个随机数的算法,我就不多做介绍了。为了表达清楚,下图展示了如何通过一步步的插入操作从而形成一个skiplist的过程:
从上面skiplist的创建和插入过程可以看出,每一个节点的层数(level)是随机出来的,而且新插入一个节点不会影响其它节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。这就降低了插入操作的复杂度。
根据上图中的skiplist结构,我们很容易理解这种数据结构的名字的由来。skiplist,翻译成中文,可以翻译成“跳表”或“跳跃表”,指的就是除了最下面第1层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针故意跳过了一些节点(而且越高层的链表跳过的节点越多)。这就使得我们在查找数据的时候能够先在高层的链表中进行查找,然后逐层降低,最终降到第1层链表来精确地确定数据位置。在这个过程中,我们跳过了一些节点,从而也就加快了查找速度。
结构类型 | 结构存储的值 | 结构的读写能力 |
---|---|---|
STRING |
可以是字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作;对整数和浮点数执行自增(increment)或者自减(decrement)操作 |
LIST |
一个链表,链表上的每个节点都包含了一个字符串 | 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪(trim);读取单个或者多个元素;根据值查找或者移除元素 |
SET | 包含字符串的无序收集器(unordered collection),并且被包含的每个字符串都是独一无二、各不相同的 | 添加、获取、移除单个元素;检查一个元素是否存在于集合中;计算交集、并集、差集;从集合里面随机获取元素 |
HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对;获取所有键值对 |
ZSET | 字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定 | 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素 |
redis 127.0.0.1:6379> SET mykey "redis"