1.SDS动态字符串:
Redis中key保存的是字符串,value也往往是字符串或者字符串的集合,不过,Redis并未直接使用C语言中的字符串,因为C语言中的字符串存在一些问题,比如获取字符串长度需要通过运算且字符串不可修改,字符串是非二进制安全的,由此,Redis基于C语言构建了简单动态字符串SDS结构。
1.1 SDS结构实现
SDS是基于C语言中的结构体实现的,每个SDS有四个属性,buf[]为数据存储数组,len为buf中已保存的数据字节数(不包含结束标识),alloc表示buf申请的总字节数(包含结束标识),flags表示不同SDS类型,Redis中定义了多种类型的SDS,不同类型的SDS存储不同大小的头部和数据存储能力。


1.2 SDS动态扩容
SDS具备动态扩容的能力,例如,我们将一个内容为"hi"的SDS追加一段字符串",Amy",首先会申请新内存空间,进行内存预分配操作:
如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1
如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1


基于SDS,我们实现了在O(1)时间复杂度获取字符串长度,实现了字符串的动态扩容,减少了内存分配次数,以及实现了二进制安全。
2.Inset结构
2.1 InSet结构实现
inset结构是Redis的Set集合的一种实现方式(当数据量较少并且只包含整数元素时Set结构采用Inset),也是基于结构体实现,具备长度可变,有序等特征,每个结构体有三个属性:content[]为整数数组,保存集合数据;length表示元素个数;encoding表示编码方式(即content数组中存储的具体数据类型),encoding有三种模式。
下图中的int8_t contents[]
是 C 语言里的柔性数组成员,它本身不占用结构体固定内存,只是一个 "占位符"。编译时,结构体实际大小不包含这个数组,运行时会动态分配内存。

为了提高查找效率,Redis会将数据按升序依次放到content数组中,如图所示:encodeing占4字节,length占4字节,contents占2*3=6字节
2.2 Inset升级流程
此时,我们向inset中添加一个数字:50000,这个数字超出了2字节的存储范围,因此,inset会自动升级为合适的编码方式。
首先,encoding会升级为INTSET_ENC_INT32,每个整数占4字节,并按照新的编码方式扩容数组,扩容过程中倒序依次将数组中的元素拷贝到扩容后的位置:



之后,将待添加的元素加入到数组末尾,最后,inset的encoding属性改为INTSET_ENC_INT32,length属性改为4
升级过程源码:


Inset可以看作特殊类型的整数数组,会确保元素的唯一性和有序性,具备类型升级机制,可以节省内存空间,并且底层采用二分查找的方式来查找元素
3.Dict数据结构
3.1 Dict结构实现
Redis是一个键值型数据库,可以通过键对数据进行CRUD操作,而键与值的映射关系通过Dict实现,Dict由三部分构成:哈希表(DictHashTable),哈希节点(DictEntry),字典(Dict)

当向Dict添加键值对时,Redis会首先根据Key计算出hash值,然后利用h & sizemask计算元素应存储到数组中哪个索引位置。


3.2 Dict扩容机制
Dict中的HashTable使用的是数组加链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,查询效率大大降低。
Dict在每次新增键值对时会监测负载因子(Load=userd/size),即哈希表中使用的节点个数除以哈希表大小,如果负载因子满足以下两种情况,会触发哈希表扩容机制:
load>=1并且服务器没有执行 BGSAVE 或者 BGREWRITEAOF 等后台进程;
load>5;
同样,每次删除元素时,都会对负载因子做检查,当load<0.1时,会做收缩操作。

3.3 Rehash操作
不管是扩容还是收缩,必然会创建新哈希表,导致哈希表的size和sizemask变化,而key查询又依赖于sizemask,因此,必须对哈希表中每一个key重新计算索引,插入到新的哈希表,此过程称为rehash,过程如下:
计算新哈希表的realSize,值取决于当前要做扩容还是收缩操作:
如果是扩容,则新size为第一个大于等于dict.ht[0].used + 1的2^n;
如果是收缩,则新size为第一个大于等于dict.ht[0].used的2^n (不得小于4);
根据新的realSize创建申请新的内存空间,创建dicthet,并赋值给dict.ht[1]
设置rehashidx=0,表示开始进行rehash
将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]
将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来dict.ht[0]的内存



扩容操作:


Dict的rehash并不是一次性完成的,如果数据量非常大,要在一次rehash完成可能会导致主线程阻塞,因此rehash是渐进式完成的,每次执行增删改查操作时,都会检查dict.rehashidx是否大于-1,如果是,则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++,直到dict.ht[0]所有数据都rehash到dict.ht[1],rehash完成,将rehashidx赋值为-1。相当于每次只rehash一个节点的链表,进行删改查操作时,两个哈希表都去找,找到数据后进行操作。进行新增操作时,直接写入ht[1]。