Redis 源码分析(1):字典和哈希表(dict.c 和 dict.h)
http://huangz.iteye.com/blog/1455808
两个点:
字典结构的运作流程
哈希表的渐进式 rehash操作
哈希表是 redis 的核心结构之一,在 redis 的源码中, dict.c 和 dict.h 就定义了哈希结构。

dict 、 dictht 和 dictEntry 这三个核心数据结构

/* 字典结构 */
typedef struct dict {
dictType *type; // 为哈希表中不同类型的值所使用的一族函数
void *privdata; //传给类型特定函数的可选参数
dictht ht[2]; // 每个字典使用两个哈希表
int rehashidx; // 指示 rehash 是否正在进行,如果不是则为 -1
int iterators; // 当前正在使用的 iterator 的数量
} dict;

代码的注释基本都说明相关属性的作用了,需要补充的一些是:

每个字典使用两个哈希表,是因为要实现渐增式 rehash ,redis 会逐个逐个地将 0 号哈希表的元素移动到 1 号哈希表,直到 0 号哈希表被清空为止,

另外, rehashidx 记录的实际上是 rehash 进行到的索引,比如如果 rehash 进行到第 10 个元素,那么 rehashidx 的值就为 9,以此类推,如果没有在进行 rehash ,rehashidx 的值就为 -1 。

哈希表结构 —— dictht 结构,这个哈希表是一个 separate chaining hash table 实现,它通过将哈希值相同的元素放到一个链表中来解决冲突问题:
typedef struct dictht {
dictEntry **table; // 节点指针数组
unsigned long size; // 桶的数量
unsigned long sizemask; // mask 码,用于地址索引计算
unsigned long used; // 已有节点数量
} dictht;

table 属性组成了一个数组,数组里带有节点指针,用作链表。

size 、 sizemask 和 used 这三个属性初看上去让人有点头晕,实际上,它们分别代表的是:
size :桶的数量,也即是, table 数组的大小。
sizemask :这个值通过 size - 1 计算出来,给定 key 的哈希值计算出来之后,就会和这个数值进行 & 操作,决定元素被放到 table 数组的那一个位置上。
used :这个值代表目前哈希表中元素的数量,也即是哈希表总共保存了多少 dictEntry 结构。

链表节点结构
typedef struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64;
int64_t s64;
} v; // 值(可以有几种不同类型)
struct dictEntry *next; // 指向下一个哈希节点(形成链表)
} dictEntry;

字典创建流程

在初步解了几个核心数据结构之后,是时候可以来看看相关的函数怎么来使用这些数据结构了,让我们从最开始的创建字典开始,一步步研究字典(以及哈希表)的运作流程。

因为调用流程可以给我们一个高层次的观点来了解数据结构的运作流程,而不必陷入到代码细节中,因此,文章这里只给出程序调用流程的部分核心代码,如果你对代码的其他细节有兴趣,可以到我的 github 上去找注释版的代码,上面有完整的代码,而且我给大部分函数都加上了注释。

OK,说回来字典这边,创建新字典执行的调用链是: dictCreate -> _dictInit -> _dictReset

其中 dictCreate 函数为 dict 结构分配了空间,然后将新的 dict 传给 _dictInit 函数,让它初始化 dict 结构的相关属性,而 _dictInit 又调用 _dictReset ,对字典的 ht 属性(也即是两个哈希表)进行常量属性的设置。

注意, _dictReset 只是为字典所属的两个哈希表进行常量属性的设置(size、 sizemask 和 used),但并不为哈希表的链表数组分配内存:

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

0 号哈希表的创建流程

我们知道,一个 dict 结构使用两个哈希表,也即是 d->ht[0] 和 d->ht[1] ,为了称呼方便,我们将他们分别叫做 0 号和 1 号哈希表。

从上一节可以知道, dictCreate 并不为哈希表的链表数组分配内存( d->ht[0]->table 和 d->ht[1]->table 都被设为 NULL),那么,什么时候哈希表的链表数组会被初始化呢?

答案是,当首次通过 dictAdd 向字典添加元素的时候, 0 号哈希表的链表数组会被初始化。

首次向字典增加元素将执行以下的调用序列: dictAdd -> dictAddRaw -> _dictKeyIndex -> dictExpandIfNeeded -> dictExpand

其中 dictAdd 是 dictAddRaw 的调用者, dictAddRaw 是添加元素这一工作的底层实现,而 dictAddRaw 为了计算新元素的 key 的地址索引,会调用 _dictKeyIndex :

dictEntry *dictAddRaw(dict *d, void *key)
{
// 被省略的代码...

// 计算 key 的 index 值
// 如果 key 已经存在,_dictKeyIndex 返回 -1
if ((index = _dictKeyIndex(d, key)) == -1)
return NULL;

// 被省略的代码...
}

然后 _dictKeyIndex 会在计算 地址索引前,会先调用 _dictExpandIfNeeded 检查两个哈希表是否有空间容纳新元素:
static int _dictKeyIndex(dict *d, const void *key)
{
// 被省略的代码...

/* Expand the hashtable if needed */
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;

// 被省略的代码...
}

到 _dictExpandIfNeeded 这步,一些有趣的事情就开始发生了, _dictExpandIfNeeded 会检测到 0 号哈希表还没有分配任何空间,于是它调用 dictExpand ,传入 DICT_HT_INITIAL_SIZE 常量作为 0 号哈希表的初始大小(目前的版本 DICT_HT_INITIAL_SIZE = 4 ),为 0 号哈希表分配空间:
static int _dictExpandIfNeeded(dict *d)
{
// 被忽略的代码...

/* If the hash table is empty expand it to the intial size. */
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

// 被忽略的代码...
}

dictExpand 会创建一个分配了链表数组的新哈希表,然后进行判断,决定是该将新哈希表赋值给 0 号哈希表还是 1 号哈希表。
这里因为我们的 0 号哈希表的 size 还是 0 ,因此,这里会执行 if 语句的第一个 case ,将新哈希表赋值给 0 号哈希表:
int dictExpand(dict *d, unsigned long size)
{
// 被省略的代码...

// 计算哈希表的(真正)大小
unsigned long realsize = _dictNextPower(size);

// 创建新哈希表
dictht n;
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*)); // 分配链表数组
n.used = 0;

// 字典的 0 号哈希表是否已经初始化?
// 如果没有的话,我们将新建哈希表作为字典的 0 号哈希表
if (d->ht[0].table == NULL) {
d->ht[0] = n;
} else {
// 否则,将新建哈希表作为字典的 1 号哈希表,并将它用于 rehash
d->ht[1] = n;
d->rehashidx = 0;
}

// 被省略的代码...
}

字典的扩展和 1 号哈希表的创建

在 0 号哈希表创建之后,我们就有了一个可以执各式各样操作(添加、删除、查找,诸如此类)的字典实例了。

但是这里还有一个问题: 这个最初创建的 0 号哈希表非常小(当前版本的 DICT_HT_INITIAL_SIZE = 4),它很快就会被元素填满,这时候, rehash 操作就会被激活。

字典:dict.c/dict.h的更多相关文章

  1. Python基础8:列表推导式(list)字典推导式(dict) 集合推导式(set)

    推导式分为列表推导式(list),字典推导式(dict),集合推导式(set)三种 1.列表推导式也叫列表解析式.功能:是提供一种方便的列表创建方法,所以,列表解析式返回的是一个列表格式:用中括号括起 ...

  2. [转载]python中将普通对象作为 字典类(dict) 使用

    目前我知道的有两种方法: 1 定义的类继承dict类 例如 class A(dict): pass a = A() a['name'] = 12 2 给自定义的类添加 __setitem__() __ ...

  3. 比较字典推导式/dict()/通过键来构造的字典的速率 笔记

    # 下面结果执行一次不容易出差距,所以都执行100000次 import time dict1 = {'a':1, 'b':2, 'c':3, 'd':4} # 第一种:字典推导式 start_tim ...

  4. Pythhon 字典 key in dict 比 dict.has_key (key)效率高 为什么?

    has_key是去取key对应的值,时间复杂度在最优情况下为O(1); in 是直接去dict.__contains__这个保存这key的list中去获取,相当与是去数组中获取. 所以in 比has_ ...

  5. 查询set、dict、dict.keys()的速度对比

    查找效率:set>dict>list 单次查询中: list set dict O(n) set做了去重,本质应该一颗红黑树 (猜测,STL就是红黑树),复杂度 O(logn): dict ...

  6. python字典构造函数dict(mapping)解析

    Python字典的构造函数有三个,dict().dict(**args).dict(mapping),当中第一个.第二个构造函数比較好理解也比較easy使用, 而dict(mapping)这个构造函数 ...

  7. 【Redis源代码剖析】 - Redis内置数据结构之字典dict

    原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51018337 今天我们来讲讲Redis中的哈希表. 哈希表在C++中相应的是ma ...

  8. Python——字典dict()详解

    一.字典 字典是Python提供的一种数据类型,用于存放有映射关系的数据,字典相当于两组数据,其中一组是key,是关键数据(程序对字典的操作都是基于key),另一组数据是value,可以通过key来进 ...

  9. python中集合set,字典dict和列表list的区别以及用法

    python中set代表集合,list代表列表,dict代表字典 set和dict的区别在于,dict是存储key-value,每一个key都是唯一的,set相对于dict存储的是key,且key是唯 ...

随机推荐

  1. junit5荟萃知识点(一):junit5的组成及安装

    1.什么是junit5? 和之前的junit版本不一样,junit5是由三个模块组成. JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage ...

  2. HTML页面布局

    接下来的下面代码,只是给了一个大的前端编写布局,如果你已经是牛人了,就当没看到,如果是一些初学者,不妨拿去用用,里面也写了一些常用的css样式,现在虽然有很多牛逼的前段框架,用起来也非常得心应手,但是 ...

  3. setlocale同mbstowcs函数的关系(VS2008下setlocale(LC_ALL, "chs")可以执行成功,BCB使用setlocale(LC_ALL, "Chinese (Simplified)_People's Republic of China"),linux上locale别名表大概在 /usr/lib/X11/locale/locale.alias)

    序中,如果要将ASCII码字符串转换为宽字符(Unicode),可以利用标准C的mbstowcs函数. 微软在MSDN中有示例,如下: 然而,这段代码在处理含有汉字的字符串时就会出现问题.比如将: w ...

  4. 我的Android进阶之旅------>Android百度地图定位SDK功能学习

    因为项目需求,需要使用百度地图的定位功能,因此去百度地图开发平台下载了百度地图的Android定位SDK最新版本的开发包和示例代码学习. Android 定位SDK地址:http://develope ...

  5. [Idea]安装avtiviti插件以及 插件中文乱码

    安装插件 打开IDEA,按ctrl+alt+S,打开Pluging 乱码问题 idea 安转activiti插件后,编辑流程图发现保存后中文乱码,并且idea的字符集(Settings—>Edi ...

  6. C#如何使用结构化异常处理

    Knowledge Base: Chinese (Simplified) 如何使用 Visual C# .NET 和 Visual C# 2005 中的结构化异常处理文章ID: 816157 最近更新 ...

  7. python基础知识——包

    包是一种通过使用“模块名”来组织python模块的名称空间的方式. 无论是import形式还是from...import形式,凡是在导入语句中(不是在使用时)遇到带点的,就需要意识到——这是包. 包是 ...

  8. 转:USB枚举

  9. java word导入导出工具类

    package com.shareworx.yjwy.utils; import java.io.InputStream; import java.util.HashMap; import java. ...

  10. Pro*C基础

    SQL变量的申明: EXEC SQL BEGIN DECLARE SECTION; 类型 变量名[长度] varchar2 serv_number[]; 其中可以定义C变量 EXEC SQL END ...