Redis数据结构实现

Redis五种基本数据结构:String、List、Set、Hash、Zset

一、Redis对象基本结构

Redis是键值对方式的存储,每一个Redis存储对象都包括两个对象,一个是Key,一个是Value。

Redisc创建的对象RedisObject数据结构如下:

unsigned type: 4  // 类型,其中type的值对应5中redis数据类型的类型常量

unsigned encoding: 4 // 编码,encoding则记录了底层实现所用的数据结构

void *ptr // 指向底层数据结构指针

二、内部数据结构

2.1 简单动态字符串

redis实现了一个SDS结构(简单动态字符串)类型来替代C语言中的原生字符串, 结构如下

struct sdshdr {

    int len; //记录buf中字符串的实际长度

    int free; //记录buf数组空闲长度

    char buf[];//字节数组,用于保存字符串

};

和C语言原生字符串相比, 其有一下优点:

1、因为存储了字符串长度,可以在长量时间内获取字符串长度。

2、同样由于存储了字符串实际长度和buff空闲长度,字符串变化时不需要每次都重新分配内存;同时也避免了字符串长度变化可能导致的缓冲区溢出和内存泄漏。

3、SDS的设计由于根据字符串长度来判断字符串的末尾,因此中间可以存储任何数据包括空字符

2.2 双向链表

redis中的链表类型就是最常用的双向链表,涉及数据结构如下

//链表类型

typedef struct list {

listNode * head;//表头节点

listNode * tail;//表未节点,方便逆向遍历

unsigned long len;//节点数目, 优化链表求长度

}list;

2.3、Hash表

redis中的字典类型也就是基本的hash表

Redis hash 数据结构

redis中的rehash就是通过创建一个临时的字典,将正在使用的字典中的所有键值对复制到临时字典中(这个过程需要重新计算hash值和索引值,因此称为rehash),最后释放老的字典,并将其指针指向临时字典。

当数据量比较小的时候,这个过程可以一次性完成,但是数据量大的时候,庞大的计算量可能导致整个服务停止,因此redis采用了渐进式rehash:

1、同时维护两个hash表hd[0]和hd[1]

2、设置rehash_idx设置为0,在0< rehash_idx < 槽位数组长度时, 对hash表的任何操作除了执行正常的操作过程以外,还会将hd[0]槽位数组rehash_idx索引上的所有键值对rehash到hd[1]

3、同时所有的添加键值对操作只会在hd[1]上进行,保证了hd[0]只减不增。

4、rehash完成后,hash表指针指向hd[1],释放hd[0]内存。

2.4 跳跃表

跳跃表是一种有序的数据结构,支持平均O(logN)复杂度的节点查找,其效率可以和平衡树媲美,同时实现简单,而且由于不需要reblance操作,在高并发情况下表现更加出色。redis中的跳跃表也就是基本的跳跃表实现, 涉及数据结构如下。

typedef struct zskiplistNode{

    struct zskiplistLevel{   // 层数组,相当于索引,其中包括了当前节点砸每一层的前进指针以及到下一节点的跨度

           struct zskiplistNode *forward; // 本层节点中的下一节点

            unsinged int span; // 跨度,用于计算节点的排位。

   } level[]

   struct zskiplistNode *backWard; //指向前一个节点

   duble score; // 所有节点按分值排序

    robj *obj // 成员对象

typedef struct zskiplist {

    struct zskiplistNode *header ,*tail;

    unsigned long length; // 表中节点数

    in level; 最大层数

}


redis 跳表

实际上对有序链表稍加改造,我们就可以对链表进行二分查找。这就是我们要说的跳表。下面我们来看一下,跳表是怎么跳的。

原始链表

上图是一个简单的有序的单链表,如果要查找某个数据,只能从头至尾遍历链表,查找到值与给定元素时返回该结点,这样的查询效率很低,时间复杂度是为O(n)。

假如对链表进行改造,先对链表中每两个节点建立第一级索引,再对第一级索引每两个节点建立第二级索引。如下图所示:

跳表建立索引

对于上图中的带二级索引的链表中,我们查询元素 16,先从第二级索引查询 1 -> 7->13,发现16大于13 ,然后通过 13 的 down 指针找到第一级索引的 17,发现 16 小于17 ,再通过13 的 down 指针找到链表中的 16,只需要遍历 6 个节点就完成 16 的查找。如果在单链表中直接查找 16 的话,只能顺序遍历,需要遍历 10 个节点,是不是效率上有所提升呢,由于数据量较小,遍历 10 个节点到遍历 6 个节点你可能觉得没有提升多少性能。

所以,当链表的长度 n 比较大时,比如 1000、10000 的时候,在构建索引之后,查找效率的提升就会非常明显。

跳表有多占内存?

假如有 n 个元素的链表,第一级索引为 n/2 个,第二级为 n/4 个,第三级为 n/8 个,......,最后一级为 2 个。这几级索引的结点总和就是n/2+n/4+n/8…+8+4+2=n-2。所以,跳表的空间复杂度是 O(n)。也就是说,如果将包含 n 个结点的单链表构造成跳表,我们需要额外再用接近 n 个结点的存储空间。那我们有没有办法降低索引占用的内存空间呢?

其实 redis 中有序集合支持的核心操作也就是这几个。这里说下为什么 redis 使用跳表而不使用红黑树。

1、红黑树在查找区间元素的效率没有跳表高,其他操作时间复杂度一致。

2、相比红黑树,跳表的实现还是简单的,简单就意味着不容易出错,bug 少,稳定,易读,易维护。

3、跳表更加灵活,通过改变索引构建策略,有效平衡效率和内存消耗

2.5 压缩表

压缩列表是Redis为了节约内存而开发的, 是由一系列特殊编码的连续内存块组成的顺序型数据结构,每个压缩列表节点可以保存一个字节数组或一个整数值。其由三部分构成,如下所示:


zlbytes 表示的是整个压缩列表使用的内存字节数

zltail 指定了压缩列表的尾节点的偏移量

zllen 是压缩列表 entry 的数量

entry 就是 ziplist 的节点

zlend 标记压缩列表的末端

再看看一个 entry 的结构

typedef struct zlentry { 

// prevrawlen 前置节点的长度 ;prevrawlensize 编码 prevrawlen 所需的字节大小

 unsigned int prevrawlensize, prevrawlen;

 // len 当前节点值的长度 ,lensize 编码 len 所需的字节大小

  unsigned int lensize, len; 

// 当前节点 header 的大小  等于 prevrawlensize + lensize 

unsigned int headersize;     

 // 当前节点值所使用的编码类型 

unsigned char encoding; 

// 指向当前节点的指针 

unsigned char *p;

 } zlentry;      

prevrawlen 前置节点的长度,这里多了一个 size,其实是记录了 prevrawlen 的尺寸。Redis 为了节约内存并不是直接使用默认的 int 的长度,而是逐渐升级的。

同理 len 记录的是当前节点的长度,lensize 记录的是 len 的长度。

三、数据类型

String类型

String类型在Redis底层可以是int,raw和embstr。

如果一个String对象保存的是整数值,并且可以使用long来表示,那么将会以整数形式保存。

如果一个String对象保存的是字符串值,并且其长度大于32字节,那么就直接使用SDS结构来保存,其encoding标记为raw。

如果一个String对象保存的字符串长度小于32字节,那么会使用embstr编码,embstr是一种短字符串的优化,其存储还是使用SDS结构,但raw编码会调用两次内存分配函数来分别创建redisObject结构和SDS结构,而embstr编码则通过调用一次内存分配函数来分配 一块连续的空间,空间中依次包含redisObject和SDS结构。

List类型

List类型在Redis底层可以是ziplist(压缩列表)或linkedlist(双向列表)。

Hash类型

Hash类型在Redis底层可以是ziplist(压缩列表)或hashtable(Hash表),ziplist编码的哈希对象每当 有新的键值对要插入时,会将保存了键和值的节点依次推入到压缩列表表尾,因此同一键值对顺序存放在一起。

Set类型

Set类型在Redis底层可以是intset(整数集合)或hashtable(Hash表),只有当Set中的所有元素均为整数类型时才会使用intset。

Zset类型

Zset类型在Redis底层可以是ziplist(压缩列表)或skiplist(跳跃


数据结构

[1]Redis的数据结构

[2] 跳表的原理

[3] 晴天哥_374 redis hash底层数据结构

你可能感兴趣的:(Redis数据结构实现)