Redis---动态字符串,链表,字典(数据结构)

SDS

Redis构建了一种简单动态字符串,simple dynamic string , SDS,用作默认字符串。

结构:

struct sdshdr{

int len; //记录buf数组中已使用字节的数量

int free; // buf数组中未使用字节的数量

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

}

SDS和C字符串的区别

1:获取字符串长度的复杂度

C语言通过遍历字符串获取长度,SDS有 字段 len专门表示。 两者时间复杂度分别是 O(n) ,O(1)

2:杜绝缓存区溢出

动态修改内存,检查到所需空间内存不够会进行扩展=

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

通过 空间预分配和 惰性空间释放 两种方法

4:采用二进制编码比较安全

链表

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并可以通过增删节点调整链表长度。发布订阅,慢查询,监视器也都用到了链表,并且Redis服务器也使用链表保存客户端的状态信息,以及使用链表构建客户端输出缓冲区。

链表节点结构:

typedef struct listNode{

//前置节点

struct listNode * prev;

//后置节点

struct listNode * next;

//节点的值

void * value;

} listNode;

链表结构:

typedef struct list{

//表头节点

listNode * head;

//表尾节点

listNode * tail;

//链表所包含的节点数量

unsigned long len;

//节点值复制函数--复制链表节点保存的值

void * (*dup)(void * ptr);

//节点值释放函数-- 释放保存的值

void *(*free)(void *ptr);

//节点值对比函数-- 对比链表节点保存的值和另个输入值是否相等

int (*match)(void *ptr,void *key);

}list;

链表实现特性:

1:双端:链表节点带有prev 和next指针,获取某个节点的前置或后置节点复杂度为O(1)

2:无环:表头节点的prev 指针 和表尾节点的next指针都指向 NULL,对链表的访问以NULL为终点

3:带表头指针和表尾指针: 通过list结构的head指针和tail 指针,获取表头节点和表尾节点的复杂度为O(1)

4:带链表长度计数器:使用list结构的len属性对list 持有的链表节点进行计数。获取节点数量的复杂度为O(1)

5:多态,使用 链表节点 保存节点值,通过list结构的 dup ,free, match 为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值

字典

字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个节点只保存字典中的一个键值对。

字典结构:

typedef struct dict {

//是一个指向dictType结构的指针,保存了为不同的字典设置的操作特定类型键值对函数。

dictType * type;

//私有数据

void * privdata;

//哈希表

dictht ht[2];

//rehash 索引, 不在进行时,值为-1

int trehashind;

}

哈希表结构:

typedef struct dictht{

//哈希表数组

dictEntry ** table;

//哈希表大小

unsigned long size;

//哈希表大小掩码,用于计算索引值,总是等于size-1

unsigned long sizemask;

//该哈希表已持有的节点数量

} dictht ;

哈希表节点结构:

typedef struct dictEntry {

//键

void * key;

//值

union {

void * val; --指针

unit64_t u64; --值的类型

int64_t s64; --值的类型

} v;

//指向下个哈希表节点,形成链表。 可以将多个哈希值相同的键值对连接在一起,来解决键冲突的问题。

struct dictEntry * next;

}dictEntry;

类型特定函数的结构

typedef struct dictType{

//计算哈希值的函数

unsigned int (* hashFunction) (const void * key);

//复制键的函数

void *(*keyDup) (void *privdata, const void*key);

//复制值的函数

void *(valDup) (void * privdata, const void * obj);

//对比值的函数

int (*keyCompare) (void * privdata,const void*key1, const void*key2);

//销毁键的函数

void (*keyDestructor) (void *privdata, void *key);

//销毁值的函数

void (*valDestructor) (void *privdata, void *obj);

}

哈希算法

当要添加键值对到字典时,先根据键计算出哈希值和索引值,再根据索引值,将包含键值对的哈希表节点放到指定的哈希表数组索引上。

计算key的hash值方法,Redis使用MurmurHash2算法计算hash值

hash = dict->type->hashFunction(key);

计算key的索引值方法

index = hash & dict->ht[x].sizemask;

键冲突

Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成单向链表,被分配到同一个索引上的多个节点可以用这个单向链表链接起来。 因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度,总是将新节点添加到链表的表头位置,复杂度为O(1)。

rehash(重新散列)

当哈希表保存的键值对数量太多或太少时,为了让负载因(load factor)维持在一个合理范围,需要对哈希表的大小就行扩展或收缩。

1:为字典ht[1]哈希表分配空间:扩展操作,那么 ht[1]的大小为第一个大于等于ht[0].used*2的2的n次幂。 收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2的n次幂

2:将保存在ht[0]中的所有键值对rehash(重新计算哈希值和索引值,然后将键值对放到ht[1]哈希表指定位置上。

3:当ht[0]所有的键值对都迁移到ht[1]后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表。

负载因子的公式: load factor = ht[0].used / ht[0].size;

渐进式rehash

为了避免rehash 对服务器性能影响,服务器分多次,渐进式将ht[0]里面的键值对慢慢的rehash到ht[1] 。

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

2:在字典中维持一个索引计数器变量 rehashidx, 设置为0,表示rehash 开始

3:在rehash期间,每次对字典执行操作时,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash 工作完成之后,将rehashidx属性的值增1;

4:随着字典操作的不断执行,最终所有的键值对都会rehash到ht[1]。这是rehashidx 属性的值设为-1,表示已完成。

在此期间,字典新添加的键值对一律保存到ht[1]中,删除,查找,更新 操作会在两个哈希表上进行。

发布了50 篇原创文章 · 获赞 2 · 访问量 2296

猜你喜欢

转载自blog.csdn.net/eafun_888/article/details/104714510