本文共 5217 字,大约阅读时间需要 17 分钟。
Redis 有 5 种基础数据结构,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)
Redis 的字符串是动态字符串,是可以修改的字符串,类似于 Java 的 ArrayList
/*
* 保存字符串对象的结构 */ struct sdshdr { // buf 中已占用空间的长度 int len; // buf 中剩余可用空间的长度 int free; // 数据结构 char buf[]; };
相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,需要对链表进行遍历,时间复杂度为 O(n),这点让人非常意外。当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理
快速链表quicklist
Redis 底层存储的还不是一个简单的 linkedlist
,而是称之为快速链表 quicklist
的一个结构。
列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist
(压缩列表)。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist
。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是 int
类型的数据,结构上还需要两个额外的指针 prev
和 next
。所以 Redis 将链表和 ziplist
结合起来组成了 quicklist
。也就是将多个 ziplist
使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
list这种数据结构的特点:
O(1)
O(N)
quicklist
是一个ziplist
的双向链表(双向链表是由多个节点Node组成的)。也就是说quicklist
的每个节点都是一个ziplist
。ziplist本身也记录了数据节点的顺序,而且在内存中的位置是相邻的。
Redis 为了节约内存空间而开发压缩列表 (ziplist) ,zset 和 hash 容器对象在元素个数较少的时候,采用ziplist进行存储。压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙 , 支持双向遍历。因为 ziplist 都是紧凑存储,没有冗余空间. 意味着插入一个新的元素就需要调用 realloc 扩展内存。取决于内存分配器算法和当前的 ziplist 内存大小,realloc 可能会重新分配新的内存空间,并将之前的内容一次性拷贝到新的地址,也可能在原有的地址上进行扩展,这时就不需要进行旧内容的内存拷贝。如果 ziplist 占据内存太大,重新分配内存和拷贝内存就会有很大的消耗。所以 ziplist 不适合存储大型字符串,存储的元素也不宜过多。
压缩列表为了支持双向遍历,所以ziplist才会有 ztail_offset
这个字段,用来快速定位到最后一个元素,然后倒着遍历。
struct ziplist<T> {
int32 zlbytes; // 整个压缩列表占用字节数 int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点 int16 zllength; // 元素个数 T[] entries; // 元素内容列表,挨个挨个紧凑存储 int8 zlend; // 标志压缩列表的结束,值恒为 0xFF }
quicklist 如果使用的redis3.2
版本以上的,那么就会发现在程序中quicklist
基本取代了ziplist
。既然取代肯定意味着有功能上有优化并且对程序更加友好。其实Redis对外暴露的list的数据结构的底层实现就是quicklist
。先回忆下list这种数据结构的特点:
O(1)
O(N)
而quicklist
是一个ziplist
的双向链表(双向链表是由多个节点Node组成的)。也就是说quicklist
的每个节点都是一个ziplist
。ziplist本身也记录了数据节点的顺序,而且在内存中的位置是相邻的。
quicklist
是由ziplist
组成的双向链表,链表中的每个节点都以压缩列表ziplist
的结构保存着数据,而ziplist有多个entry
节点保存多个数据。相当于在一个quicklist
节点中保存的是一整片数据,而不是一个单独的数据。
//真正表示quicklist的数据结构typedef struct quicklist { // 指向头节点的指针(最左边) quicklistNode *head; // 指向尾节点的指针(最右边) quicklistNode *tail; // 所有ziplist数据项的个数总和 unsigned long count; // quicklist节点的个数 unsigned int len; // ziplist大小设置 int fill : 16; // 节点压缩深度设置 unsigned int compress : 16;} quicklist;typedef struct quicklistNode { //前驱节点 struct quicklistNode *prev; //后继节点 struct quicklistNode *next; //数据指针,如果当前节点的数据没有压缩,它就指向一个ziplist结构,否则指向quicklistLZF结构 unsigned char *zl; // 表示zl指向的ziplist的总大小,如果ziplist被压缩了,它的值仍然是压缩前的大小 unsigned int sz; // 表示ziplist里面包含的数据项个数,这个字段16bit unsigned int count : 16; // 表示ziplist是否压缩了,1代表没有压缩 2代表使用LZF压缩 unsigned int encoding : 2; // 预留字段,固定值2,表示使用ziplist作为数据容器 unsigned int container : 2; // 此节点之前是否已经压缩过 unsigned int recompress : 1; // 测试用的,暂时用不上 unsigned int attempted_compress : 1; // 扩展字段,暂时无用 unsigned int extra : 10;} quicklistNode;// 此结构表示一个被压缩过的ziplisttypedef struct quicklistLZF { // 压缩后的ziplist大小 unsigned int sz; // 存放压缩后的ziplist字节数组 char compressed[];} quicklistLZF;
quicklist结构.png
Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。
不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。
Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL
。当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。
zset 可能是 Redis 提供的最为特色的数据结构 它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。它的内部实现用的是一种叫做「跳跃表」的数据结构。zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。
跳跃表(有序集合zset的底层实现之一,适合元素多或者元素的成员是比较长的字符串,可以理解为多层的链表, 类似二叉搜索树,我们把一些节点提取出来作为索引)
zset 内部的排序功能是通过「跳跃表」数据结构来实现的,它的结构非常特殊,也比较复杂。因为 zset 要支持随机的插入和删除,所以它不好使用数组来表示。我们先看一个普通的链表结构。
需要链表按照 score 值进行排序。这意味着当有新元素需要插入时,要定位到特定位置的插入点,这样才可以继续保证链表是有序的。通常我们会通过二分查找来找到插入点,但是二分查找的对象必须是数组,只有数组才可以支持快速位置定位,链表做不到( 链表没有线性表那种连续下标,无法确定中间位置 ),那该怎么办?
跳跃表就是类似于这种层级制,最下面一层所有的元素都会串起来。然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级指针串起来。然后在这些代表里再挑出二级代表,再串起来。最终就形成了金字塔结构
跳跃表是一种有序的数据结构,它通过在每个节点上维持多个指向其它节点的指针来达到快速访问的目的。跳跃表在插入、删除和查找操作上的平均复杂度为O(logN)
,最坏为O(N)
,可以和红黑树相媲美,但是在实现起来,比红黑树简单很多。在Redis中的zset
就是使用了跳跃表的实现有序集合。
跳跃表具有以下性质:
a. 跳跃表是由多层结构组成,并且每一层都是一个有序的链表 b. 最底层(Level 1)的链表包含所有元素 , 多个节点可以包含相同的分值,但每个节点的对象必须是唯一的 c. 跳跃表(Level 2)开始每个节点包含2个指针,一个指向同一链表的下一个元素,一个指向下一层的元素 如下图。 d: 跳跃表节点的level可以视作是索引,高层索引数目少于低层,因此查找等操作时间复杂度可类比于二分查找
typedef struct zskiplistNode{
//层级( level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针) struct zskiplistLevel{ //前进指针(用于指向表尾方向的前进指针) struct zskiplistNode *forward; //跨度(用于记录两个节点之间的距离) unsigned int span; } level[];//后退指针( 用于从表尾向表头方向访问节点 )
struct zskiplistNode *backward; //分值( 跳跃表中的所有节点都按分值从小到大排序。成员对象指向一个字符串,这个字符串对象保存着一个SDS值 ) double score; //成员对象 robj *obj; }跳跃表结构
typedef struct zskiplist { //表头节点和表尾节点( 跳跃表的头结点和尾节点 ) structz skiplistNode *header,*tail; //表中节点数量 unsigned long length; //表中层数最大的节点的层数 ( 记录最大的层数 ) int level;}zskiplist;
转载地址:http://jzesn.baihongyu.com/