redis 数据结构1

 

简单动态字符串(simple dynamic string)SDS

Redis 没有直接使用C语言传统的字符串表示,而是本身构建了一种名为简单动态字符串(simple dynamic string SDS)的抽象类型,并将SDS用做Redis 的默认字符串表示:redis

10.143.128.165:6379> SET msg "hello world"
OK

设置一个key= msg,value = hello world 的新键值对,算法

键(key)是一个字符串对象,对象的底层实现是一个保存着字符串“msg” 的SDS;数组

值(value)也是一个字符串对象,对象的底层实现是一个保存着字符串“hello world” 的SDS安全

SDS除了用来保存字符串之外,SDS还被用做缓冲区(buffer)AOF模块中的AOF缓冲区数据结构

SDS 的定义

Redis 中定义动态字符串的结构:函数

/*  
 * 保存字符串对象的结构  
 */  
struct sdshdr {       
    int len;// buf 中已占用空间的长度      
    int free;// buf 中剩余可用空间的长度    
    char buf[];// 数据空间  
};

一、len 变量,用于记录buf 中已经使用的空间长度(这里指出Redis 的长度为5)优化

二、free 变量,用于记录buf 中还空余的空间(初次分配空间,通常没有空余,在对字符串修改的时候,会有剩余空间出现ui

三、buf 字符数组,用于记录咱们的字符串(记录Redis)编码

SDS 与 C 字符串的区别

 

C 字符串 SDS
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
API 是不安全的,可能会形成缓冲区溢出 API 是安全的,不会形成缓冲区溢出
修改字符串长度N次必然须要执行N次内存重分配 修改字符串长度N次最多执行N次内存重分配
只能保存文本数据 能够保存二进制数据和文本文数据
可使用全部<String.h>库中的函数 可使用一部分<string.h>库中的函数

1 获取字符串长度(SDS O(1)/C 字符串 O(n))spa

传统的C 字符串 使用长度为N+1 的字符串数组来表示长度为N 的字符串,因此为了获取一个长度为C字符串的长度,必须遍历整个字符串。

SDS 的数据结构中,有专门用于保存字符串长度的变量,能够经过获取len 属性的值,直接知道字符串长度。

2 杜绝缓冲区溢出

C 字符串 不记录字符串长度,除了获取的时候复杂度高之外,还容易致使缓冲区溢出。

假设程序中有两个在内存中紧邻着的 字符串 s1 和 s2,其中s1 保存了字符串“redis”,二s2 则保存了字符串“MongoDb”:

若是咱们如今将s1 的内容修改成redis cluster,可是又忘了从新为s1 分配足够的空间,这时候就会出现如下问题:

本来s2 中的内容已经被S1的内容给占领了,s2 如今为 cluster,而不是“Mongodb”。

 

当须要对一个SDS 进行修改的时候,redis 会在执行拼接操做以前,预先检查给定SDS 空间是否足够,若是不够,会先拓展SDS 的空间,而后再执行拼接操做:

3 减小修改字符串时带来的内存重分配次数

C语言字符串在进行字符串的扩充和收缩的时候,都会面临着内存空间的从新分配问题。

1. 字符串拼接会产生字符串的内存空间的扩充,在拼接的过程当中,原来的字符串的大小极可能小于拼接后的字符串的大小,那么这样的话,就会致使一旦忘记申请分配空间,就会致使内存的溢出。

2. 字符串在进行收缩的时候,内存空间会相应的收缩,而若是在进行字符串的切割的时候,没有对内存的空间进行一个从新分配,那么这部分多出来的空间就成为了内存泄露。

咱们须要对下面的SDS进行拓展,则须要进行空间的拓展,这时候redis 会将SDS的长度修改成13字节,而且将未使用空间一样修改成1字节

 

由于在上一次修改字符串的时候已经拓展了空间,再次进行修改字符串的时候会发现空间足够使用,所以无须进行空间拓展

经过这种预分配策略,SDS将连续增加N次字符串所需的内存重分配次数从一定N次下降为最多N次

4 惰性空间释放

SDS 的free 属性,是用于记录空余空间的。除了在拓展字符串的时候会使用到free 来进行记录空余空间之外,在对字符串进行收缩的时候,也可使用free 属性来进行记录剩余空间,避免下次对字符串进行再次修改的时候,须要对字符串的空间进行拓展。

SDS 提供了相应的API,能够在有须要的时候,自行释放SDS 的空余空间。

经过惰性空间释放,SDS 避免了缩短字符串时所需的内存重分配操做,并未未来可能有的增加操做提供了优化

5 二进制安全

C 字符串中的字符必须符合某种编码,而且除了字符串的末尾以外,字符串里面不能包含空字符,不然最早被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存想图片,音频,视频,压缩文件这样的二进制数据。

Redis 不是靠空字符来判断字符串的结束的,而是经过len这个属性

6 兼容部分C字符串函数

虽然SDS 的API 都是二进制安全的,但同样遵循C字符串以空字符串结尾的惯例。

========================================================================

链表

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,而且能够经过增删节点来灵活地调整链表的长度。

Redis 中 列表键的底层实现之一就是链表。当一个列表键包含了数量较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表做为列表键的底层实现。

每一个链表节点使用一个 listNode结构表示:

typedef struct listNode{
      struct listNode *prev;
      struct listNode * next;
      void * value;  
}

多个链表节点组成的双端链表:

list:

typedef struct list{   
    listNode  * head;//表头节点  
    listNode  * tail;//表尾节点   
    unsigned long len;//链表长度   
    void *(*dup) (void *ptr);//节点值复制函数 
    void (*free) (void *ptr);//节点值释放函数 
    int (*match)(void *ptr, void *key);//节点值对比函数
}

链表的特性

  • 双端:链表节点带有prev 和next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(N)
  • 无环:表头节点的 prev 指针和表尾节点的next 都指向NULL,对立案表的访问时以NULL为截止
  • 表头和表尾:由于链表带有head指针和tail 指针,获取链表头结点和尾节点的时间复杂度为O(1)
  • 长度计数器:链表中存有记录链表长度的属性 len
  • 多态:链表节点使用 void* 指针来保存节点值,而且能够经过list 结构的dup 、 free、 match三个属性为节点值设置类型特定函数。

========================================================================

字典

字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对的抽象数据结构。 

在字典中,一个键(key)能够和一个值(value)进行关联,字典中的每一个键都是独一无二的。在C语言中,并无这种数据结构,可是Redis 中构建了本身的字典实现

10.143.128.165:6379> SET msg "hello world"
OK

字典的定义

1 哈希表

Redis 字典所使用的哈希表由 dict.h/dictht 结构定义:

typedef struct dictht {  
   dictEntry **table; //哈希表数组  
   unsigned long size;//哈希表大小   
   unsigned long sizemask;//哈希表大小掩码,用于计算索引值  
   unsigned long used;//该哈希表已有节点的数量
}

一个空的字典的结构图以下:

在结构中存有指向dictEntry 数组的指针,而咱们用来存储数据的空间就是 dictEntry

 2. 哈希表节点( dictEntry )

typeof struct dictEntry{
   void *key; //键
   union{   //值
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;
}

在数据结构中,key 是惟一的,可是存入里面的key 并非直接的字符串,而是一个hash 值,经过hash 算法,将字符串转换成对应的hash 值,而后在dictEntry 中找到对应的位置。

若是出现hash 值相同的状况,Redis 采用了链地址法:

当k1 和k0 的hash 值相同时,将k1中的next 指向k0 造成一个链表。

3 字典

typedef struct dict {  
    dictType *type;  // 类型特定函数    
    void *privedata; // 私有数据   
    dictht  ht[2];  // 哈希表  
    in trehashidx; // rehash 索引
}

type 属性 和privdata 属性是针对不一样类型的键值对,为建立多态字典而设置的。

ht 属性是一个包含两个项(两个哈希表)的数组

普通状态下的字典:

解决哈希冲突

在插入一条新的数据时,会进行哈希值的计算,若是出现了hash值相同的状况,Redis 中采用了连地址法(separate chaining)来解决键冲突。每一个哈希表节点都有一个next 指针,多个哈希表节点可使用next 构成一个单向链表,被分配到同一个索引上的多个节点可使用这个单向链表链接起来解决hash值冲突的问题。

 如今要插入k2,经过hash 算法计算到k2 的hash 值为2,即须要将k2 插入到dictEntry[2]中:

 在插入后,dictEntry指向了k2,k2的next 指向了k1,从而完成了一次插入操做(这里选择表头插入是由于哈希表节点中没有记录链表尾节点位置)

 

Rehash

随着对哈希表的不断操做,哈希表保存的键值对会逐渐的发生改变,为了让哈希表的负载因子维持在一个合理的范围以内,须要对哈希表的大小进行相应的扩展或者压缩,能够经过 rehash(从新散列)操做来完成。

1 目前的哈希表状态:

2 为哈希表分配空间

若是执行的是拓展操做,那么ht[1] 的大小为第一个大于等于ht[0] 的2的n次幂

若是执行的是收缩操做,那么ht[1] 的大小为第一个大于等于ht[0] 的2的n次幂

所以这里 为ht[1] 分配 空间为8,

3 数据转移

将ht[0]中的数据转移到ht[1]中,在转移的过程当中,须要对哈希表节点的数据从新进行哈希值计算

数据转移后的结果:

4 释放ht[0]

将ht[0]释放,而后将ht[1]设置成ht[0],最后为ht[1]分配一个空白哈希表:

5 渐进式 rehash

在进行拓展或者压缩的时候,能够直接将全部的键值对rehash 到ht[1]中,这是由于数据量比较小。

在实际开发过程当中,这个rehash 操做并非一次性、集中式完成的,而是分屡次、渐进式地完成的。

渐进式rehash 的详细步骤:

一、为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表

二、在几点钟维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash 开始

三、在rehash 进行期间,每次对字典执行CRUD操做时,程序除了执行指定的操做之外,还会将ht[0]中的数据rehash 到ht[1]表中,而且将rehashidx加一

四、当ht[0]中全部数据转移到ht[1]中时,将rehashidx 设置成-1,表示rehash 结束

采用渐进式rehash 的好处在于它采起分而治之的方式,避免了集中式rehash 带来的庞大计算量。

 

 

 

 

 

待续

相关文章
相关标签/搜索