Redis dict.c源码阅读

dict.c解读



一、dict 概念

1.背景

Redis是一个使用ANSI C编写的开源、支持网络、基于内存、分布式、可选持久性的键值对存储数据库。

从2015年6月开始,Redis的开发由Redis Labs赞助,而2013年5月至2015年6月期间,其开发由Pivotal赞助。

在2013年5月之前,其开发由VMware赞助。

根据月度排行网站DB-Engines.com的数据,Redis是最流行的键值对存储数据库。

2. 一些定义

  • 字典,是一种用于实现键值对(key-value pair)保存的抽象数据结构,通过字典,可以在单个键(key)与单个值(value)之间进行关联(或者说是将键映射成值),而这些关联的键与值即为键值对。
  • 对于键值对,字典中的每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对等。
  • 映射过程,通常使用hash算法实现,因此也称映射过程为哈希化,存放记录的数组叫做散列,或哈希表。
  • 冲突:对不同的关键字,可能得到同一个散列地址,即不同的key散列到同一个数组下标(即哈希桶)。处理方法,最常用的是拉链法,即在冲突的下标处,维护一个链表,所有映射到该下标的记录,都添加到该链表上。也就是说,一个哈希表可以有多个哈希结点,每一个哈希节点保存了一组键值对。
  • 哈希桶:dict由数组+链表构成,数组的每个元素占用的槽位就叫做哈希桶(bucket),也就是数组下标,当出现散列冲突,就会在这个桶下挂一个链表,解决冲突。

哈希桶解决冲突举例,即拉链法:

哈希桶解决冲突举例,即拉链法

3. 如何实现

Redis字典dict 的底层实现,本质就是用数组+链表,实现了分布式哈希表。

当不同的关键字、散列到数组相同的位置,用链表维护冲突的记录。当冲突记录越来越多、链表越来越长,遍历列表的效率就会降低,此时需要考虑将链表的长度变短。

将链表的长度变短,一个最直接有效的方式就是扩容数组。将数组+链表结构中的数组扩容,数组变长、对应数组下标就增多了;将原数组中所有非空的索引下标、搬运到扩容后的新数组,经过重新散列,自然就把冲突的链表变短了。即rehash过程。

二、dict 的结构

1.哈希表结点

哈希结点,也称为桶,在dict.c/dictEntry中进行了定义:

//实际存放数据的地方
struct dictEntry {
    //键
    void *key;
    //值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下一个哈希节点,构成单向链表,用以解决键冲突(链地址法)
    struct dictEntry *next;    
};
  • key:记录哈希值对应的数组位置。
  • v:即值(value),当它的值是uint64_t、int64_t或double类型时,就不再需要额外的存储,这有利于减少内存碎片。v也可以是void指针,以便能存储任何类型的数据。
  • next:指向另一个dictEntry结构

2.字典

字典,在dict.h/dict中定义:

//字典结构体
struct dict {
	//type中包含一系列哈希表需要用到的函数
    dictType *type;
	
	//指向实际的哈希表记录
    dictEntry **ht_table[2];
    //哈希表中记录数量
    unsigned long ht_used[2];
	
	//当rehashidx值为1时,不进行rehash
    long rehashidx; 

    //在末尾保留小变量以获得最佳(最小)结构填充
    //当pauserehash大于0,rehashing暂停,小于0则表示编码错误
    int16_t pauserehash; 
    //大小指数,size = 2的exp次方
    signed char ht_size_exp[2]; 
};
  • type:指向dictType结构的指针,每个 dictType 结构保存了一组用于操纵特定类型键值对的函数,而 Redis 会为不同用途的字典设置不同的类型特定函数。
  • ht_table:每一个字典(dict)都有两个哈希表结点(dictEntry),用来实现渐进式 rehash ,记录保存在 ht_table 数组当中。并且,在字典当中,一般只会使用 ht_table[0] 哈希表,ht_table[1]中没有任何数据,只有在对 ht_table[0] 哈希表进行 rehash 时才会用到 ht_table[1]。
  • ht_used:用于记录两个哈希表中维护冲突的记录,分别储存在ht_used[0]和ht_used[1]中。
  • rehashidx:用来记录rehash进度。当rehash没有在进行的时候,rehashidx = -1。

其中,dictType函数在dict.h/dictType中定义:

//dictType结构包含若干函数指针,用于对key和value的各种操作进行自定义
typedef struct dictType {
	//计算哈希值
    uint64_t (*hashFunction)(const void *key);
    //复制键
    void *(*keyDup)(dict *d, const void *key);
    //复制值
    void *(*valDup)(dict *d, const void *obj);
    //对比键
    int (*keyCompare)(dict *d, const void *key1, const void *key2);
    //销毁键
    void (*keyDestructor)(dict *d, void *key);
    //销毁值
    void (*valDestructor)(dict *d, void *obj);
    //在字典初始化/rehashing开始时调用(已创建旧 ht 和新 ht)
    int (*expandAllowed)(size_t moreMem, double usedRatio);
    //在字典初始化/rehashing从旧 ht 到新 ht 的所有条目结束时调用。
    //这两个 ht 仍然存在,并在此回调后被清理。
    void (*rehashingStarted)(dict *d);
    //如果设置了“no_value”标志,则表示未使用值,即字典是集合。
    //设置此标志后,无法访问 dictEntry 的值,也无法使用 dictSetKey()。条目元数据也不能使用。
    void (*rehashingCompleted)(dict *d);
    //如果 no_value = 1,并且所有键都是奇数 (LSB=1),则设置 keys_are_odd = 1 
    //将启用另一个优化:在没有分配 dictEntry 的情况下存储键。
    unsigned int no_value:1;
    //TODO:添加一个“keys_are_even”标志,如果设置了该标志,则使用类似的优化。
    unsigned int keys_are_odd:1;
} dictType;
  • hashFunction,对key进行哈希值计算的哈希算法。
  • keyDup和valDup,分别定义key和value的拷贝函数,用于在需要的时候对key和value进行深拷贝,而不仅仅是传递对象指针。
  • keyCompare,定义两个key的比较操作,在根据key进行查找时会用到。
  • keyDestructor和valDestructor,分别定义对key和value的销毁函数。

3.字典迭代器

字典迭代器(dictIterator),在dict.h/dictIterator中定义:

//字典迭代器
typedef struct dictIterator {
	//被迭代的字典
    dict *d;
	//迭代器当前所指的哈希表的索引位置
    long index;
    //table : 正在被迭代的哈希表,值可以是 0 或 1 。
    //safe : 标识这个迭代器是否安全
    int table, safe;

	//entry : 当前迭代到的哈希结点
	//nextentry : 当前迭代到的哈希节点的下一个哈希结点
    dictEntry *entry, *nextEntry;
   
    //用于误用检测的不安全迭代器指纹。
    unsigned long long fingerprint;
} dictIterator;

迭代器的作用就是遍历字典,遍历对象为dictEntry结点。安全的迭代器会阻止迭代过程中的rehash。

  • d:指向dict结构的指针,保存了当前迭代器正在处理的字典。
  • index:用来表示迭代器当前所指向的哈希表的索引位置。
  • table:用来表示当前正在被迭代的哈希表,即dict结构中的dictEntry数组ht_table的下标,取值有 0 或 1 。
  • safe:用来标识这个迭代器是否安全,safe=1时,该迭代器安全,可以在迭代时对字典进行增删改等操作,即调用 dictDelete、dictFind等函数,否则,不安全的迭代器只能使用 dictNext 遍历函数,而不能对字典进行修改。
  • entry:指向dictEntry结构的指针,指向当前迭代到的哈希节点。
  • nextEntry:指向dictEntry结构的指针,指向当前迭代到的哈希节点的下一个哈希节点。这个属性存在的原因是,在安全迭代器运行时,entry 指针所指向的节点可能会被修改,所以需要一个指针来保存下一节点的位置,来防止指针丢失。
  • fingerprint:字典的指纹,用于误用检测,即在不安全的迭代器对字典进行操作后,与操作前的指纹值进行对比,若不同,则说明在迭代过程中有对字典进行增删操作,而这些操作在不安全的迭代器中是不被允许的。

4.宏定义常量

//字典的操作状态
//操作成功
#define DICT_OK 0
//操作失败
#define DICT_ERR 1
//每个哈希表的初始大小
#define DICT_HT_INITIAL_EXP      2
#define DICT_HT_INITIAL_SIZE     (1<<(DICT_HT_INITIAL_EXP))
  • DICT_OK:字典操作状态,表示操作成功。
  • DICT_ERR:字典操作状态,表示操作失败或出错。
  • DICT_HT_INITIAL_EXP:哈希表的初始大小的指数级别为2 。
  • DICT_HT_INITIAL_SIZE:哈希表的初始大小为以2为底数,DICT_HT_INITIAL_EXP值为指数的数字的值,即22=4。

三、dict 的 API

(一)哈希函数

//哈希函数
//设置哈希种子
void dictSetHashFunctionSeed(uint8_t *seed) {
    memcpy(dict_hash_function_seed,seed,sizeof(dict_hash_function_seed));
}
//获取哈希种子
uint8_t *dictGetHashFunctionSeed(void) {
    return dict_hash_function_seed;
}
//对字符串进行哈希
uint64_t dictGenHashFunction(const void *key, size_t len) {
    return siphash(key,len,dict_hash_function_seed);
}
//对大小写不敏感的字符串进行哈希
uint64_t dictGenCaseHashFunction(const unsigned char *buf, size_t len) {
    return siphash_nocase(buf,len,dict_hash_function_seed);

(二)常用 API

1. dict 的创建

//这四个函数都为 dict 的创建做准备,彼此相互套用
//重设Hash表,设置好相应值
/* Reset hash table parameters already initialized with _dictInit()*/
static void _dictReset(dict *d, int htidx)
{
    d->ht_table[htidx] = NULL;
    d->ht_size_exp[htidx] = -1;
    d->ht_used[htidx] = 0;
}

//创建一个新的 Hash 表,为表分配存储空间,初始化表的内部结构
/* Create a new hash table */
dict *dictCreate(dictType *type)
{
    dict *d = zmalloc(sizeof(*d));
    _dictInit(d,type);
    return d;
}

//创建字典数组
/* Create an array of dictionaries */
dict **dictCreateMultiple(dictType *type, int count)
{
    dict **d = zmalloc(sizeof(dict*) * count);
    for (int i = 0; i < count; i++) {
        d[i] = dictCreate(type);
    }
    return d;
}


//初始化 Hash 表
/* Initialize the hash table */
int _dictInit(dict *d, dictType *type)
{
	//_dictReset函数初始化dict的两个表格
    _dictReset(d, 0);
    _dictReset(d, 1);
    //给字典的内部结构赋值,即初始化
    d->type = type;
    d->rehashidx = -1;
    d->pauserehash = 0;
    //返回初始化成功
    return DICT_OK;
}
  • d:操作对象。
  • type:字典类型。
  • htidx:标志字典是否正在rehash,如果是值为1,不是为0 。用来表明操作字典中的第几个哈希表。

2. 对 dict 进行操作的函数

刚创建好的dict 不能存储任何数据,其两个哈希表的size都为0,所以要对字典进行一些操作。

(1)dictResize

缩小给定的字典的负载因子,即 ratio = ht_table[0].used / ht_table[0].size,让其比例接近于 1 : 1。

//将表的大小调整为包含所有元素的最小大小,但 USED/SIZE 比率不变,接近 <= 1
int dictResize(dict *d)
{
    unsigned long minimal;
	
	//dict_can_resize是一个标志,在dict.c文件开头用静态常量定义。
	//如果dict_can_resize不等于enable,或者字典满足rehash条件,返回err
    if (dict_can_resize != DICT_RESIZE_ENABLE || dictIsRehashing(d)) return DICT_ERR;
    //最小大小设置为哈希表的冲突记录数,如果该值小于哈希表初始值4
    //则将minimal设置为4,并返回dictExpand函数对字典进行扩容处理。
    minimal = d->ht_used[0];
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);
}

注:负载因子:就是used与size的比值,也称装载因子(load factor)。dictEntry中用used记录了冲突的记录数,size记录了数组大小,这个比值越大,哈希值冲突概率越高。当比值超过某个阈值,会强制进行rehash。这个阈值为5,在dict.c文件开头用静态常量定义。

//对一些变量进行解释
//定义dict_can_resize 的值
static dictResizeEnable dict_can_resize = DICT_RESIZE_ENABLE;
//负载因子的最大值
static unsigned int dict_force_resize_ratio = 5;

//其中,DICT_RESIZE_ENABLE是一个enum常量,在dict.h中定义
typedef enum {
    DICT_RESIZE_ENABLE,
    DICT_RESIZE_AVOID,
    DICT_RESIZE_FORBID,
} dictResizeEnable;
(2)_dictExpand

创建或者扩容hash表。该函数不能与rehashing操作同时进行,也不能强制缩容。在使用_dictNextExp函数得到需要的size之后,它先是使用一个临时变量n去分配空间,然后进行判断,若ht[0].table的值为NULL,则认为是刚create出来的dict,直接把n赋值给ht[0],否则给ht[1],并开始rehashing操作。

int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
    if (malloc_failed) *malloc_failed = 0;

    //若d正在rehash或者已有元素数量大于扩容后的容量,则返回err
    if (dictIsRehashing(d) || d->ht_used[0] > size)
        return DICT_ERR;

    //创建新的哈希表,声明新表used变量,设置新表大小的指数级别(_dictNextExp函数下面会讲)
    dictEntry **new_ht_table;
    unsigned long new_ht_used;
    signed char new_ht_size_exp = _dictNextExp(size);

    //计算新表的容量大小,若小于给定size值或者内存空间不够,返回err
    size_t newsize = 1ul<<new_ht_size_exp;
    if (newsize < size || newsize * sizeof(dictEntry*) < newsize)
        return DICT_ERR;

    //如果新表size的指数级别等于原表,则扩容无效,返回err
    if (new_ht_size_exp == d->ht_size_exp[0]) return DICT_ERR;

    //判断是否分配内存失败,是则重新分配内存
    if (malloc_failed) {
        new_ht_table = ztrycalloc(newsize*sizeof(dictEntry*));
        *malloc_failed = new_ht_table == NULL;
        if (*malloc_failed)
            return DICT_ERR;
    } else
        new_ht_table = zcalloc(newsize*sizeof(dictEntry*));

    new_ht_used = 0;
	//判断dict是否是第一次初始化,即是否创建ht_table[0]
	//若是,则该d为新创建的dict,进而设置d的各个结构。返回操作成功
    if (d->ht_table[0] == NULL) {
        if (d->type->rehashingStarted) d->type->rehashingStarted(d);
        if (d->type->rehashingCompleted) d->type->rehashingCompleted(d);
        d->ht_size_exp[0] = new_ht_size_exp;
        d->ht_used[0] = new_ht_used;
        d->ht_table[0] = new_ht_table;
        return DICT_OK;
    }

    //如果不是新表,即ht_table[1],给各个结构赋值,并开始rehashing。
    d->ht_size_exp[1] = new_ht_size_exp;
    d->ht_used[1] = new_ht_used;
    d->ht_table[1] = new_ht_table;
    d->rehashidx = 0;
    if (d->type->rehashingStarted) d->type->rehashingStarted(d);
    return DICT_OK;
}

  • d:操作对象。
  • size:该函数建立的hash表容量大小。
  • malloc_failed:当 malloc_failed 为非 NULL 时,如果 malloc 失败,它将避免出现恐慌(在这种情况下,它将设置为 1)。
(3)dictRehash

调用dictExpand函数进入rehashing状态后,rehashing 操作将会把ht[0]里,rehashidx的值对应的bucket下的所有dictEntry,移至ht[1],之后对rehashidx进行自增处理。当ht[0]->used为0时,认为ht[0]的所有dictEntry已经移至ht[1],此时return 0,否则 return 1,告诉调用者,还需要继续进行rehashing操作。同时,rehashing时允许最多跳过10n的空bucket,就要退出流程。假设传入的n=1,即只进行一次rehashing操作

int dictRehash(dict *d, int n) {
    //rehash n*10个桶数量的元素
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    //判断d是否符合rehashing条件
    unsigned long s0 = DICTHT_SIZE(d->ht_size_exp[0]);
    unsigned long s1 = DICTHT_SIZE(d->ht_size_exp[1]);
    if (dict_can_resize == DICT_RESIZE_FORBID || !dictIsRehashing(d)) return 0;
    if (dict_can_resize == DICT_RESIZE_AVOID && 
        ((s1 > s0 && s1 / s0 < dict_force_resize_ratio) ||
         (s1 < s0 && s0 / s1 < dict_force_resize_ratio)))
    {
        return 0;
    }
	
	//d符合rehash条件,开始rehash
    while(n-- && d->ht_used[0] != 0) {
        dictEntry *de, *nextde;
        
        assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);
        //当前桶已空,将rehashidx++,对下一个桶进行迁移
        while(d->ht_table[0][d->rehashidx] == NULL) {
            d->rehashidx++;
            //该次rehash结束,判断表中是否还有元素
            if (--empty_visits == 0) return 1;
        }
        //将当前桶迁移给de
        de = d->ht_table[0][d->rehashidx];
        //将这个桶中所有的key都迁移到新表
        while(de) {
            uint64_t h;
			//用nextde记录当前桶的下一个桶,key指向de的key
            nextde = dictGetNext(de);
            void *key = dictGetKey(de);
            //得到新表的索引
            h = dictHashKey(d, key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
            
            d->ht_table[1][h] = de;
            d->ht_used[0]--;
            d->ht_used[1]++;
            de = nextde;
        }
        d->ht_table[0][d->rehashidx] = NULL;
        d->rehashidx++;
    }

    //检查是否rehash完毕
    if (d->ht_used[0] == 0) {
        if (d->type->rehashingCompleted) d->type->rehashingCompleted(d);
        zfree(d->ht_table[0]);
        //将新表复制到旧表
        d->ht_table[0] = d->ht_table[1];
        d->ht_used[0] = d->ht_used[1];
        d->ht_size_exp[0] = d->ht_size_exp[1];
        _dictReset(d, 1);
        //重置rehashidx为-1
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}
  • d:操作对象
  • n:rehash元素数量为n*10 。

rehashing操作的触发有两种方式:
1.定时操作:

long long timeInMilliseconds(void) {
    struct timeval tv;

    gettimeofday(&tv,NULL);
    return (((long long)tv.tv_sec)*1000)+(tv.tv_usec/1000);
}

//以 ms+“delta” 毫秒为单位进行重新哈希。“delta”的值大于 0
//并且在大多数情况下小于 1。确切的上限取决于 dictRehash(d,100) 的运行时间。
int dictRehashMilliseconds(dict *d, unsigned int ms) {
    if (d->pauserehash > 0) return 0;

    monotime timer;
    elapsedStart(&timer);
    int rehashes = 0;

    while(dictRehash(d,100)) {
        rehashes += 100;
        if (elapsedMs(timer) >= ms) break;
    }
    return rehashes;
}

外部传入一个毫秒时间,在这时间内循环执行rehashing,每次执行100次。

2.操作时触发:

static void _dictRehashStep(dict *d) {
    if (d->pauserehash == 0) dictRehash(d,1);
}

此函数由字典中的常见查找或更新操作调用,以便哈希表在主动使用时自动从ht_table[0]迁移到 ht_table[1]。
注:此函数仅执行重新散列的步骤,并且仅当哈希表的哈希尚未暂停时。当我们在重新散列过程中有迭代器时,我们不能弄乱两个散列表,否则可能会遗漏或重复一些元素。

(4)增加元素

向哈希表中增加元素。这里说明三个函数。dictAdd函数向目标哈希表中添加元素,其内部调用dictAddRaw函数,创建新键,添加空值的entry。dictAddRaw函数内部又调用dictInsertAtPosition函数,创建key。

  1. dictAdd 函数
//向目标哈希表添加元素
int dictAdd(dict *d, void *key, void *val)
{
    //调用dictAddRaw函数,添加无值的entry
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    if (!d->type->no_value) dictSetVal(d, entry, val);
    return DICT_OK;
}
  1. dictAddRaw 函数
//此函数添加entry,但不是设置值,而是将entry返回给用户
//这将确保按照用户的意愿填充值字段。
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    //用私人函数dictFindPositionForInsert得到新键的位置
    //再用dictInsertAtPosition函数在该位置添加一个键
    void *position = dictFindPositionForInsert(d, key, existing);
    if (!position) return NULL;

    /* Dup the key if necessary. */
    if (d->type->keyDup) key = d->type->keyDup(d, key);

    return dictInsertAtPosition(d, key, position);
}
  1. dictInsertAtPosition 函数
//在字典的哈希表中,在前面调用 dictFindPositionForInsert 返回的位置添加一个键。
dictEntry *dictInsertAtPosition(dict *d, void *key, void *position) {
	//这是一个桶,但被该API函数隐藏了
    dictEntry **bucket = position; 
    dictEntry *entry;
    
    //如果正在进行rehashing,则在表 1 中插入,否则在表 0 中插入。
    int htidx = dictIsRehashing(d) ? 1 : 0;
    assert(bucket >= &d->ht_table[htidx][0] &&
           bucket <= &d->ht_table[htidx][DICTHT_SIZE_MASK(d->ht_size_exp[htidx])]);
    if (d->type->no_value) {
        if (d->type->keys_are_odd && !*bucket) {
            //将key直接存储在目标桶中,无需分配entry
            entry = key;
            assert(entryIsKey(entry));
        } else {
            entry = createEntryNoValue(key, *bucket);
        }
    } else {
        //头插法,在顶部插入元素,节约插入时的时间消耗
        //分配存储空间
        entry = zmalloc(sizeof(*entry));
        assert(entryIsNormal(entry)); /* Check alignment of allocation */
        entry->key = key;
        entry->next = *bucket;
    }
    *bucket = entry;
    d->ht_used[htidx]++;

    return entry;
}
  • d:操作对象。
  • key:键对应变量。
  • val:值对应变量。
  • position:键对应位置。
(5)替换元素

向Hash表中增加一个元素,如果Hash表中已经有该元素的话,则将该元素进行替换掉。

int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, *existing;

    //增加元素,如果键不存在,则在添加键并设置键对应的值
    entry = dictAddRaw(d,key,&existing);
    if (entry) {
        dictSetVal(d, entry, val);
        return 1;
    }
    
    //设置新值并释放旧值
    void *oldval = dictGetVal(existing);
    dictSetVal(d, existing, val);
    if (d->type->valDestructor)
        d->type->valDestructor(d, oldval);
    return 0;
}
  • d:操作对象。
  • key:键对应变量。
  • val:值对应变量。
(6)删除操作
  1. 删除指定的key
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    uint64_t h, idx;
    dictEntry *he, *prevHe;
    int table;

    /* dict is empty */
    if (dictSize(d) == 0) return NULL;

	//返回key对应的dictEntry
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);

    for (table = 0; table <= 1; table++) {
        idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
        if (table == 0 && (long)idx < d->rehashidx) continue;
        he = d->ht_table[table][idx];
        prevHe = NULL;
        while(he) {
            void *he_key = dictGetKey(he);
            if (key == he_key || dictCompareKeys(d, key, he_key)) {
                /* Unlink the element from the list */
                if (prevHe)
                    dictSetNext(prevHe, dictGetNext(he));
                else
                    d->ht_table[table][idx] = dictGetNext(he);
                if (!nofree) {
                	//如果需要释放,则进行相应的释放操作
                    dictFreeUnlinkedEntry(d, he);
                }
                //记录数相应减小
                d->ht_used[table]--;
                return he;
            }
            //进行相应的赋值操作
            prevHe = he;
            he = dictGetNext(he);
        }
        if (!dictIsRehashing(d)) break;
    }
    //返回错误
    return NULL; /* not found */
}
  • d:操作对象。
  • key:指定的键。
  1. 删除指定的value
int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
  • d:操作对象。
  • key:指定的键。
  1. 删除指定的元素

dictUnlink函数从表中删除一个元素,但不实际释放键、值和字典条目。
此函数适用于:当我们想从哈希表中删除某些内容,但想在实际删除条目之前使用其值时,此函数很有用。

dictEntry *dictUnlink(dict *d, const void *key) {
    return dictGenericDelete(d,key,1);
}
  • d:操作对象。
  • key:指定的键。

使用完成后,再调用dictFreeUnlinkedEntry函数以释放该entry。

void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
    if (he == NULL) return;
    dictFreeKey(d, he);
    dictFreeVal(d, he);
    if (!entryIsKey(he)) zfree(decodeMaskedPtr(he));
}
  • d:操作对象。
  • he:要删除的桶。
  1. 删除整个字典

删除整个字典数据,保留字典结构

int _dictClear(dict *d, int htidx, void(callback)(dict*)) {
    unsigned long i;
	//释放所有元素
    /* Free all the elements */
    for (i = 0; i < DICTHT_SIZE(d->ht_size_exp[htidx]) && d->ht_used[htidx] > 0; i++) {
        dictEntry *he, *nextHe;

        if (callback && (i & 65535) == 0) callback(d);
		//如果哈希表中没有链表元素
        if ((he = d->ht_table[htidx][i]) == NULL) continue;
        while(he) {
            nextHe = dictGetNext(he);
            //释放key和value,以及结构体
            dictFreeKey(d, he);
            dictFreeVal(d, he);
            if (!entryIsKey(he)) zfree(decodeMaskedPtr(he));
            d->ht_used[htidx]--;
            he = nextHe;
        }
    }
    /* Free the table and the allocated cache structure */
    zfree(d->ht_table[htidx]);
    //重新初始化整个哈希表
    _dictReset(d, htidx);
    return DICT_OK; /* never fails */
}
  • d:操作对象。
  • htidx:标志当前操作哈希表是表[0]还是表[1]。
  1. 删除哈希表
void dictRelease(dict *d)
{
	//清除两个哈希表中的数据,并释放其空间,即彻底删除
    _dictClear(d,0,NULL);
    _dictClear(d,1,NULL);
    zfree(d);
}
  • d:操作对象。
(7)查找
  1. 查找指定key对应的entry。
dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    uint64_t h, idx, table;
	
	//哈希表为空
    if (dictSize(d) == 0) return NULL; /* dict is empty */
    //渐进式rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
    	//具体索引
        idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
        if (table == 0 && (long)idx < d->rehashidx) continue; 
        he = d->ht_table[table][idx];
        while(he) {
            void *he_key = dictGetKey(he);
            //存在需要查找的dictEntry
            if (key == he_key || dictCompareKeys(d, key, he_key))
            	//返回entry
                return he;
            he = dictGetNext(he);
        }
        //在rehash需要再查
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

  • d:操作对象。
  • key:指定的键,查找对应的entry。
  1. 查找指定key对应的value值
void *dictFetchValue(dict *d, const void *key) {
    dictEntry *he;

    he = dictFind(d,key);
    return he ? dictGetVal(he) : NULL;
}
  • d:操作对象。
  • key:指定的键,查找对应的entry。
(8)一些简单函数
  1. 设置key和value
void dictSetKey(dict *d, dictEntry* de, void *key) {
    assert(!d->type->no_value);
    if (d->type->keyDup)
        de->key = d->type->keyDup(d, key);
    else
        de->key = key;
}

void dictSetVal(dict *d, dictEntry *de, void *val) {
    assert(entryHasValue(de));
    de->v.val = d->type->valDup ? d->type->valDup(d, val) : val;
}
  • d:操作对象。
  • de:相应entry。
  • key:要设置的key。
  • val:要设置的value。
  1. 获得key和value
void *dictGetKey(const dictEntry *de) {
    if (entryIsKey(de)) return (void*)de;
    if (entryIsNoValue(de)) return decodeEntryNoValue(de)->key;
    return de->key;
}

void *dictGetVal(const dictEntry *de) {
    assert(entryHasValue(de));
    return de->v.val;
}
  • de:需要获得key或value信息的对应entry。
  1. 获得或设置下一节点
static dictEntry *dictGetNext(const dictEntry *de) {
    if (entryIsKey(de)) return NULL; /* there's no next */
    if (entryIsNoValue(de)) return decodeEntryNoValue(de)->next;
    return de->next;
}

//返回下一结点的指针,如果下一结点不存在,返回NULL
static dictEntry **dictGetNextRef(dictEntry *de) {
    if (entryIsKey(de)) return NULL;
    if (entryIsNoValue(de)) return &decodeEntryNoValue(de)->next;
    return &de->next;
}
//设置下一个结点
static void dictSetNext(dictEntry *de, dictEntry *next) {
    assert(!entryIsKey(de));
    if (entryIsNoValue(de)) {
        dictEntryNoValue *entry = decodeEntryNoValue(de);
        entry->next = next;
    } else {
        de->next = next;
    }
}
  • de:操作对象字典。
  • next:下一结点指针。

3.对迭代器进行操作的函数

  1. 字典迭代器的创建

首先是用dictInitIterator函数初始化字典迭代器,后面在创建迭代器函数中先分配迭代器内存,再调用上述函数对其进行初始化。dictResetIterator()函数根据字典对应迭代器状态,重设字典内部变量值。

//对分配好内存的迭代器进行初始化,对内部进行赋值
//注意此时safe=0,迭代器非安全
//程序只会调用dictNext对字典进行迭代,而不能对字典进行修改
void dictInitIterator(dictIterator *iter, dict *d)
{
    iter->d = d;
    iter->table = 0;
    iter->index = -1;
    iter->safe = 0;
    iter->entry = NULL;
    iter->nextEntry = NULL;
}

//创建并返回给定结点的安全迭代器,修改safe为1
//意味着这个迭代器可以对字典进行修改操作,如dictAdd,dictFind等函数
void dictInitSafeIterator(dictIterator *iter, dict *d)
{
    dictInitIterator(iter, d);
    iter->safe = 1;
}

//根据字典迭代器参数重新设置d中pauserehash变量值,控制rehash进程
void dictResetIterator(dictIterator *iter)
{
    if (!(iter->index == -1 && iter->table == 0)) {
        if (iter->safe)
        	//#define dictResumeRehashing(d) ((d)->pauserehash--) (from dict.h)
        	//dictResumeRehashing被宏定义为控制dict内部pauserehash变量值减1的函数
        	//当pauserehash大于0,rehashing暂停,小于0则表示编码错误
            dictResumeRehashing(iter->d);
        else
            assert(iter->fingerprint == dictFingerprint(iter->d));
    }
}

//创建迭代器结构,分配空间并初始化
//返回迭代器指针
dictIterator *dictGetIterator(dict *d)
{
    dictIterator *iter = zmalloc(sizeof(*iter));
    dictInitIterator(iter, d);
    return iter;
}

//在dictGetIterator函数基础上,将创建好的迭代器设置为安全迭代器
dictIterator *dictGetSafeIterator(dict *d) {
    dictIterator *i = dictGetIterator(d);

    i->safe = 1;
    return i;
}
  • d:操作对象字典。
  • iter:操作目标迭代器。
  1. 对哈希表进行迭代遍历操作
dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
        if (iter->entry == NULL) {
        	//若遍历的index为-1,并且此时为ht_table[0]
            if (iter->index == -1 && iter->table == 0) {
            	//若此迭代器安全,则停止rehash
                if (iter->safe)
                    dictPauseRehashing(iter->d);
                else
                    iter->fingerprint = dictFingerprint(iter->d);

                //跳过表[0]的rehash
                if (dictIsRehashing(iter->d)) {
                    iter->index = iter->d->rehashidx - 1;
                }
            }
            iter->index++;
            //若遍历的index大于整个哈希表数组的大小,说明遍历完成
            //若该哈希表正在遍历,则跳到表1,index归0,否则结束跳出
            if (iter->index >= (long) DICTHT_SIZE(iter->d->ht_size_exp[iter->table])) {
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    iter->table++;
                    iter->index = 0;
                } else {
                    break;
                }
            }
            iter->entry = iter->d->ht_table[iter->table][iter->index];
        } else {
        	//遍历下一个元素
            iter->entry = iter->nextEntry;
        }
        if (iter->entry) {
            /* We need to save the 'next' here, the iterator user
             * may delete the entry we are returning. */
            //返回遍历过程中的下一个元素
            iter->nextEntry = dictGetNext(iter->entry);
            //返回当前遍历的元素
            return iter->entry;
        }
    }
    return NULL;
}
  • iter:操作目标迭代器。
  1. 释放迭代器
void dictReleaseIterator(dictIterator *iter)
{
    dictResetIterator(iter);
    zfree(iter);
}
  1. 从哈希表中获取随机的key
dictEntry *dictGetRandomKey(dict *d)
{
    dictEntry *he, *orighe;
    unsigned long h;
    int listlen, listele;
	//若字典大小为0,即没有冲突记录,没有元素,返回NULL
    if (dictSize(d) == 0) return NULL;
    if (dictIsRehashing(d)) _dictRehashStep(d);
    if (dictIsRehashing(d)) {
        unsigned long s0 = DICTHT_SIZE(d->ht_size_exp[0]);
        do {
            //确保indexes中从0到rehashidx-1没有元素
            h = d->rehashidx + (randomULong() % (dictBuckets(d) - d->rehashidx));
            he = (h >= s0) ? d->ht_table[1][h - s0] : d->ht_table[0][h];
        } while(he == NULL);
    } else {
    //下面的做法是先随机选取散列数组中的一个槽,这样就得到一个链表
    //(如果该槽中没有元素则重新选取)然后在该列表中随机选取一个键值对返回
        unsigned long m = DICTHT_SIZE_MASK(d->ht_size_exp[0]);
        do {
            h = randomULong() & m;
            he = d->ht_table[0][h];
        } while(he == NULL);
    }

    //计算处于这个slot里面的元素数目
    listlen = 0;
    orighe = he;
    while(he) {
        he = dictGetNext(he);
        listlen++;
    }
    //从整个slot链表中选择元素的位置
    listele = random() % listlen;
    he = orighe;
    //找到对应位置
    while(listele--) he = dictGetNext(he);
    return he;
}
  • d:操作对象字典。

四、dict 的 private functions

1. 判断是否允许扩充

因为当 dict 扩展时,可能需要一次分配巨大的内存块,所以如果 dict->type具有 expandAllowed 成员函数,将检查是否允许此分配。

static int dictTypeExpandAllowed(dict *d) {
    if (d->type->expandAllowed == NULL) return 1;
    return d->type->expandAllowed(
                    DICTHT_SIZE(_dictNextExp(d->ht_used[0] + 1)) * sizeof(dictEntry*),
                    (double)d->ht_used[0] / DICTHT_SIZE(d->ht_size_exp[0]));
}
  • d:操作对象字典。

2. 需要时扩充哈希表

判断字典的哈希表是否需要扩充,如果需要则对哈希表进行dictExpand。

static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    //如果正在进行rehash,直接返回
    if (dictIsRehashing(d)) return DICT_OK;

    /* If the hash table is empty expand it to the initial size. */
    //如果哈希表为空,扩充哈希表到初始大小,4
    if (DICTHT_SIZE(d->ht_size_exp[0]) == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    //如果当前哈希表数据记录数used大于小于容量大小size,则将大小扩充为2倍
    if ((dict_can_resize == DICT_RESIZE_ENABLE &&
         d->ht_used[0] >= DICTHT_SIZE(d->ht_size_exp[0])) ||
        (dict_can_resize != DICT_RESIZE_FORBID &&
         d->ht_used[0] / DICTHT_SIZE(d->ht_size_exp[0]) > dict_force_resize_ratio))
    {
        if (!dictTypeExpandAllowed(d))
            return DICT_OK;
        return dictExpand(d, d->ht_used[0] + 1);
    }
    return DICT_OK;
}
  • d:操作对象字典。

3.哈希表大小的指数级别

哈希表size是2的整数次幂,本函数求该指数exp大小。

static signed char _dictNextExp(unsigned long size)
{
    if (size <= DICT_HT_INITIAL_SIZE) return DICT_HT_INITIAL_EXP;
    //不能超过长整型的最大值
    if (size >= LONG_MAX) return (8*sizeof(long)-1);

    return 8*sizeof(long) - __builtin_clzl(size-1);
}
  • size:哈希表容量大小。

4.查找指定键的位置

void *dictFindPositionForInsert(dict *d, const void *key, dictEntry **existing) {
    unsigned long idx, table;
    dictEntry *he;
    //调用对应哈希函数获得给定key的哈希值
    uint64_t hash = dictHashKey(d, key);
    if (existing) *existing = NULL;
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return NULL;
    for (table = 0; table <= 1; table++) {
        idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
        if (table == 0 && (long)idx < d->rehashidx) continue; 
        
        he = d->ht_table[table][idx];
        //依次比较该slot上的所有键值对的keu是否与给定key相等
        while(he) {
            void *he_key = dictGetKey(he);
            if (key == he_key || dictCompareKeys(d, key, he_key)) {
                if (existing) *existing = he;
                return NULL;
            }
            he = dictGetNext(he);
        }
        //若不在执行rehash操作,第二个表为空表,无需继续查找
        if (!dictIsRehashing(d)) break;
    }

    //如果正在rehash,则桶已经在第二个哈希表返回
    dictEntry **bucket = &d->ht_table[dictIsRehashing(d) ? 1 : 0][idx];
    return bucket;
}
  • d:操作对象字典。
  • key:指定键。
  • existing:一个entry指针。

5.清空字典数据

只清空数据,但不释放空间。(而_dictClear函数释放字典空间)

void dictEmpty(dict *d, void(callback)(dict*)) {
    _dictClear(d,0,callback);
    _dictClear(d,1,callback);
    d->rehashidx = -1;
    d->pauserehash = 0;
}
  • d:操作对象字典。

6.使用指针和预先计算的哈希值寻找结点

哈希值用dictGetHsh函数提供。如果找到了符合要求的结点,返回值是一个指向该结点的oldptr指针,否则返回NULL。

dictEntry *dictFindEntryByPtrAndHash(dict *d, const void *oldptr, uint64_t hash) {
    dictEntry *he;
    unsigned long idx, table;

    if (dictSize(d) == 0) return NULL; /* dict is empty */
    for (table = 0; table <= 1; table++) {
        idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
        if (table == 0 && (long)idx < d->rehashidx) continue;
        he = d->ht_table[table][idx];
        while(he) {
            if (oldptr == dictGetKey(he))
                return he;
            he = dictGetNext(he);
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}
  • d:操作对象字典。
  • oldkey:一个死指针,不应被访问。
  • hash:给定的预先计算的哈希值,用来寻找符合要求的指针。

7.返回哈希表大小

提供给定字典的两个哈希表的size,这个函数仅在初始化或者rehash过程中调用。

void dictRehashingInfo(dict *d, unsigned long long *from_size, unsigned long long *to_size) {
    /* Expansion during initialization. */
    if (d->ht_size_exp[0] == -1) {
        *from_size = DICTHT_SIZE(d->ht_size_exp[0]);
        *to_size = DICTHT_SIZE(DICT_HT_INITIAL_EXP);
        return;
    }
    /* Invalid method usage if rehashing isn't ongoing. */
    assert(dictIsRehashing(d));
    *from_size = DICTHT_SIZE(d->ht_size_exp[0]);
    *to_size = DICTHT_SIZE(d->ht_size_exp[1]);
}
  • d:操作对象字典。
  • from_size:指代哈希表的size。
  • to_size:指代哈希表size的以2为底形式的指数。

五、debugging

debugging模块中是一系列检查哈希表数据的函数,用dictStats结构体保存哈希表数据,所以这些函数都是在stats上操作,用以排查哈希表错误之处。

1.dictStats结构声明

typedef struct dictStats {
    int htidx;
    unsigned long buckets;
    unsigned long maxChainLen;
    unsigned long totalChainLen;
    unsigned long htSize;
    unsigned long htUsed;
    unsigned long *clvector;
} dictStats;
  • htidx:哈希表索引。
  • buckets:dict由数组+链表构成,数组的每个元素占用的槽位就叫做哈希桶,当出现散列冲突,就会在这个桶下挂一个链表,解决冲突。
  • maxChainLen:链表最大长度,即桶的深度。
  • totalChainLen:当前链表总长度。
  • htSize:哈希表容量大小,即数组长度。
  • htSize:哈希表冲突记录数。
  • clvector:一个指针,指代Stats。

2.dictFreeStats

释放Stats内存空间。

void dictFreeStats(dictStats *stats) {
    zfree(stats->clvector);
    zfree(stats);
}
  • stats:操作对象哈希表数据统计结构。

3.dictCombineStats

将from数据加到into中,融合两个哈希表的数据。

void dictCombineStats(dictStats *from, dictStats *into) {
    into->buckets += from->buckets;
    into->maxChainLen = (from->maxChainLen > into->maxChainLen) ? from->maxChainLen : into->maxChainLen;
    into->totalChainLen += from->totalChainLen;
    into->htSize += from->htSize;
    into->htUsed += from->htUsed;
    for (int i = 0; i < DICT_STATS_VECTLEN; i++) {
        into->clvector[i] += from->clvector[i];
    }
}
  • from:哈希表数据统计结构,包含要添加的数据。
  • into:哈希表数据统计结构,被添加。

4.dictGetStatsHt

创建一个stats,并用指定字典的某哈希表的数据赋值,返回stats。获得需要的哈希表的数据。

dictStats *dictGetStatsHt(dict *d, int htidx, int full) {
    unsigned long *clvector = zcalloc(sizeof(unsigned long) * DICT_STATS_VECTLEN);
    dictStats *stats = zcalloc(sizeof(dictStats));
    stats->htidx = htidx;
    stats->clvector = clvector;
    stats->htSize = DICTHT_SIZE(d->ht_size_exp[htidx]);
    stats->htUsed = d->ht_used[htidx];
    if (!full) return stats;
    //full不为0,计算stats
    /* Compute stats. */
    for (unsigned long i = 0; i < DICTHT_SIZE(d->ht_size_exp[htidx]); i++) {
        dictEntry *he;

        if (d->ht_table[htidx][i] == NULL) {
            clvector[0]++;
            continue;
        }
        stats->buckets++;
        /* For each hash entry on this slot... */
        unsigned long chainlen = 0;
        he = d->ht_table[htidx][i];
        while(he) {
            chainlen++;
            he = dictGetNext(he);
        }
        clvector[(chainlen < DICT_STATS_VECTLEN) ? chainlen : (DICT_STATS_VECTLEN-1)]++;
        if (chainlen > stats->maxChainLen) stats->maxChainLen = chainlen;
        stats->totalChainLen += chainlen;
    }

    return stats;
}
  • d:操作对象字典。
  • htidx:哈希表索引。
  • full:是否计算统计信息的标志。若为0,则直接返回初始化赋值的stats,若不为0,则要计算stats。

5.dictGetStatsMsg

将stats中的数据转换成用户可以直接理解的直观数据并输出。

size_t dictGetStatsMsg(char *buf, size_t bufsize, dictStats *stats, int full) {
    if (stats->htUsed == 0) {
        return snprintf(buf,bufsize,
            "Hash table %d stats (%s):\n"
            "No stats available for empty dictionaries\n",
            stats->htidx, (stats->htidx == 0) ? "main hash table" : "rehashing target");
    }
    size_t l = 0;
    l += snprintf(buf + l, bufsize - l,
                  "Hash table %d stats (%s):\n"
                  " table size: %lu\n"
                  " number of elements: %lu\n",
                  stats->htidx, (stats->htidx == 0) ? "main hash table" : "rehashing target",
                  stats->htSize, stats->htUsed);
    if (full) {
        l += snprintf(buf + l, bufsize - l,
                      " different slots: %lu\n"
                      " max chain length: %lu\n"
                      " avg chain length (counted): %.02f\n"
                      " avg chain length (computed): %.02f\n"
                      " Chain length distribution:\n",
                      stats->buckets, stats->maxChainLen,
                      (float) stats->totalChainLen / stats->buckets, (float) stats->htUsed / stats->buckets);

        for (unsigned long i = 0; i < DICT_STATS_VECTLEN - 1; i++) {
            if (stats->clvector[i] == 0) continue;
            if (l >= bufsize) break;
            l += snprintf(buf + l, bufsize - l,
                          "   %ld: %ld (%.02f%%)\n",
                          i, stats->clvector[i], ((float) stats->clvector[i] / stats->htSize) * 100);
        }
    }

    /* Make sure there is a NULL term at the end. */
    buf[bufsize-1] = '\0';
    /* Unlike snprintf(), return the number of characters actually written. */
    return strlen(buf);
}
  • buf:字符指针。
  • bufsize:buf指代字符串大小。
  • stats:操作对象哈希表数据统计结构。
  • full:判断标志。

6.dictGetStats

调用dictGetStatsHt函数,创建一个stats,记录字典数据。如果字典rehashing,则再创建一个stats,输出两个哈希表数据,否则只输出表[0]数据。输出后释放stats空间。

void dictGetStats(char *buf, size_t bufsize, dict *d, int full) {
    size_t l;
    char *orig_buf = buf;
    size_t orig_bufsize = bufsize;

    dictStats *mainHtStats = dictGetStatsHt(d, 0, full);
    l = dictGetStatsMsg(buf, bufsize, mainHtStats, full);
    dictFreeStats(mainHtStats);
    buf += l;
    bufsize -= l;
    if (dictIsRehashing(d) && bufsize > 0) {
        dictStats *rehashHtStats = dictGetStatsHt(d, 1, full);
        dictGetStatsMsg(buf, bufsize, rehashHtStats, full);
        dictFreeStats(rehashHtStats);
    }
    /* Make sure there is a NULL term at the end. */
    orig_buf[orig_bufsize-1] = '\0';
}

总结

  • 学习了解dict的结构:本文分析了redis dict的几个模块,分别是hash functions,API implementation,private functions,以及debugging。
  • 了解dict应用场景 : 字典被广泛用于实现Redis的各自功能,其中包括数据库和哈希键。

注:
1.作者初学者,学习这个也是老师留的大作业,参考很多文章,在此感谢。同时欢迎指正交流学习。
2.本文源码来源:https://github.com/redis/redis/blob/unstable/src/dict.c
3.这个源码好像是最新版,目前我看到的文章很少有跟这个源码一样的。

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值