文章目录
- 动态字符串(SDS)
- 概念
- `SDS`特点
- `SDS`的优势
- IntSet
- 概念
- IntSet的特点
- 升序
- 统一的编码格式
- `IntSet`自动升级
- Dict
- 概念
- Dict特点
- Dict的伸缩
- Dict的扩容
- `Dict`收缩
- `Dict`的`rehash`
- 渐进式哈希
- 总结
- `Dict`的结构
- `Dict`的伸缩
动态字符串(SDS)
概念
Redis是使用C语言实现的,C语言字符串底层是字符数组,结束使用/0来表示,但Redis并没有直接使用C语言的字符串,因为C语言字符串存在多个问题:
- 获取字符串长度需要运算
- 非二进制安全
- 不可修改(保存在常量池中,是不能修改的)
所以Redis创建了一种新的字符串结构,称之为简单动态字符串
Redis是使用C语言实现的,其中SDS是一个结构体
Redis源码:

几个重点:
uint8_t:无符号8比特位整型,所以buf字符数组中最多保存2^8-1=254个字符(此处是sdshdr8类型的SDS)Unsigned char flags:指代SDS的五种类型—SDS_TYPE_5,SDS_TYPE_8,SDS_TYPE_16,SDS_TYPE_32,SDS_TYPE_64
所以,一个SDS字符串的结构如下:

SDS特点
SDS之所以叫做动态字符串,是因为它具备动态扩容的能力

SDS的优势
- 获取字符串长度的时间复杂度为
O(1) - 支持动态扩容
- 减少内存分配次数(因为做了内存的预分配)
- 二进制安全(不以
\0为结束标志,而是以len属性来维护一个字符串)
IntSet
概念
IntSet是Redis中set集合的一种实现方式,基于整数数组来实现,并具备长度可变,有序等特征

uint32_t length:contents[]数组中的元素个数;int8_t contents[]:存放整数数据encoding包含三种模式,表示存储整数的大小;

IntSet的特点
升序
为了方便查找,Redis会将intset中所有的整数按照升序依次保存在contents数组中,结构如图:

统一的编码格式
为了快速定位数据,统一编码格式后,可以根据索引下标和第一个数据的内存地址,算出偏移量.
这里的理解方式如下:

我们获得了第一个数据的内存地址之后,由于编码方式是统一的,那么这里的每一个整数数据都是占有2个字节,那么第三个数据的偏移量为2*2个字节,这样我们就可以直接得出第三个数据的地址,而不需要进行遍历(这也就是我们的索引要从0开始的原因)
计算公式:
startptr+(sizeof(encoding)*index)
刚才的例子中,所有数据可以使用16Byte来表示,那么如果这时加入一个数据,无法使用16Byte来表示了,例如:5000,该怎么办?这就涉及我们的IntSet自动升级了
IntSet自动升级

升级的流程:
- 升级编码为
INTSET_ENC_INT32,每个整数占4字节,并按照新的编码方式以及元素个数扩容数组 - 倒序依次将数组中的元素拷贝到扩容后的正确位置
- 将待添加的元素放到数组末尾
- 最后将
intset的encoding属性改为INTSET_ENC_INT32,将length属性改为4
Dict
概念
我们知道Redis是一个键值型(Key-Value Pair)的数据库,我们可以根据键实现快速的增删改查.而键与值的映射关系正是通过Dict来实现的.
Dict是由三部分组成:哈希表(DictHashTable),哈希节点(DicEntry),字典(Dict)
当我们向Dict添加键值对的时候,Redis首先根据Key计算出hash值(h),然后利用h&sizemask(由于size=sizemask-1与h%size的效果是一样的)来计算元素应该存储到数组中哪个索引的位置
下图是这三部分结构体的设计:

这三者的关系是:

Dict特点
Dict的伸缩
Dict的扩容
Dict的扩容:Dict中的HashTable就是数组结合单向链表来实现的,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低.
Dict在每次新增键值对时都会检查负载因子(LoadFactor=used/size),满足以下两种情况时会触发哈希表扩容:
- 哈希表的
LoadFactor>=1,并且服务器没有执行bgsave或者bgrewriteaof等后台进程(后台进程的对CPU的消耗是很大的); - 哈希表的
LoadFactor>5;
扩容的源码:

注意:这里的扩容大小为used+1,但并不是代表就是扩容used+1,而是扩容到第一个大于used+1的2^n
Dict收缩
Dict收缩:每次删除元素时,也会对负载因子做检查,当LoadFactor<0.1时,会做哈希收缩.
Dict执行哈希收缩的原码:

Dict的rehash
不管是扩容还是收缩,必定会创建新的哈希表,导致哈希表的size和sizemask发生变化,而key的查询与sizemask有关.因此必须对哈希表中的每一个key重新计算索引,插入新的哈希表,这个过程称为rehash.过程是这样的:
- 计算新
hash表的realeSize,值取决于当前要做的是扩容还是收缩:
- 如果是扩容,则新的
size为第一个大于等于dict.ht[0].used+1的2^n - 如果是收缩,则新的
size为第一个的大于等于dict.ht[0].uesd的2^n(不得小于4)
- 按照新的
realeSize申请内存空间,创建dictht,并赋值给dict.ht[1] - 设置
dict.rehashidx=0,标示开始rehash - 将
dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1] - 将
dict.ht[1]赋值给dict.ht[0],给dict.ht[0]初始化为空哈希表,释放原来的dict.ht[1]内存
如图所示:

渐进式哈希
因为新增或者删除操作一般来说都是在**Redis主线程中进行的,所以如果在主线程中触发了rehash操作,在数据量很大的情况下,可能会导致出现主线程阻塞的情况.
所以在Redis当中,rehash是分多次,渐进式**完成的,因此称之为渐进式rehash.流程如下:
- 计算新
hash表的size,值取决于当前要做的是扩容还是收缩:
- 如果是扩容,则新的
size为第一个大于等于dict.ht[0].used+1的2^n - 如果是收缩,则新的
size为第一个的大于等于dict.ht[0].uesd的2^n(不得小于4)
- 按照新的
size申请内存空间,创建dictht,并赋值给dict.ht[1] - 设置
dict.rehashidx=0,标示开始rehash(这里标记成0的原因是进行数据迁移的时候,下标就是从0开始的) - 将
dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]
每次执行到新增,查询 ,修改,删除操作时,都检查一下dict.rehashidx是否大于-1,如果是,则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++.直至dict.ht[0]的所有数据都rehash到了dict.ht[1]
所以,渐进式哈希,相当于一次每次只是迁移了一个哈希节点上的链表 - 将
dict.ht[1]赋值给dict.ht[0],给dict.ht[0]初始化为空哈希表,释放原来的dict.ht[0]内存 - 将
rehashidx赋值为-1,代表rehash结束 - 在
rehash过程中.新增操作,则直接写入ht[1],查询,修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并且执行.这样可以确保ht[0]的数据只减不增,随着rehash最终为空.
总结
Dict的结构
- 类似
java的HashTable,底层是数组加链表来解决哈希冲突
2.Dict包含两个哈希表,ht[0]平常用.ht[1]用来rehash
Dict的伸缩
- 当
LoadFactor大于5或者LoadFactor大于1并且没有子进程任务时,Dict扩容 - 当
LoadFactor<0.1时,Dict收缩 - 扩容大小为第一个大于等于
user+1的2^n - 收缩大小为第一个大于等于
user的2^n
5.Dict采用渐进式hash,每次访问Dict时执行一次rehash rehash时ht[0]只减不增,新增操作只在ht[1]执行,其他操作在两个哈希表



















