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]中,删除,查找,更新 操作会在两个哈希表上进行。