redis 源码学习 之 字典dict

字典应用

Redis的数据库就是使用字典作为底层实现的,对数据库的增、删、查、改都是建立在对字典的操作上。此外,字典还是Redis中哈希键的底层实现,当一个哈希键包含的键值对比较多,或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现

Redis中的字典采用哈希表作为底层实现

dict数据结构

实现了一个内存哈希表, 它支持插入、删除、替换、查找和获取随机元素等操作。哈希表会自动在表的大小的二次方之间进行调整。 键的冲突通过链表来解决
1、哈希表节点(dictEntry)
字典中每一对键值都以dictEntry节点的形式存放,其结构体实现如下:

typedef struct dictEntry {
    void *key;  // 键
    union {   
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;  // 值
    struct dictEntry *next;  // 指向下一个哈希表节点
    // 此处可以看出字典采用了开链法才解决哈希冲突
} dictEntry;

2、哈希表dictht

typedef struct dictht {
    dictEntry **table; // 哈希表数组
    unsigned long size;  // 哈希表大小
    unsigned long sizemask; // 哈希表大小掩码,用于计算索引值
    unsigned long used;  // 该哈希表中已有节点的数量
} dictht;

3、哈希表dict

typedef struct dict {
    dictType *type; // 字典类型,保存一些用于操作特定类型键值对的函数
    void *privdata; // 私有数据,保存需要传给那些类型特定函数的可选数据
    dictht ht[2];  // 一个字典结构包括两个哈希表
    long rehashidx; // rehash索引,不进行rehash时其值为-1
    int iterators; // 当前正在使用的迭代器数量
} dict;

4、字典类型函数dictType

typedef struct dictType {
    // 计算哈希值的函数
    unsigned int (*hashFunction)(const void *key);
    // 复制键的函数
    void *(*keyDup)(void *privdata, const void *key);
    // 复制值的函数
    void *(*valDup)(void *privdata, const void *obj);
    // 比较键的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    // 销毁键的函数
    void (*keyDestructor)(void *privdata, void *key);
    // 销毁值的函数
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

5、Redis字典结构
在这里插入图片描述

哈希值和索引值的计算

1、就先计算哈希值
hash = dict->type->hashFunction(键值);
2、计算索引值
index = hash & dict->ht[0].sizemask ;

rehash算法

rehash是Redis字典实现的一个重要操作。dict采用链地址法来处理哈希冲突,那么随着数据存放量的增加,必然会造成冲突链表越来越长,最终会导致字典的查找效率显著下降。这种情况下,就需要对字典进行扩容。另外,当字典中键值对过少时,就需要对字典进行收缩来节省空间,这些扩容和收缩的过程就采用rehash来实现。

通常情况下,字典的键值对数据都存放在ht[0]里面,如果此时需要对字典进行rehash,会进行如下步骤:

  1. 为ht[1]哈希表分配空间,空间的大小取决于要执行的操作和字典中键值对的个数
  2. 将保存在ht[0]中的键值对重新计算哈希值和索引,然后存放到ht[1]中。
  3. 当ht[0]中的数据全部迁移到ht[1]之后,将ht[1]设为ht[0],并为ht[1]新创建一个空白哈希表,为下一次rehash做准备

rehash算法的源码:

// 执行N步渐进式的rehash操作,如果仍存在旧表中的数据迁移到新表,则返回1,反之返回0
// 每一步操作移动一个索引值下的键值对到新表
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; // 最大允许访问的空桶值,也就是该索引下没有键值对
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        // rehashidx不能大于哈希表的大小
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        // 获取需要rehash的索引值下的链表
        de = d->ht[0].table[d->rehashidx];
        // 将该索引下的键值对全部转移到新表
        while(de) {
            unsigned int h;

            nextde = de->next;
            // 计算该键值对在新表中的索引值
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    // 键值是否整个表都迁移完成
    if (d->ht[0].used == 0) {
        // 清除ht[0]
        zfree(d->ht[0].table);
        // 将ht[1]转移到ht[0]
        d->ht[0] = d->ht[1];
        // 重置ht[1]为空哈希表
        _dictReset(&d->ht[1]);
        // 完成rehash,-1代表没有进行rehash操作
        d->rehashidx = -1;
        return 0;
    }

    // 如果没有完成则返回1
    return 1;
}

##字典 特性

  1. 字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在rehash使用
  2. 当字典被用作数据库的底层实现,或者哈希键的底层实现时,redis用MurmurHash2算法来计算哈希值
  3. 哈希表使用链地址法来解决键冲突,被分配到同一个索引上的多个键值对会连接成一个单向链表
  4. 在对哈希表进行扩展或者进行收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是 一次玩车个的,而是渐进式完成的

dict基本操作

  1. dict创建
    时间复杂度:T = O(1)
dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));

    _dictInit(d,type,privDataPtr);

    return d;
}

//初始化哈希表
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;  // 设置哈希表 rehash 状态
    d->iterators = 0; // 设置字典的安全迭代器数
    return DICT_OK;
}
  1. 添加键值对
    向字典中添加键值对时需要考虑如下情况:
    如果此时没有进行rehash操作,直接计算出索引添加到ht[0]中
    如果此刻正在进行rehash操作,则根据ht[1]的参数计算出索引值,添加到ht[1]中
  尝试将给定键值对添加到字典中
  只有给定键 key 不存在于字典时,添加操作才会成功
  添加成功返回 DICT_OK ,失败返回 DICT_ERR
  最坏 T = O(N) ,平摊 O(1)
int dictAdd(dict *d, void *key, void *val)
{
    // 尝试添加键到字典,并返回包含了这个键的新哈希节点
    // T = O(N)
    dictEntry *entry = dictAddRaw(d,key);
    // 键已存在,添加失败
    if (!entry) return DICT_ERR;
    // 键不存在,设置节点的值
    // T = O(1)
    dictSetVal(d, entry, val);

    // 添加成功
    return DICT_OK;
}
 /*尝试将键插入到字典中
 *
 * 如果键已经在字典存在,那么返回 NULL
 *
 * 如果键不存在,那么程序创建新的哈希节点,
 * 将节点和键关联,并插入到字典,然后返回节点本身
 * */
 dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;
    // 如果条件允许的话,进行单步 rehash
    // T = O(1)
    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    // 计算键在哈希表中的索引值
    // 如果值为 -1 ,那么表示键已经存在
    // T = O(N)
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;

    // T = O(1)
    /* Allocate the memory and store the new entry */
    // 如果字典正在 rehash ,那么将新键添加到 1 号哈希表
    // 否则,将新键添加到 0 号哈希表
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    // 为新节点分配空间
    entry = zmalloc(sizeof(*entry));
    // 将新节点插入到链表表头
    entry->next = ht->table[index];
    ht->table[index] = entry;
    // 更新哈希表已使用节点数量
    ht->used++;
    /* Set the hash entry fields. */
    // 设置新节点的键
    // T = O(1)
    dictSetKey(d, entry, key);
    return entry;
}

上述添加方式在,在存在该key的时候,直接返回NULL,Redis还提供了另一种添加键值对的函数,它在处理存在相同key的情况时,直接用新键值对来替换旧键值对

int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, auxentry;
    // 直接调用dictAdd函数,如果添加成功就表示没有存在相同的key
    if (dictAdd(d, key, val) == DICT_OK)
        return 1;
    // 运行到这里,说明键 key 已经存在,那么找出包含这个 key 的节点
    // 如果存在相同的key,则先获取该键值对
    entry = dictFind(d, key);
    // 然后用新的value来替换旧value
    auxentry = *entry;
    dictSetVal(d, entry, val); // 设置新的值
    dictFreeVal(d, &auxentry); // 然后释放旧值
    return 0;
}
  1. 查找键值对
    根据键值对的键大小在字典中查找对应的键值对。
    返回字典中包含键 key 的节点, 找到返回节点,找不到返回 NULL
    时间复杂度:T = O(1)
dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    unsigned int h, idx, table;

    // 字典(的哈希表)为空
    if (d->ht[0].size == 0) return NULL; /* We don't have a table at all */

    // 如果条件允许的话,进行单步 rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 计算键的哈希值
    h = dictHashKey(d, key);
    // 在字典的哈希表中查找这个键
    // T = O(1)
    for (table = 0; table <= 1; table++) {

        // 计算索引值
        idx = h & d->ht[table].sizemask;

        // 遍历给定索引上的链表的所有节点,查找 key
        he = d->ht[table].table[idx];
        // T = O(1)
        while(he) {

            if (dictCompareKeys(d, key, he->key))
                return he;

            he = he->next;
        }

        // 如果程序遍历完 0 号哈希表,仍然没找到指定的键的节点
        // 那么程序会检查字典是否在进行 rehash ,
        // 然后才决定是直接返回 NULL ,还是继续查找 1 号哈希表
        if (!dictIsRehashing(d)) return NULL;
    }

    // 进行到这里时,说明两个哈希表都没找到
    return NULL;
}

Redis还定义了dictFetchValue函数,用来返回给定键的值,,底层实现还是调用dictFind函数

void *dictFetchValue(dict *d, const void *key) {
    dictEntry *he;
    // 获取该键值对
    he = dictFind(d,key);
    // 返回该key对应的value
    return he ? dictGetVal(he) : NULL;
}

Redis 还定义了一个函数,用于从字典中随机返回一个键值对。随机返回字典中任意一个节点。 可用于实现随机化算法。
如果字典为空,返回 NULL
时间复杂度:T = O(N)

dictEntry *dictGetRandomKey(dict *d)
{
    dictEntry *he, *orighe;
    unsigned int h;
    int listlen, listele;
    // 字典为空
    if (dictSize(d) == 0) return NULL;
    // 进行单步 rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);
    // 如果正在 rehash ,那么将 1 号哈希表也作为随机查找的目标
    if (dictIsRehashing(d)) {
        // T = O(N)
        do {
            h = random() % (d->ht[0].size+d->ht[1].size);
            he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] :
                                      d->ht[0].table[h];
        } while(he == NULL);
    // 否则,只从 0 号哈希表中查找节点
    } else {
        // T = O(N)
        do {
            h = random() & d->ht[0].sizemask;
            he = d->ht[0].table[h];
        } while(he == NULL);
    }
    // 目前 he 已经指向一个非空的节点链表
    // 程序将从这个链表随机返回一个节点
    listlen = 0;
    orighe = he;
    // 计算节点数量, T = O(1)
    while(he) {
        he = he->next;
        listlen++;
    }
    // 取模,得出随机节点的索引
    listele = random() % listlen;
    he = orighe;
    // 按索引查找节点
    // T = O(1)
    while(listele--) he = he->next;

    // 返回随机节点
    return he;
}
  1. 删除键值对
    dictDelete函数用于从字典中删除给定键所对应的键值对,有两种形式:
// 删除该键值对,并释放键和值
int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0);
}
// 删除该键值对,不释放键和值
int dictDeleteNoFree(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,1);
}

/*
 * 查找并删除包含给定键的节点
 *
 * 参数 nofree 决定是否调用键和值的释放函数
 * 0 表示调用,1 表示不调用
 *
 * 找到并成功删除返回 DICT_OK ,没找到则返回 DICT_ERR
 *
 * T = O(1)
 */
static int dictGenericDelete(dict *d, const void *key, int nofree)
{
    unsigned int h, idx;
    dictEntry *he, *prevHe;
    int table;

    // 字典(的哈希表)为空
    if (d->ht[0].size == 0) return DICT_ERR; /* d->ht[0].table is NULL */

    // 进行单步 rehash ,T = O(1)
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 计算哈希值
    h = dictHashKey(d, key);

    // 遍历哈希表
    // T = O(1)
    for (table = 0; table <= 1; table++) {

        // 计算索引值 
        idx = h & d->ht[table].sizemask;
        // 指向该索引上的链表
        he = d->ht[table].table[idx];
        prevHe = NULL;
        // 遍历链表上的所有节点
        // T = O(1)
        while(he) {
            if (dictCompareKeys(d, key, he->key)) {
                // 找目标节点
                // 从链表中删除
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;
                // 释放调用键和值的释放函数?
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                } 
                // 释放节点本身
                zfree(he);
                // 更新已使用节点数量
                d->ht[table].used--;
                // 返回已找到信号
                return DICT_OK;
            }
            prevHe = he;
            he = he->next;
        }
        // 如果执行到这里,说明在 0 号哈希表中找不到给定键
        // 那么根据字典是否正在进行 rehash ,决定要不要查找 1 号哈希表
        if (!dictIsRehashing(d)) break;
    }
    // 没找到
    return DICT_ERR; 
}

  1. 字典删除
    dictRelease函数用于删除和释放整个字典结构
void dictRelease(dict *d)
{
    _dictClear(d,&d->ht[0],NULL); // 清除哈希表ht[0]
    _dictClear(d,&d->ht[1],NULL); // 清除哈希表ht[1]
    zfree(d); // 释放字典
}
/*
 * 删除哈希表上的所有节点,并重置哈希表的各项属性
 *
 * T = O(N)
 * */
int _dictClear(dict *d, dictht *ht, void(callback)(void *)) {
    unsigned long i;
    // 遍历整个哈希表
    // T = O(N)
    for (i = 0; i < ht->size && ht->used > 0; i++) {
        dictEntry *he, *nextHe;
        if (callback && (i & 65535) == 0) callback(d->privdata);
        // 跳过空索引
        if ((he = ht->table[i]) == NULL) continue;

        // 遍历整个链表
        // T = O(1)
        while(he) {
            nextHe = he->next;
            // 删除键
            dictFreeKey(d, he);
            // 删除值
            dictFreeVal(d, he);
            // 释放节点
            zfree(he);

            // 更新已使用节点计数
            ht->used--;

            // 处理下个节点
            he = nextHe;
        }
    }

    /* Free the table and the allocated cache structure */
    // 释放哈希表结构
    zfree(ht->table);

    /* Re-initialize the table */
    // 重置哈希表属性
    _dictReset(ht);

    return DICT_OK; /* never fails */
}


  1. dict小结
    Redis字典结构采用哈希表作为底层实现,每个字典包括两个哈希表,一个用来平常使用,另一个在rehash的时候使用。Redis的哈希表采用了链地址法来解决哈希冲突。最有特点的是,Redis在对字典进行扩容和收缩时,需要对哈希表中的所有键值对rehash到新哈希表里面,这个rehash操作不是一次性完成的,而是采用渐进式完成,这一措施使得rehash过程不会影响Redis对字典进行增删查改操作的效率。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值