redis源码分析 - 字典哈希

字典,又称为符号表 (symbol table)、关联数组 (associated array)或映射 (map),是一种用于保存键值对 (key-value pair)的抽象数据结构。在 redis 中,哈希键和数据库都是通过字典作为底层实现的。

结构体

redis 中字典的结构如下:

typedef struct dictEntry {
    void *key;  //键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;    //值,使用联合体,可以是指针,也可以是其他类型的值
    struct dictEntry *next; //使用拉链法解决哈希冲突问题
} dictEntry;

对两个64位类型解释如下

typedef unsigned __int64 uint64_t;
typedef signed __int64 int64_t;

可以将 __int64理解成 long long

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;

dictType结构体为一簇操作特定类型键值对的函数,privdata为传给这些函数的可选参数

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

dictht 为哈希表结构,哈希表结构有一个 dictEntry 的双重指针(可理解成 dictEntry 指针数组),size 为哈希表大小,sizemask 为哈希表大小掩码,用于计算哈希索引值,总是等于 size-1,used 为哈希表元素个数,即已有的节点的数量。

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;

dict 为字典结构,包括两个哈希表ht[2], 一个用于正常使用,另一个,当需要扩大哈希表时,将ht[0]中的节点rehashed 到 ht[1] 上,然后再将 ht[1] 设置为ht[0], ht[1]置空,准备下一次 rehashed。 rehashidx 记录了 rehash 的进度,当没有做 rehash 时,它的值为 -1, 当在做 rehash 的值时,它的值表示的是当前 rehash 到了 ht[0] 中的哪一个位置了(可以理解成 ht[0].table[rehashidx])。 redis 的 rehash 是渐进式的,即通过 rehashidx 一个一个递增的形式 rehash 的。

字典的详细实现

计算哈希值和索引值

redis计算哈希值和索引值,是根据键值来计算的,先计算出哈希值,然后根据哈希值和 sizemask 计算索引值。

/* 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.
 *
 * 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 int _dictKeyIndex(dict *d, const void *key)
{
    unsigned int h, idx, table;
    dictEntry *he;

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR)     //判断是否需要扩大哈希表的大小,如果正在 rehash,返回 true
                                                //如果used/size 大于安全阀值5时,将需要扩大哈希表大小
        return -1;
    /* Compute the key hash value */
    h = dictHashKey(d, key);        //计算哈希值
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;    //计算索引值,在哈希表中查找是否存在,存在返回-1,不存在,返回索引值
        /* Search if this slot does not already contain the given key */
        he = d->ht[table].table[idx];
        while(he) {
            if (dictCompareKeys(d, key, he->key))
                return -1;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

上面这个函数,为获取哈希值和索引值的过程, 首先通过 dictHashKey(d, key) 获取哈希值,这是一个宏函数

#define dictHashKey(d, key) (d)->type->hashFunction(key)

然后根据哈希值求索引值,idx = h & d->ht[table].sizemask,如果此元素在哈希表中已存在,返回-1,不需要再次插入,否则返回一个可以用的位置。搜索哈希表时,需要在两个哈希表中都要搜索。

那么,当 _dictExpandIfNeed (d) 判断出需要扩大哈希表时,是如何扩大哈希表的呢?_dictExpandIfNeed (d)这个函数在判断时:
1. 如果 rehashidx != -1 说明,哈希表在 rehash,此时表明就是正在扩大哈希
2. 如果 used/size (这两个都是 dictht 的成员)大于阀值 dict_force_resize_ratio(为5)时,就需要扩大哈希表。

扩大哈希表的函数如下

/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(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) //如果rehashidx不是-1,说明正在扩,不需重复操作;而且扩大后,used 应该小于 size
        return DICT_ERR;

    /* 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. */
    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;   // 将 ht[1] 设置为扩大后的哈希表,然后将 rehashidx 置为0,表明从 0 开始 rehash
    d->rehashidx = 0;
    return DICT_OK;
}

此函数需要注意最后一段代码

    d->ht[1] = n;   
    d->rehashidx = 0;

将扩大后的哈希设置为 ht[1],然后设置 rehashidx 为0,启动 rehash,将 ht[0] 都 从 ht[0].table[0] 开始全部 rehash 到 ht[1].table 中,后面将详细介绍 redis 的 rehash 的过程。

计算哈希值

字典在计算哈希值时,是通过调用宏函数 (d)->type->hashFunction (key)得到的,这只是一个函数指针,在 redis 中,有两个方法计算哈希值。

/* MurmurHash2, by Austin Appleby
 * Note - This code makes a few assumptions about how your machine behaves -
 * 1. We can read a 4-byte value from any address without crashing
 * 2. sizeof(int) == 4
 *
 * And it has a few limitations -
 *
 * 1. It will not work incrementally.
 * 2. It will not produce the same results on little-endian and big-endian
 *    machines.
 */
unsigned int dictGenHashFunction(const void *key, int len) {
    /* 'm' and 'r' are mixing constants generated offline.
     They're not really 'magic', they just happen to work well.  */
    uint32_t seed = dict_hash_function_seed;
    const uint32_t m = 0x5bd1e995;
    const int r = 24;

    /* Initialize the hash to a 'random' value */
    uint32_t h = seed ^ len;

    /* Mix 4 bytes at a time into the hash */
    const unsigned char *data = (const unsigned char *)key;

    while(len >= 4) {
        uint32_t k = *(uint32_t*)data;

        k *= m;
        k ^= k >> r;
        k *= m;

        h *= m;
        h ^= k;

        data += 4;
        len -= 4;
    }

    /* Handle the last few bytes of the input array  */
    switch(len) {
    case 3: h ^= data[2] << 16;
    case 2: h ^= data[1] << 8;
    case 1: h ^= data[0]; h *= m;
    };

    /* Do a few final mixes of the hash to ensure the last few
     * bytes are well-incorporated. */
    h ^= h >> 13;
    h *= m;
    h ^= h >> 15;

    return (unsigned int)h;
}

当字典被用作数据库的底层实现或者哈希键的底层实现时, redis 使用 Murmurhash2 算法来计算哈希的值。 Murmurhash 哈希算法是有 Austin Appleby 与 2008 年发明的,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。

/* And a case insensitive hash function (based on djb hash) */
// 这是一个比较简单的计算哈希值的方法,字符串哈希,就是不断乘以33
unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) {
    unsigned int hash = (unsigned int)dict_hash_function_seed;      //哈希方法的种子

    while (len--)
        hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */
    //hash << 5 + hash = hash * 2^5 + hash = hash * 32 + hash = hash * 33
    return hash;
}

解决键冲突

当不同的 key 利用哈希算法得到相同的 hash 值时,哈希表时如何解决冲突问题的呢?通过前面的哈希节点的结构可以看到dictEntry结构是一个链表的节点,有一个指向 dictEntry节点的指针成员,确实,哈希表就是通过拉链法(或者链地址法)来解决冲突问题的,每一个哈希节点都有一个next指针,相同哈希索引的多个节点可以通过next指针相连构成一个单链表,这就解决了冲突问题。
哈希链表

因为dictEntry 组成的单链表是没有尾节点的,每次插入一个节点都是从头部插入。

/* Low level add. This function adds the entry but instead of setting
 * a value returns the dictEntry structure to the user, that will make
 * sure to fill the value field as he wishes.
 *
 * This function is also directly exposed to the user API to be called
 * mainly in order to store non-pointers inside the hash value, example:
 *
 * entry = dictAddRaw(dict,mykey);
 * if (entry != NULL) dictSetSignedIntegerVal(entry,1000);
 *
 * Return values:
 *
 * If key already exists NULL is returned.
 * If key was added, the hash entry is returned to be manipulated by the caller.
 */
/*如果key已经在哈希表中已经存在,返回NULL;否则返回该哈希节点*/
dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;

    /* 判断是否正在 rehash,正在 rehash 中的哈希表与没有 rehash 的哈希表插入操作不同
     1. 当 rehash 时,需要将ht[0] 中的所有节点 rehash 到 ht[1]中,这时,为了保证ht[0]中的节点是一直减少的,插入时插入到ht[1]中
     2. 没有 rehash ,直接将节点插入到 ht[0]中
    */  
    if (dictIsRehashing(d)) _dictRehashStep(d); 

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry */
    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. */
    dictSetKey(d, entry, key);
    return entry;
}

从链表头插入,不需要单向遍历到链表尾部在插入节点,降低了插入节点时的时间复杂度 O(1)

rehash

哈希表的一个重要的比例参数为负载因子(load factor),它的计算公式为

load_factor = used / size; //代码中的注释为 USED/BUCKETS ratio

随着操作的不断进行,哈希表中的节点会逐渐的增加或者减少,为了维持 load factor 在合适的范围之内,程序需要对哈希表进行扩展 (expand) 或者收缩,而这,是通过 rehash 来完成的。

redis 对字典的哈希表进行 rehash 操作的步骤如下:
1) 为 ht[1] 分配空间,这个空间的大小取决于要执行的操作(扩展还是收缩),以及 ht[0] 当前包含的键值对的数量(ht[0].used):
  a. 如果是扩展操作,ht[1] 的大小为不小于 ht[0].used*2 的 2^n;
  b. 如果是收缩操作,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 做准备。

引用中的内容摘自 黄健宏的《Redis设计与实现》4.4节 rehash

扩展

/* Using dictEnableResize() / dictDisableResize() we make possible to
 * enable/disable resizing of the hash table as needed. This is very important
 * for Redis, as we use copy-on-write and don't want to move too much memory
 * around when there is a child performing saving operations.
 *
 * Note that even when dict_can_resize is set to 0, not all resizes are
 * prevented: a hash table is still allowed to grow if the ratio between
 * the number of elements and the buckets > dict_force_resize_ratio. */
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;

以上两个 static 变量说明的是允许哈希表进行扩展或者收缩的前提条件。

redis 使用了写时复制的原则(COW),打个比方,当父进程创建子进程时,这时,采用写时复制的原则,父子进程访问的数据都是同一份拷贝,只有当其中一个需要对数据进行修改时,才会将父进程的内容拷贝一份给子进程,这样能够极大的节约内存。所以,当 redis 创建服务器子进程时,通过写时复制原则节约内存,就希望在子进程存在期间,尽可能的不要对哈希表进行扩展操作,服务器通过增大负载因子来达到这一目的,从而最大限度的节约内存。

dict_can_resize设置为1时,表示哈希表可以扩展或者收缩,设置为0时,表示不能操作,但是,当负载因子超过阈值 dict_forece_resize_ratio时,会强制进行扩展操作。上文也介绍了 _dictExpandIfNeeded这个函数

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;

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

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    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;
}

从代码中可以看出:

  • 当负载因子(load factor)达到 1 时,如果允许扩展(dict_can_resize设置为1),就进行扩展操作
  • 当负载因子超过安全阈值 dict_force_resize_ratio时,就会强制进行扩展

收缩

当哈希表节点数减少时,为了保证 load factor 在一个合理范围内,需要对哈希表进行收缩,当负载因子小于等于 1 时,就需要做这种操作。

/* Resize the table to the minimal size that contains all the elements,
 * but with the invariant of a USED/BUCKETS ratio near to <= 1 */
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;
    return dictExpand(d, minimal);
}

哈希表调整大小的计算

哈希表进行扩展或者收缩,新的哈希大小是如何计算的呢

/* Our hash table capability is a power of two */
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;

    if (size >= LONG_MAX) return LONG_MAX;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

DICT_HT_INITAL_SIZE的值为4,即 2^2,所以每次设置的新哈希表的大小均为 2 的 n 次幂。

渐进式 rehash

哈希表 rehash 操作时将 ht[0] 中的所有键值对 rehash 到 ht[1] 中,但是这个动作并不是一次性、集中式的完成的,而是渐进式的、分多次完成的。因为当键值对数量很大时,集中式的 rehash 操作,需要庞大的计算量,可能会导致服务器在一段时间内停止服务。为了避免 rehash 对服务器性能造成影响,所以将 rehash 操作分多次执行,渐进式的操作。

1) 当需要 rehash 时,将 rehashidx 设置为0
2) 为 ht[1] 分配空间,此时字典同时拥有 ht[0] 和 ht[1] 两个哈希表
3) 在 rehash 期间,通过判断 rehashidx 是否等于 -1,每次对字典进程添加、删除、查找和更新操作时,出了执行指定操作之外,还会将 ht[0].table[rehashidx] 这个单链表上的所有键值对 rehash 到 ht[1] 中,操作完成后,将 rehashidx 加 1
4) 当 rehash 操作完成后,rehashidx 的值被设置为 -1

/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, however
 * since part of the hash table may be composed of empty spaces, it is not
 * guaranteed that this function will rehash even a single bucket, since it
 * will visit at max N*10 empty buckets in total, otherwise the amount of
 * work it does would be unbound and the function may block for a long time. */
// 返回0表示 rehash 结束,返回1 表示还有键值对需要继续 rehash 
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

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

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        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;
        }
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) {
            unsigned int h;

            nextde = de->next;
            /* Get the index in the new hash table */
            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++;     //操作完将 rehashidx 加1
    }

    /* Check if we already rehashed the whole table... */
    if (d->ht[0].used == 0) {   //判断是否 rehash 操作完成
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}

哈希操作

查找操作

查找操作,当 rehashidx 的值不等于 -1 时,说明正在 rehash,那么进行一次rehash操作,在 _dictRehashStep(d)中调用 dictRehash(d, 1)执行一次。

查找时,在 ht[0] 和 ht[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 */
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            if (dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

添加

添加操作在上文中解决键冲突一节已经介绍了。

删除

删除操作,先在哈希表中查找指定 value 的键值对,然后删除。

int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0);
}

/* Search and remove an element */
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 */
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);    //获取哈希值

    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;    //计算索引值
        he = d->ht[table].table[idx];
        prevHe = NULL;
        while(he) {
            if (dictCompareKeys(d, key, he->key)) { //找到了
                /* Unlink the element from the list */
                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;
        }
        if (!dictIsRehashing(d)) break;
    }
    return DICT_ERR; /* not found */
}

更新

/* Add an element, discarding the old if the key already exists.
 * Return 1 if the key was added from scratch, 0 if there was already an
 * element with such key and dictReplace() just performed a value update
 * operation. */
int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, auxentry;

    /* Try to add the element. If the key
     * does not exists dictAdd will suceed. */
    if (dictAdd(d, key, val) == DICT_OK)
        return 1;
    /* It already exists, get the entry */
    entry = dictFind(d, key);
    /* Set the new value and free the old one. Note that it is important
     * to do that in this order, as the value may just be exactly the same
     * as the previous one. In this context, think to reference counting,
     * you want to increment (set), and then decrement (free), and not the
     * reverse. */
    auxentry = *entry;
    dictSetVal(d, entry, val);
    dictFreeVal(d, &auxentry);
    return 0;
}

参考文献:
1. Redis设计与实现,黄健宏
2. 博客文章[http://blog.csdn.net/androidlushangderen/article/details/39860693]

发布了56 篇原创文章 · 获赞 70 · 访问量 8万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览