redis 设计与实现 摘抄笔记

https://yuedu.baidu.com/ebook/f9f378a176eeaeaad1f33085?pn=1&click_type=10010002&isFromWenku=1&rf=https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3DPJVoGbrLWi2TbbozQGpKizg0ho4QGbwxMl7uKsQ0CrGf98vrh_XME1LNPikEiB8uRFCa8TjGBVZKniRKjJVXGO_YfSE8uKAb07VZy6bVA_G%26wd%3D%26eqid%3Ddb3dc8c6000061e0000000065bd07d05

​ Redis 设计与实现学习总结

前言

在描述算法复杂度时,经常用到o(1), o(n), o(logn), o(nlogn)来表示对应算法的时间复杂度, 这里进行归纳一下它们代表的含义: 这是算法的时空复杂度的表示。不仅仅用于表示时间复杂度,也用于表示空间复杂度。

O后面的括号中有一个函数,指明某个算法的耗时/耗空间与数据增长量之间的关系。其中的n代表输入数据的量。

​ 比如时间复杂度为O(n),就代表数据量增大几倍,耗时也增大几倍。比如常见的遍历算法。 再比如时间复杂度O(n^2),就代表数据量增大n倍时,耗时增大n的平方倍,这是比线性更高的时间复杂度。比如冒泡排序,就是典型的O(n^2)的算法,对n个数排序,需要扫描n×n次。再比如O(logn),当数据增大n倍时,耗时增大logn倍(这里的log是以2为底的,比如,当数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度)。二分查找就是O(logn)的算法,每找一次排除一半的可能,256个数据中查找只要找8次就可以找到目标。

O(nlogn)同理,就是n乘以logn,当数据增大256倍时,耗时增大256*8=2048倍。这个复杂度高于线性低于平方。归并排序就是O(nlogn)的时间复杂度。 O(1)就是最低的时空复杂度了,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。 哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(不考虑冲突的话)

 

第1 章​ 1.1 简单动态字符串(SDS)

​ Redis 没有直接使用C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS 用作Redis 的默认字符串表示。Redis中所有的字符串对象的键值,以及键值对应的value值如是字符串对象,其底层都是以SDS形式存储。

​ 1:SDS定义

每个sds.h/sdshdr 结构表示一个SDS 值,如图所示:

struct sdshdr {
// 记录buf 数组中已使用字节的数量
// 等于SDS 所保存字符串的长度
int len;
// 记录buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};

 

(1)free 属性的值为0,表示这个SDS 没有分配任何未使用空间。(2) len 属性的值为5,表示这个SDS 保存了一个五字节长的字符串。(3) buf属性是一个char类型的数组,数组的前五个字节分别保存了'R'、'e'、'd'、'i'、's' 五个字符,而最后一个字节则保存了空字符'\0'。这里遵循空字符结尾这一惯例的好处是就是可以重用一部分C 字符串函数库里面的函数。

1.2 SDS 与C 字符串的区别

​ 根据传统,C 语言使用长度为N+1 的字符数组来表示长度为N 的字符串,并且字符数组的最后一个元素总是空字符'\0'。而redis处于安全性与效率考虑,才有SDS结构体对象形式,保存字符串;优势如下:

(1)读取长度速度快,由于sds字符串长度在其更新和保存的时候已经自己记录到len属性中,这样获取长度复杂度就是O(1),而传统字符数组表示的字符串获取其长度时,需要进行遍历length次,即复杂度O(N);

(2)杜绝缓冲区溢出;由于C 字符串不记录自身长度,会导致由于分配字符数组空间不足,导致缓冲区溢出情况,比如两个字符串拼接时,如果字符串定义过小,就会导致缓冲区溢出;与C 字符串不同,SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API 需要对SDS 进行修改时,API 会先检查SDS 的空间是否满足修改所需的要求,如果不满足的话,API 会自动将SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS 既不需要手动修改SDS 的空间大小,也不会出现前面所说的缓冲区溢出问题。

(3)减少修改字符串时带来的内存重分配次数。Redis 作为数据库,为了减少内存重新分配的次数,降低性能消耗,redis在第一次内存分配后,会多分配预存空间,多分配长度与保存字符串长度len相等,其值保存在free属性中,即在SDS 中,buf 数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由SDS 的free 属性记录。通过未使用空间,SDS 实现了空间预分配和惰性空间释放两种优化策略。、

​ (a)空间预分配

空间预分配用于优化SDS 的字符串增长操作:当SDS 的API 对一个SDS 进行修改,并且需要对SDS 进行空间扩展的时候,程序不仅会为SDS 分配修改所必须要的空间,还会为SDS 分配额外的未使用空间。其中,额外分配的未使用空间数量由以下公式决定:

​  如果对SDS 进行修改之后,SDS 的长度(也即是len 属性的值) 将小于1MB,那么程序分配和len 属性同样大小的未使用空间,这时SDS len 属性的值将和free 属性的值相同。举个例子,如果进行修改之后,SDS 的len 将变成13 字节,那么程序也会分配13 字节的未使用空间,SDS 的buf 数组的实际长度将变成13+13+1=27 字节(额外的一字节用于保存空字符)。​  如果对SDS 进行修改之后,SDS 的长度将大于等于1MB,那么程序会分配1MB 的未使用空间。举个例子,如果进行修改之后,SDS 的len 将变成30MB,那么程序会分配1MB 的未使用空间,SDS 的buf 数组的实际长度将为30 MB + 1MB + 1byte。通过这种预分配策略,SDS 将连续增长N 次字符串所需的内存重分配次数从必定N 次降低为最多N 次。

​ (b)惰性空间释放

​ 惰性空间释放用于优化SDS 的字符串缩短操作:当SDS 的API 需要缩短SDS 保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free 属性将这些字节的数量记录起来,并等待将来使用。当减少buf数组中字符串数据值时,SDS中的len属性会减少,但其free属性并不会减少,会保持原来的长度不变,未使用的空间将会在以后新增元素时用到,同时SDS 也提供了相应的API,让我们可以在有需要时,真正地释放SDS 的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。

(4)二进制安全

​ 传统字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾。这些限制使得C 字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。通过使用二进制安全SDS,而不是C 字符串,使得Redis 不仅可以保存文本数据,还可以保存任意格式的二进制数据。

(5)兼容部分C 字符串函数

​ 字符串和SDS 之间的区别进行了总结

第二章 链表

1 链表和链表节点的实现

​ 每个链表节点使用一个adlist.h/listNode 结构来表示:

typedef struct listNode {
// 前置节点
struct listNode * prev;
// 后置节点
struct listNode * next;
// 节点的值
void * value;
}listNode;

多个listNode 可以通过prev 和next 指针组成双端链表

虽然仅仅使用多个listNode 结构就可以组成链表,但使用adlist.h/list 来持有链表的话,方便链表操作,有点像java集合中LinkedList;

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;

list 结构为链表提供了表头指针head、表尾指针tail,以及链表长度计数器len,而dup、free 和match 成员则是用于实现多态链表所需的类型特定函数: dup 函数用于复制链表节点所保存的值; free 函数用于释放链表节点所保存的值;

 match 函数则用于对比链表节点所保存的值和另一个输入值是否相等。

一个包含三个listNode节点的list列表;Redis 的链表实现的特性可以总结如下:

 双端:链表节点带有prev 和next 指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。 无环:表头节点的prev 指针和表尾节点的next 指针都指向NULL,对链表的访问以NULL 为终点。 带表头指针和表尾指针:通过list 结构的head 指针和tail 指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。 带链表长度计数器:程序使用list 结构的len 属性来对list 持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。 多态:链表节点使用void* 指针来保存节点值,并且可以通过list 结构的dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

第三章:字典

1:字典的实现

Redisd的字典使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点,而名哈希表节点就保存了字典中的一个键值对。

1.1 哈希表

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

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

1.2 哈希表节点

​ 哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着键值对:

typedef struct dictEntry{
  //键
  void *key;
  //z值,表示键值对值的值可以是指针,或整数
  union{
    void *val;
    uint64_tu64;
    int64_ts64;
  }v;
  //指向下个哈希表节点,形成链表
  struct dictEntry *next;
}dictEntry;

next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对链接在一起,以此解决键冲突(collision)的问题,简称拉链法,与java HashMap 键值冲突处理类似;

1.3 字典

//Rdeis 中的字典有dict.h/dict 结构表示
typedef struct dict{
  //类型特定函数
  dictType *type;//保存了一簇用于操作特定类型键值对的函数,类似java map中的方法
  //私有数据
  void *privdata;//保存了需要传给那些类型特定函数的可选参数
  //哈希表
  dictht ht[2];//包含两个dictht哈希表,一般情况只使用ht[0],ht[1]只会在对ht[0]进行rehash时用到
  //rehash 索引,当rehash不在进行时,值为-1;//记录rehash目前的进度
  in trehashidx;
}dict;

1.4 哈希算法

​ 当需要字典中哈希表添加新的键值对时,首先根据键值key,计算出哈希值和索引值,找到在哈希表数组中索引位置;hash值计算运行MurmurHash2算法,得出值;然后hash值与哈希表中的maskSize掩码值相与,得出索引值。同时为了解决哈希值冲突问题,哈希表使用链地址法,将相同哈希值得节点,同一节点属性next指针构成单向链表,由于dictEntry节点组成链表没有指向表为的指针,所有为了追求速度,新增节点总是排在单链表的表头位置。

1.5 rehash(重新散列)

​ 当哈希表保存值不在其负载因子(ht[0]表used/size )合理的返回内时,哈希表就会进行收缩或扩展操作。

同时rehash的扩展收缩在大数据量下时分多次,渐进式完成的;

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值