redis 源码分析(一)HashTable 上篇

转载请注明出处:https://www.jianshu.com/p/a57a6e389a03

redis 中的 HashTable 实现,是一个叫 dict 的结构体以及其相关的操作函数。本文将对 dict 中重要的结构体、操作方法进行介绍,阐述其实现逻辑,对于 redis 生命周期内对 dict 的其他操作,我会进一步的补充。另外,为了让源码能被更好的理解,我在必要的地方进行了改写,虽然这样可能使得源码不再可以正确运行,甚至无法通过语法检测。

一、重要的结构体

1. dict

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; 
    unsigned long iterators;
} dict;
  • dict.dictType 指定了该 dict 的实现方式。
  • dict.dictht 是一个包含 2 个 dictht 的数组
  • dict.rehashidx 表明了该 dict 进行 rehashing 的进度,-1:表示此时并没有在 rehashing。
  • dict.iterators 表明现在正在操作该 dict 的迭代器数量。

2. dictType

typedef struct dictType {
    uint64_t (*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.hashFunction 计算 key 在 [0, dict.dictht.sizemask] 范围内的 hash 值,这个值是 value 存放到 dict.dictht.table 数组中的下标。
  • dictType.keyDup
  • dictType.valDup 如何将 value 放入 dictEntry 。可以对 value 的进行处理,默认 null 则表示不需要处理
  • dictType.keyCompare 如何比较 key1 和 key2 是否相等,默认 null 表示 key1==key2 时相等
  • dictType.keyDestructor 当释放 key 时的回调
  • dictType.valDestructor 当需要释放 value 时的回调

3. dictht

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
  • dictht.table 是一个 dictEntry 数组的起始指针,用来存在 value。
  • dictht.size 记录了目前 dictht.table 的容量(即:数组长度)。
  • dictht.sizemask dictht.table 容量的掩码。
  • dictht.used 记录了 dict 中目前存放的 value 数量。

4. dictEntry

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
  • dictEntry.key 存放 key
  • dictEntry.v 存放 value
  • dictEntry.next 是下一个 dictEntry 的指针。相同 dict.dictht.table 下标的 dictEntry 会 以单向链表的方式存储,因此,dictEntry.next 是这个单向链表的头指针

二、重要的操作方法

1. dictCreate 用于创建一个空的 dict

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;
    d->iterators = 0;
    return DICT_OK;
}
static void _dictReset(dictht *ht){
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}

可以看到:
dictCreate 方法只是创建了一个空的 dict 实例,这个 dict 的各个属性都处于初始值,甚至连 dict.dictht[0].tabledict.dictht[1].table 都还是 null
此时的 dictjavascript 对象表示,有点儿像:

{
    "privdata": null,
    "rehashidx": -1,
    "iterators": 0,
    "dictType": {
        "hashFunction": function(key){return parse_int(key)%10;},
        "keyDup": function(privdata, key){},
        "valDup": function(privdata, value){return value;},
        "keyCompare": function(privdata, key1, key2){return key1===key2;},
        "keyDestructor": function(privdata, key){unset kye;},
        "valDestructor": function(privdata, obj){unset obj;}
    },
    "dictht": [
        {
            "size": 0,
            "sizemask": 0,
            "used": 0,
            "table": null,
        },
        {
            "size": 0,
            "sizemask": 0,
            "used": 0,
            "table": null,
        }
    ]
}

2.dictExpanddict 进行“扩容”

static int _dictExpandIfNeeded(dict *d){
    if (dictIsRehashing(d)) return DICT_OK;
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
    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;
}
int dictExpand(dict *d, unsigned long size){
    if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR;
    dictht n;
    unsigned long realsize = _dictNextPower(size);
    if (realsize == d->ht[0].size) return DICT_ERR;
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

由源码可见:

dict.dictht[0] 触发增加容量的条件是:
  • dict.dictht[0].size=0;意味着:刚初始化 dict,还没初始化 dict.dictht[0].table 时。
  • dict.dictht[0].used >= dict.dictht[0].sizedict_can_resize=1;当 dict_can_resize=1 并且 dict.dictht[0] 存放了 dict.dictht[0].size 以上个 value 时。
  • dict.dictht[0].used >= dict.dictht[0].sizedict.dictht[0].useddict.dictht[0].size 5倍以上时。
dict.dictht[0] 增加容量到多少:
  • 当初始化 dict.dictht[0].table 时,会将容量增大到 DICT_HT_INITIAL_SIZE,也就是 4 个。
  • 其他情况,会将容量增大到 大于 dict.dictht[0].used 2倍的第一个 2^n
③ 扩容意外:
  • 即使触发了增加容量的动作,但如果此时 dict 正在执行 rehashing,将放弃此次增加容量。
④ 当 dict 扩容成功后,dict 将开启 rehash

3. dictAdddict 中存入一对 key=>value

#define dictIsRehashing(d) ((d)->rehashidx != -1)
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing){
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;
    if (_dictExpandIfNeeded(d) == DICT_ERR) return -1;
    for (table = 0; table <= 1; table++) {
        idx = hash & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || d.dictType.keyCompare(d.privdata, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing){
    long index;
    dictEntry *entry;
    dictht *ht;
    if (dictIsRehashing(d)) _dictRehashStep(d);
    if ((index = _dictKeyIndex(d, key, d.dictType.hashFunction(key), existing)) == -1) return NULL;
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;
    dictSetKey(d, entry, key);
    return entry;
}
#define dictSetVal(d, entry, _val_) do { \
    if ((d)->type->valDup) \
        (entry)->v.val = (d)->type->valDup((d)->privdata, _val_); \
    else \
        (entry)->v.val = (_val_); \
} while(0)
int dictAdd(dict *d, void *key, void *val){
    dictEntry *entry = dictAddRaw(d,key,NULL);
    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}

由源码可以得到,在 dict 中存入一个键值对有两个步骤:

① 在 key 应该存放的位置,先放一个 v=nulldictEntry
  • 如果需要 rehash,则先进行 rehash
  • 使用 dict.dictType.hashFunction(key),计算 key 应该存放的 hash
  • 验证 dict.dictht.table 中的 hash 位置 能否存放 value。如果能,返回当前 key 的 hash 值应该存放在 dict.dictht.table 中的下标 index
  • dict.dictht.table 中的 index 位置存放一个 v=null 的 dictEntry,然后对 dict 中已经存放的 value 个数进行计数 dict.dictht.used++
② 如果放置空 dictEntry 成功,再对 dictEntry 进行赋值

需要注意的是:如果 dict 开启了 rehash,则 dictAdd 将会把 key=>value 存放到 dict.dictht[1].table

4. dictReplace 覆盖 key 的 value

int dictReplace(dict *d, void *key, void *val){
    dictEntry *entry, *existing, auxentry;
    if (entry) {
        dictSetVal(d, entry, val);
        return 1;
    }
    auxentry = *existing;
    dictSetVal(d, existing, val);
    dict.dictType.valDestructor(&auxentry);
    return 0;
}

dictReplace 的实现和 dictAdd 的实现很相似,区别在于 dictReplace 会尝试调用 dict.dictType.valDestructor(&auxentry) 将被替换的值进行删除

5. dictRehash dictRehashMillisecondsdict 进行 rehash

int dictRehashMilliseconds(dict *d, int ms) {
    long long start = timeInMilliseconds();
    int rehashes = 0;
    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}
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;
        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];
        while(de) {
            uint64_t 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) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }
    return 1;
}

由源码可知,redis rehash 的过程是:

  • redis rehash 是将 dict.dictht[0].table 中存储的 dictEntry 逐步 rehash 后存储到 dict.dictht[1].table 中的过程,这个过程往往是 分成多次执行的,甚至支持 现在先执行 100ms 的 rehash
  • 每次 rehash dict.dictht[0].table 的 n 个下标或者访问了 10n 个下标都没能成功 rehash n 个下标 时,退出
  • 因为 dict.dictht[0].table 中存储的 dictEntry 可能包含了一个 dictEntry 的单向链表(hash 冲突),因此:rehash n 个下标并不意味着 n 个 key,实际上 n 个下标可能包含大于 n 个 key
  • dict 的 rehash 是从 dict.rehashidx 开始的,每成功 rehash 一个下标 dict.rehashidx++,每成功 rehash 一个 key,dict.dictht[0].used--
  • dict.dictht[0].used=0 时,意味着 dict 的 rehash 操作完成了,此时只需要将 dict.dictht[1].table 赋值给 dict.dictht[0].table,然后对 dict.dictht[1].table 进行初始化,同时将 dict.rehashidx=1 整个 rehash 过程就完成了

6. dictFinddict 中查找指定 key

dictEntry *dictFind(dict *d, const void *key){
    dictEntry *he;
    uint64_t h, idx, table;
    if (d->ht[0].used + d->ht[1].used == 0) return 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];
        while(he) {
            if (key==he->key || dict.dictType.keyCompare(key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

在 dict 中查找的过程相对简单:

  • 计算要查找的 key 的 hash 值 index(在 dict.dictht[0/1].table 中存储的下标)
  • dict.dictht[0].table[index]dict.dictht[1].table[index] 所对应的单向链表中,从链头开始向尾查找
  • 查找的时间复杂度为 O(n)=m+n+1m 和 n 分别是 dict.dictht[0].table[index]dict.dictht[1].table[index] 的长度)。因此 查找的快慢取决于 dict 中 hash 冲突的多少,对 dict 的扩容和 rehash,正是为了减少 hash 冲突,同时,选择更好的 Hash Function,使得 hash 的结果更加均匀也能减少 hash 冲突 的频率

7. dictGenericDeletedict 中删除 key

static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    uint64_t h, idx;
    dictEntry *he, *prevHe;
    int table;
    if (d->ht[0].used == 0 && d->ht[1].used == 0) return 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 (key==he->key || dict.dictType.keyCompare(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 he;
            }
            prevHe = he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL;
}

dict 中删除 key 就是在找到 key 所在 dictEntry 单向链表的结点后,执行链表的删除,同时更新 dict.dictht[0/1].used--

8. dictUnlink 在链表中删除 key 但不释放内存

void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
    if (he == NULL) return;
    dictFreeKey(d, he);
    dictFreeVal(d, he);
    zfree(he);
}
dictEntry *dictUnlink(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,1);
}

dictUnlinkdictDelete 的区别是:是否立即释放要删除的 dictEntry 内存(注意:在 dictUnlink 时,其实已经完成了 dictEntry 的链表删除)。dictUnlink 的意义在于:在真正释放 dictEntry 前,可以执行用户逻辑,比如:输出一下 key 的 value,再删除。

  • 不使用 dictUnlink:
entry = dictFind(key);
print entry.v;
dictDelete(key)
  • 使用 dictUnlink:
entry = dictUnlink(key);
print entry.v;
dictFreeUnlinkedEntry(entry);

两种写法的区别是:不使用 dictUnlink 时,在 dictFinddictDelete 中分别执行了一次查询操作;使用 dictUnlink 时,只在 dictUnlink 中执行了一次查询操作。

9. dictRelease 删除并释放整个 dict

int _dictClear(dict *d, dictht *ht, void(callback)(void *)) {
    unsigned long i;
    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;
        while(he) {
            nextHe = he->next;
            dict.dictType.keyDestructor(d, he);
            dict.dictType.valDestructor(d, he);
            zfree(he);
            ht->used--;
            he = nextHe;
        }
    }
    zfree(ht->table);
    _dictReset(ht);
    return DICT_OK;
}
void dictRelease(dict *d){
    _dictClear(d,&d->ht[0],NULL);
    _dictClear(d,&d->ht[1],NULL);
    zfree(d);
}
  • 删除整个 dict 的重点是需要释放 dict.dictht[0].tabledict.dictht[1].table 中的所有 dictEntry,否则会造成内存泄漏。
  • 在释放每个 dictEntry 时,调用 dict.dictType.keyDestructordict.dictType.valDestructor 的意义在于:通知用户代码释放 key 和 value 的用户指针。

三、结论

  • redis 中 HashTable 的实现是一个叫 dict 的结构体及其相关的操作方法。
  • dict.dictType 指定了 dict 的具体实现,它使得一个 dict 有别于另一个 dict
  • key=>value 被存储到了 dictEntry 中,dictEntry 存储在 dictht 中。dict 中有两个 dictht,在没有开启 rehash 时,数据存放在 dict.dictht[0].table 中,一旦满足了 rehash 条件,数据将开始往 dict.dictht[1].table 中存储。
  • dict.dictht[0].used >= dict.dictht[0].size && dict_can_resize=1 或者 dict.dictht[0].used >= dict.dictht[0].size && dict.dictht[0].used / dict.dictht[0].size >=5 时,将对 dict 进行扩容,扩容至 大于 dict.dictht[0].used 2倍的第一个 2^n,扩容成功后,将开启 dict 的 rehash 过程。
  • dict 的 rehash 过程是 分多次完成的,具体过程是: 从 dict.rehashidx 开始,按照扩容后的 dict.dictht[1].sizemaskdict.dictht[0].table 中的每个 dictEntry (如果 dict.dictht[0].table[dict.rehashidx] 产生过 hash 冲突,则会通过 dictEntry.next 指向链表头指针,遍历链表中的所有 dictEntry)重新计算 hash 值(在 dict.dictht[1].table 中的下标),移动到 dict.dictht[1].table 中去。等到 dict.dictht[0].table.used=0 时,说明整个 rehash 过程完毕,会将 dict.dictht[1] 赋值给 dict.dictht[0],同时关闭 dict 的 rehash 标志(dict.rehashidx=-1

四、设计问答

1. 为什么要对 dict 进行扩容?扩容后 rehash 的意义是什么?

dict.dictht.table 的默认大小是 4,当 key=>value 数据大于 4 时,必然出现多个 key 的 hash 值相同,在 dict.dictht[0].table 中的下标一致,从而导致 dict.dictht.table 其实是一个越来越长的链表,此时对 dict 的操作会退化为对链表的操作,即:时间复杂度从 O(1) 逐渐趋于 O(n)。对 dict 扩容后,使得存储相同数量 key=>value 时产生的 hash 冲突变少,从而链表长度变短,进而操作的 时间复杂度从 O(n) 逐渐趋于 O(1)

2. 为什么要 一次 rehash n 个下标,而不是一次性把所有下标都 rehash 完?

redis 是一个单线程(不考虑持久化线程)的应用,如果一次性 rehash 完所有下标,会导致 redis 在一定时间内无法提供服务,于是 redis 巧妙的将 这个一大段时间分摊到了每次操作这个 dict 都执行一部分的一小段时间,既保证了服务可用,又能完成 rehash 过程,提高性能。

3. 为什么扩容条件是 5 倍?为什么扩容到 大于 dict.dictht[0].used 2倍的第一个 2^n?

我猜测,5倍 应该是一个经验值,即避免了频繁扩容、hash导致的性能下降,又避免了 时间复杂度从 O(1) 逐渐趋于 O(n) 的可能。而扩容至 大于 dict.dictht[0].used 2倍的第一个 2^n 应该和选用的 Hash Function 有关,毕竟 redis 的 Hash Function 中大量使用了 << 和 '>>'

本来想在本篇中继续介绍 dict.iterator 以及 redis dict 相关命令的实现方式的,限于篇幅太长,会在 redis 源码分析(一)HashTable 下篇 中继续介绍,敬请期待。
另外,码字不易,喜欢的朋友点个赞,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容