Go最新redis源码阅读—dict(字典结构)_字典规则源(6),Golang高级工程师面试实战

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

    double d;
} v; //值 
struct dictEntry \*next; //指向下一个元素指针

} dictEntry;


![在这里插入图片描述](https://img-blog.csdnimg.cn/20201229141803416.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)


**在分析hash之前想一个问题**



> 
> 提一个问题:**dictht[2]为什么会要2个数组存放,真正的数据只要一个数组就够了?**  
>  这其实和Java的HashMap相似,都是数据加链表的结构,随着数据量的增加,hash碰撞发生的就越频繁,每个数组后面的链表就越长,整个链表显得非常累赘。如果业务需要大量查询操作,因为是链表,只能从头部开始查询,等一个数组的链表全部查询完才能开始下一个数组,这样查询时间将无线拉长。  
>   
>  这无疑是要进行扩容,所以第一个数组存放真正的数据,第二个数组用于扩容用。第一个数组中的节点经过hash运算映射到第二个数组上,然后依次进行。那么过程中还能对外提供服务吗?答案是可以的,因为他可以随时停止,这就到了下一个变量rehashidx。  
>   
>  rehashidx其实是一个标志量,如果为-1说明当前没有扩容,如果不为-1则表示当前扩容到哪个下标位置,方便下次进行从该下标位置继续扩容。
> 
> 
> 


## rehash图解


rehash,重新散列,或者扩容。


### rehash条件


根据不同的场景,rehash可分为”扩展空间”和”缩减空间”两种。


当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:


* 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
* 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;



> 
> 其中哈希表的负载因子可以通过公式:  
>  负载因子 = 哈希表已保存节点数量 / 哈希表大小  
>  **load\_factor = ht[0].used / ht[0].size**  
>  比如说, 对于一个大小为 4 , 包含 4 个键值对的哈希表来说, 这个哈希表的负载因子为:  
>  **load\_factor = 4 / 4 = 1**  
>  又比如说, 对于一个大小为 512 , 包含 256 个键值对的哈希表来说, 这个哈希表的负载因子为:  
>  **load\_factor = 256 / 512 = 0.5**
> 
> 
> 


根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行, 服务器执行扩展操作所需的负载因子并不相同, 这是因为在执行 BGSAVE 命令或BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程, 而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存。


**另一方面, 当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作。**


1. 扩展空间以dictAdd函数为入口。在适当的条件下,最终调用dictExpand实现。而dictExpand会为ht[1]重新分配空间,并重置rehashidx索引值,为后面的rehash迁移做铺垫。  
 触发条件:

ht[0].used/ht[0].size>=1 && (dict_can_resize=1 || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
used/size: 负载因子
dict_can_resize: 是否开启resize标识 1:开启 0:关闭
dict_force_resize_ratio:强制resize的条件(默认值为5)

2. 缩减空间的入口函数为dictResize,内部同样调用了dictExpand。redis的server.db定期检查有使用到(于redis.c/htNeedsResize)


### rehash实现


**Redis 对字典的哈希表执行 rehash 的步骤如下:**


1. 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是ht[0].used 属性的值):

 

如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n (2 的 n 次方幂);
如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 。

2. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
3. 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。


**案例**  
 假设程序要对图 4-8 所示字典的 ht[0] 进行扩展操作, 那么程序将执行以下步骤:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201229150433662.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)


* ht[0].used 当前的值为 4 , 4 \* 2 = 8 , 而 8 (2^3)恰好是第一个大于等于 4 的 2 的 n 次方, 所以程序会将 ht[1] 哈希表的大小设置为 8 。 图 4-9 展示了 ht[1] 在分配空间之后, 字典的样子。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201229150520402.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)
* 将 ht[0] 包含的四个键值对都 rehash 到 ht[1] , 如图 4-10 所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/202012291506132.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)
* 释放 ht[0] ,并将 ht[1] 设置为 ht[0] ,然后为 ht[1] 分配一个空白哈希表,如图 4-11 所示。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201229150649527.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)


## 渐进式rehash图解


扩展和收缩都需要将ht[0]里面的所有键值对散列到ht[1]中,但是这个动作并不是一次性完成的,而是分多次,渐进式完成的。


这么做的原因在于,如果ht[0]如果只保存了四个键值对,那么服务器可以在瞬间完成,但是如果里面保存的是四百万,四千万的键值对,那么一次性将这些键值对全部散列到ht[1]中,这个计算量还是很庞大的。


因此,为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部散列到ht[1]中,而是分多次,渐进式慢慢的散列。


**以下是哈希表渐进式 rehash 的详细步骤:**


 
 
 
 
 
 
 
 注 
 
 
 意 
 
 
 观 
 
 
 察 
 
 
 在 
 
 
 整 
 
 
 个 
 
 
 r 
 
 
 e 
 
 
 h 
 
 
 a 
 
 
 s 
 
 
 h 
 
 
 过 
 
 
 程 
 
 
 中 
 
 
 , 
 
 
 字 
 
 
 典 
 
 
 的 
 
 
 r 
 
 
 e 
 
 
 h 
 
 
 a 
 
 
 s 
 
 
 h 
 
 
 i 
 
 
 d 
 
 
 x 
 
 
 属 
 
 
 性 
 
 
 是 
 
 
 如 
 
 
 何 
 
 
 变 
 
 
 化 
 
 
 的 
 
 
 
 
 
 \color{#ef246f}{注意观察在整个 rehash 过程中, 字典的 rehashidx 属性是如何变化的} 
 
 
 注意观察在整个rehash过程中,字典的rehashidx属性是如何变化的


* 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201229153115831.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)
* 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201229153323348.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)
* 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201229153531334.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)


![在这里插入图片描述](https://img-blog.csdnimg.cn/20201229153515489.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201229153614100.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)


* 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201229153631645.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NTgxOTYx,size_16,color_FFFFFF,t_70)


渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。


因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。


另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。


## 源码阅读


### 创建并初始化字典



/* 创建并初始化字典 */
dict *dictCreate(dictType *type,
void *privDataPtr)
{
dict *d = zmalloc(sizeof(*d));
_dictInit(d,type,privDataPtr);
return d;
}

/* Initialize the hash table */
int _dictInit(dict *d, dictType *type,
void *privDataPtr)
{
_dictReset(&d->ht[0]);
_dictReset(&d->ht[1]);
d->type = type;
d->privdata = privDataPtr;
d->rehashidx = -1;//赋值为-1,表示未进行hash
d->iterators = 0;
return DICT_OK;
}

//重置hash表
static void _dictReset(dictht *ht)
{
ht->table = NULL;
ht->size = 0;
ht->sizemask = 0;
ht->used = 0;
}


由dictCreate创建一个字典*d,并将d传入\_dictInit函数。而\_dictInit函数将负责*d初始化操作。在\_dictInit内部调用 \_dictReset初始化ht[0]和ht[1]数据结构。


从\_dictReset函数我们可以看到,新建dict时未对ht[0]、ht[1]分配空间,那么系统会在什么时候进行分配操作呢? 答案是在调用dictAdd操作时.


### 字典添加



int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key);
if (!entry) return DICT_ERR;
dictSetVal(d, entry, val);
return DICT_OK;
}


dictAddRaw会检查d是否存在key,如果存在,则返回NULL,否则创建key节点。  
 dictSetVal:顾名思义,设置节点的值。



dictEntry *dictAddRaw(dict *d, void *key)
{
int index;
dictEntry *entry;
dictht *ht;
//判断是否在进行rehash操作
if (dictIsRehashing(d)) _dictRehashStep(d);
//检查key是否存在,如果存在,则返回NULL
if ((index = _dictKeyIndex(d, key)) == -1)
return NULL;

//判断rehash是否正在进行,如果正在进行,则往ht[1]添加数据,否则添加至ht[0]
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
//创建key节点
entry = zmalloc(sizeof(\*entry));
//将节点的指针指向对应的链表头部
entry->next = ht->table[index];
//添加节点至链表头部
ht->table[index] = entry;
//更新used值
ht->used++;

//设置节点信息
dictSetKey(d, entry, key);
return entry;

}


从上面可以看出,代码执行顺序:dictIsRehashing->\_dictKeyIndex->dictIsRehashing->dictSetKey.细心的童鞋可能注意到,该函数内部调用两次dictIsRehashing。难道在\_dictKeyIndex函数期间dict结构会发生变化么?  
 追踪下\_dictKeyIndex代码:



static int _dictKeyIndex(dict *d, const void *key)
{
unsigned int h, idx, table;
dictEntry *he;
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;
//计算key hash值
h = dictHashKey(d, key);
//查找key,如果存在,则返回-1,否则返回hash索引
for (table = 0; table <= 1; table++) {
//计算hash索引
idx = h & d->ht[table].sizemask;
//从hash索引对应的链表中搜索
he = d->ht[table].table[idx];
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key))
return -1;
he = he->next;
}
//如果rehash未进行,则只需搜索ht[0]
if (!dictIsRehashing(d)) break;
}
return idx;
}


从\_dictKeyIndex内部,可以看到\_dictExpandIfNeeded函数。根据字面意思推测,这个应该与dict空间有关联(即ht->size)。继续追踪\_dictExpandIfNeeded代码



//判断dict是否需要扩展空间
static int _dictExpandIfNeeded(dict *d)
{
//rehash正在进行,则不进行操作
if (dictIsRehashing(d)) return DICT_OK;

//如果size=0,则设置默认大小
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

//当负载因子(used/size)>=1时,对以下两种情况扩展空间。
//1. dict\_can\_resize=1
//2. 达到强制resize条件时(used/size>dict\_force\_resize\_ratio)。
if (d->ht[0].used >= d->ht[0].size &&
    (dict_can_resize ||
     d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
    return dictExpand(d, d->ht[0].used\*2);
}
return DICT_OK;

}


对于新建的dict,执行的代码为dictExpand(d, DICT\_HT\_INITIAL\_SIZE)。DICT\_HT\_INITIAL\_SIZE在dict.h文件被定义,值为4.我们再追踪下dictExpand函数。



int dictExpand(dict *d, unsigned long size)
{
//dict的扩展空间大小:最小一个>=size的2^N数
unsigned long realsize = _dictNextPower(size);

...省略部分代码...

//设置dict大小
n.size = realsize;
//设置hash掩码
n.sizemask = realsize-1;
//初始化table空间
n.table = zcalloc(realsize\*sizeof(dictEntry\*));
n.used = 0;
//如果dict是否为空(初始化操作),则将n设置为ht[0]
if (d->ht[0].table == NULL) {
    d->ht[0] = n;
    return DICT_OK;
}

//将n赋给ht[1],并设置rehash索引
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;

}


从上面的代码可以看出,ht[0]和ht[1]的内存分配都是在这里进行的。对于一个为空的dict,系统会为ht[0]分配空间。对于一个非空的dict,系统则为ht[1]分配空间,并重置rehashidx标识。


现在应该知道dictAddRaw函数内部执行\_dictKeyIndex之后再次调用 dictIsRehashing的原因了吧。


**好了,总结下dictAdd的流程:dictAdd->dictAddRaw->\_dictKeyIndex->\_dictExpandIfNeeded->dictExpand。**


### 字典替换


dictReplace:顾名思义,替换功能,分为两种情形:当key不存在,则进行创建;当key存在,则修改key的值。代码执行流程:dictAdd->dictFind->dictSetVal



int dictReplace(dict *d, void *key, void *val)
{
dictEntry *entry, auxentry;
//如果key不存在,则进行创建
if (dictAdd(d, key, val) == DICT_OK)
return 1;
//如果key存在,则找到相应的节点
entry = dictFind(d, key);
//修改节点的值
dictSetVal(d, entry, val);
…省略部分代码…
}


### 字典删除


dictDelete



/* Remove an element, returning DICT_OK on success or DICT_ERR if the
* element was not found. */
int dictDelete(dict *ht, const void *key) {
return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}


### 扩大或者缩小空间


**dictResize**—>**dictExpand**



/* Resize the table to the minimal size that contains all the elements,
* but with the invariant of a USED/BUCKETS ratio near to <= 1 */
// 缩小hashtable空间 触发rehash的条件
int dictResize(dict *d)
{
int minimal;

if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
minimal = d->ht[0].used;
if (minimal < DICT_HT_INITIAL_SIZE)
    minimal = DICT_HT_INITIAL_SIZE;  // 4
return dictExpand(d, minimal);

}



/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;

dictht n; /\* the new hash table \*/
// 它不断计算 2 的乘幂,直到遇到大于等于 size 参数的乘幂,就返回这个乘幂作为哈希表的大小 这个地方有点意思的
unsigned long realsize = \_dictNextPower(size);  //比size大的2\*\*n

/\* Rehashing to the same table size is not useful. \*/
if (realsize == d->ht[0].size) return DICT_ERR;

/\* Allocate the new hash table and initialize all pointers to NULL \*/
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize\*sizeof(dictEntry\*));
n.used = 0;

/\* Is this the first initialization? If so it's not really a rehashing

* we just set the first hash table so that it can accept keys. */
// 如果0号哈希表为空 那么这是第一次初始化
// 程序将新的哈希表赋给0号哈希表的指针 然后字典处理键值对
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}

/\* Prepare a second hash table for incremental rehashing \*/
d->ht[1] = n;
d->rehashidx = 0;  // 标记可以rehash 
return DICT_OK;

}




---


**\_dictKeyIndex**——>**\_dictExpandIfNeeded**——>**dictExpand**



/* Returns the index of a free slot that can be populated with
* a hash entry for the given ‘key’.
* If the key already exists, -1 is returned
* and the optional output parameter may be filled.
*
* Note that if we are in the process of rehashing the hash table, the
* index is always returned in the context of the second (new) hash table. */
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
unsigned long idx, table;
dictEntry *he;
if (existing) *existing = NULL;

/\* Expand the hash table if needed \*/
if (\_dictExpandIfNeeded(d) == DICT_ERR)
    return -1;
for (table = 0; table <= 1; table++) {
    idx = hash & d->ht[table].sizemask;
    /\* Search if this slot does not already contain the given key \*/
    he = d->ht[table].table[idx];
    while(he) {
        if (key==he->key || dictCompareKeys(d, key, he->key)) {
            if (existing) \*existing = he;
            return -1;
        }
        he = he->next;

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

ot does not already contain the given key */
he = d->ht[table].table[idx];
while(he) {
if (key==he->key || dictCompareKeys(d, key, he->key)) {
if (existing) *existing = he;
return -1;
}
he = he->next;

[外链图片转存中…(img-kVfTdnxS-1715886647238)]
[外链图片转存中…(img-oIptk2dL-1715886647238)]
[外链图片转存中…(img-T4wLTJQN-1715886647238)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值