本周给大家分享一下redis的源码解读,主要内容是redis五种数据类型底层是怎么实现的,今天是第一课String类型。------- 摘自 智线云-李丰-每周小课堂
第一课、简单动态字符串(sds)
字符串的实现是使用c语言自定义的struct结构体,简称为sds(simple dynamic string)简单动态字符串,这个结构体的声明如下:
这个是3.0版本结构体定义
struct sdshdr {
unsigned int len;//存储的字符串长度
unsigned int free;//剩余的没有使用的内存空位
char buf[];//字符串存储数组
};
下面是一个定义示意图:
redis的sds的相关的特性如下:
1)长度获取的时间复杂度计算
redis的字符串类型无论存储多长的字符串在实际测试长度的时候获取的时间复杂度都是O(1),因为存在len,在做数据修改操作的时候维护了一个len,所以获取长度的时候并没有遍历buf数组,直接获取了len的值;
字符串的存储结构如下:
c语言中为了存储一个字符串,使用的一个字符串长度加1的字符数组
传统的c语言字符串测长需要遍历
2)sds的设计杜绝了C语言字符串的缓冲区溢出问题
在C语言中字符串的定义是不记录自身长度的,如果在磁盘中a和b两个字符串是紧邻在一起的,在a的后面继续拼接字符串,但是没有分配足够的内存空间的话,就会导致a的内容溢出占位到b的磁盘空间上,造成缓冲区溢出的问题。
sds的设计杜绝了这个问题,在字符串拼接或者赋值之前会先检测sds的剩余长度够不够,不够的情况下回分配足够的内存空间,然后再进行赋值或者拼接操作。
3)优化后的空间分配策略
传统的C语言字符串,每一次字符串修改操作回涉及内存的重新分配,如果字符串加长,之前分配不够需要增加空间,反之需要减少空间。
内存空间的重新分配是一个耗时操作,对于redis这种在内存中需要高效执行的非关系型数据库,要从设计层面上减少重新分配次数,sds就做了这样的设计:
两种方式内存处理方式:
(1)内存的预先分配
如果修改之后sds的大小小于1M,那个会分配sds长度为len的未使用空间
如果修改之后sbs的大小大于等于1M,那么会分配1M大小的未使用内存空间
这样在下一次加长字符串的时候就有较大几率不需要再做内存空间分配了
(2)惰性释放掉内存空间
惰性释放指的是在对字符串做缩短操作的时候,多出来的内存空间不会被立即释放掉而是记录在free属性中,以便再次对字符串做拼接操作是使用。不过sds有对应的api函数来真正的释放内存空间
4)sds对于二进制是安全的
传统的C语言字符串是以空字符串作为字符串【\0】结尾的,一个c字符串中间是不能包含空字符串的。但是redis的字符串中最终存储的是字节码,字符串的结束是使用len属性来决定的。所以sds对二进制安全,能够用来存储任意格式的二进制字符串,比如图片,音频,视频等。
5)sds能够兼容c的字符串操作的部分函数
sds的在实际存储字符串的时候是和c的字符串定义一样在最后一位存储空字符串作为结尾,所以sds能够兼容一部分c语言的的字符串类库函数比如:
字符串的比较函数:strcasecmp
字符串追加拼接函数:strcat
这样sds就可以复用一些字符串类库函数,不需要重新开发了。
参考文件和链接:
《redis的设计实现》
https://www.cnblogs.com/-wenli/p/13055314.html
https://www.cnblogs.com/hunternet/p/9957913.html
第二课:链表的实现
链表数据结构是redis中列表的实现方式之一,除了列表实现使用到了链表数据结构,redis的订阅与发布,慢查询和监视器等功能也是使用链表实现的。
1)源码定义
组成链表的是链表节点,其在源码中的定义如下:
typedef struct listNode {
struct listNode *prev;//前向指针
struct listNode *next;//后向指针
void *value;//节点的实际值
} listNode;
节点的前后指针能够保证在链表中很容易的获取到前后节点,节点的排序示意如下:
实际链表的定义入下:
typedef struct list {
listNode *head;//链表头指针
listNode *tail;//链表尾指针
void *(*dup)(void *ptr);//节点复制函数
void (*free)(void *ptr);//节点释放函数
int (*match)(void *ptr, void *key);//节点值比较函数
unsigned long len;//当前链表包含的节点数
} list;
链表中定义有链表的头尾节点指针和链表长度,除此之外还有用来实现多态链表的函数,下面是一个长度为3的链表的示意图:
2)链表特性
链表数据结构是redis中列表基础类型的实现方式之一,综合来看有下面的好处:
(1)双端特性,一个链表节点通过指针关联到前后的节点,获取点后节点的时间复杂度是常数级别的;
(2)非环形结构,头结点的前端和后节点的尾端指向的是空
(3)配备头尾指针,获取到头尾节点的时间复杂度是常数级别的
(4)存在长度计数器,获取链表长度的时间复杂度是常数级别的
(5)可以通过属性扩展多态链表
3)为什么选择链表【双向链表】
链表数据结构在节点操作上具有高效性,准确的说时间复杂度更小,下面是双向链表的常见操作的时间复杂度统计:
可以看到双向链表在很多常见操作上具有常数级别的时间复杂度。另外需要说明的是并不是所有的redis列表实现都是使用的链表来实现的,在数据量比较小的时候redis使用的是压缩列表数据结构,这个数据结构后面在详细介绍,只有数据量比较大或者包含节点很多的时候才会选择链表来实现,因为链表节点包含前后指向的指针,需要消耗更多的内存空间,实际上使用的是空间还时间的思想。
参考文档:
第三课、字典
在redis中数据库就是使用字典来实现的,另外字典还是哈希键基础类型的实现数据结构之一。字典还可以被称之为符号表,关联数组或者是映射是一种描述键值对的数据结构。
字典的底层是使用哈希表来实现的【言外之意是说哈希键的底层是使用哈希表来实现的】
一个哈希表里面有多个哈希节点,每个节点可以表示字典中的一个键值对
1)源码结构定义
哈希表的源码定义如下:
typedef struct dictht {
dictEntry **table;//哈希节点数组
unsigned long size;//哈希表的大小
unsigned long sizemask;//哈希表掩码,用来计算数据索引
unsigned long used;//哈希表里面已经存在的哈希节点的数量
} dictht;
下面是一个空哈希表的结构示意图:
图中的dictEntry表示的是哈希节点,下面是哈希节点的定义:
typedef struct dictEntry {
void *key;//键名
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;//值
struct dictEntry *next;//指向下一个哈希表节点形成链表
} dictEntry;
对应的值可以是指向数据的指针,整数【包含uint64_t或者int64_t类型】,或者是double类型的浮点数
*next指针可以用来将多个哈希值相同哈希节点连接起来解决键冲突的问题,下面是使用next指针将两个索引值相同的哈希节点连接起来的示意图:
源码中字典的定义如下:
typedef struct dict {
dictType *type;//持有若干方法用来实现多态字典
void *privdata;//私有数据
dictht ht[2];//两张哈希表
long rehashidx; //rehash索引,rehash不在进行时值 为-1
int iterators; //当前正在运行的迭代器
} dict;
上面的type和privdata是为了针对不同类型的键值对而设置的,type是一个指针指向dictype类型,里面定义了一系列的特殊方法,privdata则表示的是传递给这些方法的可选参数,其中dictype的定义如下:
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);
} dictType;
在字典定义中有一个长度为2的哈希表数组,一般情况下只会使用到ht[0],ht[1]是哈希表进项rehash的时候使用,rehashidx属性表示的是进行rehash是的进度,没有rehash操作时取值为-1【这个rehash是干啥的后面会介绍】
下面是一个没有进行rehash的字典定义示意图:
2)hash值计算和哈希冲突解决
哈希算法
在需要将一个新的键值对放到字典中的时候,就需要通过传递的键名来计算出哈希值和索引值,探后根据索引值将哈希节点放到哈希表的指定位置,分为两个步骤:
1、计算哈希值:
hash = dict->type->hashFunction
2、计算索引值
index = hash & dict->ht[x]->sizemash
其中x表示0或者1,根据实际情况不同取值不同
解决键冲突
经过上面哈希出来的索引值有可能被指向到哈希表的同一个位置,这样就造成了键冲突,哈希节点中定义了next指针,把相同索引位置的哈希节点连接起来构成了单向列表,且新加入节点是排在后加入节点的前面的:
3)rehash操作
这个操作是在哈希表中存储的键值对减少或者增多的时候为了将哈希表的负载控制在一个合理的位置,需要对哈希表进行迁移,这个时候ht[1]就要排上用场了,分为下面三个步骤:
1)为ht[1]分配合适的内存空间
2)将ht[0]的数据迁移到ht[1],完成后初始化ht[0]
3)将ht[0]转变为ht[1],ht[1]转变为ht[0]
上面提到的rehashidx属性在rehash的时候有可能会起到作用,因为在数据量比较大的时候,rehash是一个比较耗时的过程,不可能一次性迁移完毕,这样的情况下就要一个index值来记录当前操作到哪一个位置,rehashidx就是这个记录变量。
第四课、整数集合
1)结构定义
整数集合是redis中集合类型的实现方式之一,当一个集合中所有的值都是整数,且值得数量不多的时候集合的类型会使用整数集合来实现。
整数集合的定义如下:
typedef struct intset {
uint32_t encoding;//编码格式
uint32_t length;//集合长度
int8_t contents[];//集合内容数组
} intset;
encoding表示的编码格式,集合中能够保存的整数类型有int16_t,int32_t,int64_t
length表示的集合中元素的数量
contents 数组是用来存储元素的,这个数组标定的类型是int8_t,实际上不存储int8_t类型的整数,里面存储的整数类型决定于encoding的取值,下面是一个整数集合的示意图:
额外说明encoding的可取值有三种,分别能够表示的数据范围如下:
2)升级操作
在整数集合中一个新的元素被添加到集合中的时候可能会涉及到升级的操作,比如原来的集合中都是int16类型的整数,现在插入式一个int32或者int64才能表示的整数,那么这个时候就需要对整数集合里面的元素进行升级操作,升级的步骤如下:
1)根据新元素类型,扩展数组的空间大小,并为新元素分配内存空间
2)将数组中的现有元素替换成新元素类型的元素,要保持原来的顺序不变;
3)将新元素添加到数组中
下面是一个升级的动态过程:
比如现在有一个集合中存储的是int16类型表示的整数1,2,3:
在内存中的占位如下:
现在要添加一个int32类型的整数65535:
第一步根据新的类型扩展数组的长度为32*4=128,扩展之后的站位如下:
第二步转换现有数据的类型并移动存储位置,移动完毕之后占位如下:
第三步放置新元素:
升级操作的好处有两点:
1)提升整数集合的灵活性;
c语言中通常为了避免类型错误,不会再同一个数据结构中存储不同类型的数据,redis的升级能够自动实现数据类型的统一,避免类型错误
2)一定程度上节约内存空间;
当一个数组需要存储不同类型的整数时,没有升级操作的情况下,我们需要取最大可能存储整数的类型来设置每一个元素的类型,但是整数集合有了升级操作,可以尽量节省空间的情况下,存储更多类型的整数
3)降级操作
整数集合暂时不支持降级操作,比如现在所有的整数中只有一个需要int64类型来表示,这个数被删除之后其他整数的编码类型不会发生变化
参考:
第五课、跳跃表
在redis中的跳跃表示用来实现有序集合的底层数据结构之一,有序集合内包含的元素很多,或者有序集合的节点是存储的比较长的字符串的时候会使用跳跃表作为实现有序集合的方式;
跳跃表是一种有序的数据结构,通过在每个节点中维持多个指向其他节点的指针来达到快速访问节点的目的;
跳跃表的实现依赖于下面两个定义:
1)源码定义
这两个在源码中的定义如下:
节点定义:
typedef struct zskiplistNode {
robj *obj;//对象
double score;//分值
struct zskiplistNode *backward;//后退指针
struct zskiplistLevel {
struct zskiplistNode *forward;//前进指针
unsigned int span;//跨度
} level[];//层
} zskiplistNode;
表定义:
typedef struct zskiplist {
struct zskiplistNode *header, *tail;//头尾节点
unsigned long length;//跳跃表的长度
int level;//除去头结点剩下节点中层级最高的几点的层数
} zskiplist;
2)表示例和属性解释
下面是一个跳跃表的组成示意图:
红色标记的是zskiplist,绿色标记的是zskiplistnode。
跳跃表节点的属性解释如下:
1)层(level数组),每个节点可以包含多个层【最多32层】,层存在的目的是通过当前节点可以快速的访问到其他节点;
层数组里面的元素包含了前进指针和跨度两个属性,前进指针用来访问下一个节点,跨度用来计算当前节点在整个表中的排位【可能有时候会理解成需要通过跨度去遍历,实际上是不需要的,直接通过指针即可访问到下一个节点】
2)后退指针 每一个节点只有一个后退指针,用来从表尾遍历整个跳跃表
3)对象和分值
分值表示的是一个浮点型的数字,在跳跃表中节点按照分值从小到大来排列,而对象实际是一个指针用来指向实际的存储值【比如sds】
多个表节点就可以组成一个跳跃表,zskiplist其实是来维持这些节点的存在的,结构中的头尾指针用来指向跳跃表的头尾节点,所以程序获取头尾节点的时间复杂度为O(1)
length属性则用来快速获取表的长度,level用来快速获取节点中层数最高的节点的层数是多少
参考文献:
- 《redis的设计与实现》
- https://www.cnblogs.com/hunternet/p/11248192.html
- https://blog.csdn.net/WhereIsHeroFrom/article/details/84997418