Redis字典

redis/src/dict.h; redis/src/dict.c为redis字典实现的源码, redis中的字典底层使用hash实现,这意味着相比于红黑树的实现方式,redis的字典是无序的。redis中数据库的管理、hash键均有字典的身影。

1. hash表

hash表旨在通过key在O(1)时间到key对应的值,其中涉及到: 对key的hash计算生成索引; hash冲突的解决方案;rehash。redis中hash表的hash算法采用MurmurHash2d,采用拉链法解决冲突。

1.1 hash节点

  • key: void* 类型
  • v: union类型,可以是void*,有符号/无符号整数,double浮点数
  • next: 存在hash冲突时,链接同一索引位置的hash节点
  • metadata: 允许hash节点携带额外的私有数据
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     /* Next entry in the same hash bucket. */
    void *metadata[];           /* An arbitrary number of bytes (starting at a
                                 * pointer-aligned address) of size as returned
                                 * by dictType's dictEntryMetadataBytes(). */
} dictEntry;

1.2 与hash计算相关的函数

操作hash节点相关的函数,例如:要想得到节点的索引值,需要配套与之对应的hash算法,redis将这些api封装在dictType结构体中。

  • hashFunction 根据key值计算hash结果
  • keyDup 复制字典d中key的值
  • valDup 复制字典d中值为obj的值
  • keyCompare 比较字典d中两个key是否相同
  • keyDestructor 字典中hash节点key的销毁
  • valDestructor 字典中hash节点val的销毁
  • dictEntryMetadataBytes 返回一个字典中携带的总的metadata大小
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);
    int (*expandAllowed)(size_t moreMem, double usedRatio);
    /* Allow a dictEntry to carry extra caller-defined metadata.  The
     * extra memory is initialized to 0 when a dictEntry is allocated. */
    size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;

2. 字典

  • type 操作hash节点的api集合
  • ht_table 指向两个hash表
  • ht_used 当前字典存在多少个hash节点
  • rehashidx 标记是否在进行rehash操作,及rehash的当前索引值
  • pauserehash rehash是否暂停的标志位
  • ht_size_exp 记录两个哈希表中size相关的exp数( 1<<exp >= size) 我们知道,hash表在负载因子比较大的时候,存在较多的hash冲突,对于拉链法,访问key对应节点的速度降低比较明显,这时需要进行rehash, redis字典中采用了两个hash表,其中ht_table[1]
    可以理解为备用hash表,用于rehash时使用。
struct dict {
    dictType *type;

    dictEntry **ht_table[2];
    unsigned long ht_used[2];

    long rehashidx; /* rehashing not in progress if rehashidx == -1 */

    /* Keep small vars at end for optimal (minimal) struct padding */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
    signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};

3. 宏

  • do while(0)在宏定义中主要是在又复杂逻辑时,避免{},;对逻辑造成影响
// 释放字段d的entry节点
#define dictFreeVal(d, entry) \
    if ((d)->type->valDestructor) \
        (d)->type->valDestructor((d), (entry)->v.val)
// 将字典d的entry节点的值设为 _val_, 默认是浅拷贝
#define dictSetVal(d, entry, _val_) do { \
    if ((d)->type->valDup) \
        (entry)->v.val = (d)->type->valDup((d), _val_); \
    else \
        (entry)->v.val = (_val_); \
} while(0)

// 设置hash节点entry的有符号整型的值
#define dictSetSignedIntegerVal(entry, _val_) \
    do { (entry)->v.s64 = _val_; } while(0)
// 设置hash节点entry的无符号整型的值
#define dictSetUnsignedIntegerVal(entry, _val_) \
    do { (entry)->v.u64 = _val_; } while(0)
// 设置hash节点entry的double的值
#define dictSetDoubleVal(entry, _val_) \
    do { (entry)->v.d = _val_; } while(0)
// 释放hash节点entry的key
#define dictFreeKey(d, entry) \
    if ((d)->type->keyDestructor) \
        (d)->type->keyDestructor((d), (entry)->key)
// 设置hash节点entry的key值为_key_
#define dictSetKey(d, entry, _key_) do { \
    if ((d)->type->keyDup) \
        (entry)->key = (d)->type->keyDup((d), _key_); \
    else \
        (entry)->key = (_key_); \
} while(0)

// 在字典d中比较两个key是否相等
#define dictCompareKeys(d, key1, key2) \
    (((d)->type->keyCompare) ? \
        (d)->type->keyCompare((d), key1, key2) : \
        (key1) == (key2))

// 返回hash节点entry的私有数据
#define dictMetadata(entry) (&(entry)->metadata)
// 返回hash节点entry的私有数据的大小
#define dictMetadataSize(d) ((d)->type->dictEntryMetadataBytes \
                             ? (d)->type->dictEntryMetadataBytes(d) : 0)
// 判断字典d中是否包含指定的key
#define dictHashKey(d, key) (d)->type->hashFunction(key)
// 获取hash节点he的key
#define dictGetKey(he) ((he)->key)
// 获取hash节点he的val
#define dictGetVal(he) ((he)->v.val)
// 获取hash节点he的有符号整型val
#define dictGetSignedIntegerVal(he) ((he)->v.s64)
// 获取hash节点he的无符号整型val
#define dictGetUnsignedIntegerVal(he) ((he)->v.u64)
// 获取hash节点he的浮点型val
#define dictGetDoubleVal(he) ((he)->v.d)
// 整个字典当前的满载节点数
#define dictSlots(d) (DICTHT_SIZE((d)->ht_size_exp[0])+DICTHT_SIZE((d)->ht_size_exp[1]))
// 整个字典当前的使用节点数
#define dictSize(d) ((d)->ht_used[0]+(d)->ht_used[1])
// 字典当前是否处于rehash状态
#define dictIsRehashing(d) ((d)->rehashidx != -1)
#define dictPauseRehashing(d) (d)->pauserehash++
#define dictResumeRehashing(d) (d)->pauserehash--

redis hash数据结构 redis中hash的数据结构_sed

4. 字典api

4.1 几个全局变量

  • static int dict_can_resize = 1; 是否能进行rehash,当没有支持持久化命令时,该值为1表示可以进行rehash
  • static unsigned int dict_force_resize_ratio = 5; 当执行持久化命令时,负载因子大于该值才能进行rehash,尽量避免在持久化时进行rehash
  • static uint8_t dict_hash_function_seed[16]; 随机数种子,用于生成hash值
// 设置随机数种子
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;
}

对应的,通过一下两个接口生成hash值,uint64_t类型。

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);
}
  • static signed char _dictNextExp(unsigned long size); 返回 n满足 2^n >=size
  • C语言中并无显示的访问权限控制,源码中以_开头的变量或函数表明,不希望被外部访问。

4.2 创建字典,resize字典

创建空字典

根据字典的属性,我们知道创建字典时,需要初始化type,两个hash表,used, exp,rehashidx,pauserehash

  • _dictReset reset一个hash表,与hash表相关的used=0,exp=-1
  • _dictInit Init一个字典,rehash标记为-1,pauserehash=-1
  • ``dictCreate` 根据输入的dictType类型创建一个空的字典,这里redis申请内存使用封装的zmalloc,内存申请失败直接abort()
    另外这里输入参数是指针,且是浅拷贝。
/* Create a new hash table */
dict *dictCreate(dictType *type)
{
    dict *d = zmalloc(sizeof(*d));

    _dictInit(d,type);
    return d;
}

/* Initialize the hash table */
int _dictInit(dict *d, dictType *type)
{
    _dictReset(d, 0);
    _dictReset(d, 1);
    d->type = type;
    d->rehashidx = -1;
    d->pauserehash = 0;
    return DICT_OK;
}
/* 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;
}

resize字典

常用于第一次创建字典或是rehash时调整hash节点的数量

  • 初始化时
    在ht_table[0]位置申请一个hash表,填入hash表对应的属性
  • 非初始化时
    在ht_table[1]位置申请一个hash表
  1. 判断当前时候能进行resize,若是字典处于rehash状态或是负载因子为0,则直接返回DICT_ERR
  2. 根据ht_table[0]的used大小获取exp大小从而计算出新的new_size
  3. 若是new_size不大于当前ht_table[0]的used,则不需要进行调整
  4. 判断是否是首次resize,true则作用于ht_table[0]否则作用于ht_table[1]rehashidx此时设为0
  • 注意dictResize过程中多次判断是否处于rehash状态(主函数和调用函数中均有判断)
  • _dictExpand的第三参数,非空的时候允许在resize时内存申请失败(调用ztrycalloc),此时 malloc_failed = 1,空的时候内存申请失败则oom
  • _dictNextExp根据输入的size,计算一个2^n>=size的exp数n
  • dictExpand resize失败直接oom, dictTryExpand resize失败返回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)
{
    unsigned long minimal;
    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    minimal = d->ht_used[0];
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);
}

/* Expand or create the hash table,
 * when malloc_failed is non-NULL, it'll avoid panic if malloc fails (in which case it'll be set to 1).
 * Returns DICT_OK if expand was performed, and DICT_ERR if skipped. */
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
    if (malloc_failed) *malloc_failed = 0;

    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht_used[0] > size)
        return DICT_ERR;

    /* the new hash table */
    dictEntry **new_ht_table;
    unsigned long new_ht_used;
    signed char new_ht_size_exp = _dictNextExp(size);

    /* Detect overflows */
    size_t newsize = 1ul<<new_ht_size_exp;
    if (newsize < size || newsize * sizeof(dictEntry*) < newsize)
        return DICT_ERR;

    /* Rehashing to the same table size is not useful. */
    if (new_ht_size_exp == d->ht_size_exp[0]) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    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;

    /* 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_table[0] == NULL) {
        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;
    }

    /* Prepare a second hash table for incremental 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;
    return DICT_OK;
}

/* return DICT_ERR if expand was not performed */
int dictExpand(dict *d, unsigned long size) {
    return _dictExpand(d, size, NULL);
}

/* return DICT_ERR if expand failed due to memory allocation failure */
int dictTryExpand(dict *d, unsigned long size) {
    int malloc_failed;
    _dictExpand(d, size, &malloc_failed);
    return malloc_failed? DICT_ERR : DICT_OK;
}

4.3 rehash

渐进式rehash

当进行rehash时,需要将ht_table[0]哈希表中的内容映射到 ht_table[1], 为避免某一条请求阻塞时间过长,这个过程是分别完成的,
一个请求到来时迁移一个ht_table[0]的hash节点(ht_table[0][rehashidx]节点及链表)到ht_table[1];可想而知,若是rehash过程中发生查找,
则需要排查两个hash表;rehash期间为保证等待rehashidx之前的节点位置不会出现新节点且等待rehash的节点数始终递减,对于插入操作在ht_table[1]hash表中进行。

  • 输入参数n,预期进行n步的rehash.
  • empty_visits, 这个参数很有趣, 在进行rehash时,我们首先要在ht_table[0]hash表中找到rehashidx后的第一个非空节点,寻找的过程可能访问多个空节点,redis考虑到不让一次rehash阻塞太久
    限制n步rehash中最多经过n*10个空的hash节点,到数后提前返回,避免长时间阻塞。
  • rehash时老的节点插入到ht_table[1]对应索引的链表头节点位置(拉链为单项链表,无记录尾节点)
  • 迁移一个hash节点时,实际为迁移hash表该索引对应的整条单向链表
  • 若是rehash结束,则进行ht_table的切换,返回0,否则返回1,表示仍需要继续rehash
/* 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. */
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_used[0] != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);
        // 跳过空节点,超出最大允许的空节点数,则提前返回,避免长时间阻塞
        while(d->ht_table[0][d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht_table[0][d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
            // 当前节点插入到ht_table[1]对应索引位置的链表中的头节点位置
            de->next = d->ht_table[1][h];
            d->ht_table[1][h] = de;
            d->ht_used[0]--;
            d->ht_used[1]++;
            de = nextde;
        }
        // 当前节点迁移完成;rehashidx自增
        d->ht_table[0][d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    // 判断rehash是否结束,是以used=0为准,可见此构成不允许插入新节点到旧表
    if (d->ht_used[0] == 0) {
        zfree(d->ht_table[0]);
        /* Copy the new ht onto the old one */
        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);
        d->rehashidx = -1;
        return 0;
    }

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

带有时间约束的rehash

redis dict中提供了带有时间约束的rehash过程, 每次rehash 100步计时器累计一次,超出允许的等待时间则退出。

long long timeInMilliseconds(void) {
    struct timeval tv;

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

/* Rehash in ms+"delta" milliseconds. The value of "delta" is larger 
 * than 0, and is smaller than 1 in most cases. The exact upper bound 
 * depends on the running time of dictRehash(d,100).*/
int dictRehashMilliseconds(dict *d, int ms) {
    if (d->pauserehash > 0) return 0;

    long long start = timeInMilliseconds();
    int rehashes = 0;

    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}

rehash 1 step

在向字典中添加kv时,若是正处于rehash进行中的状态,字典会执行一步rehash,也即dictRehash(d, 1)

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

4.4 [增] 添加kv到字典

底层数据本身不保证多线程安全

添加一个新节点到字典中,分为一下几步:

  1. 是否处于rehash状态,是的话执行一步rehash
  2. 根据key计算索引值(这个过程中发生了很多事)
    1. 根据hash算法计算key的hash值2. 判断当前字典是否需要进行扩容 (这里值得思考,为什么扩容设定在计算索引的步骤中)。3. 判断当前key是否已经存在。hash值 & hash表掩码得到索引值, 首先查找hash表0,若是处于rehash过程,进一步查找hash表1 ,若是已存在key,直接返回
  3. 得到索引值后,确定新节点插入到哪个hash表
  4. 插入到链表头部,设置新节点的key值,value值
  • 这里单独提一下判断当前hash表是否需要扩容的逻辑
    若当前字典空(实际为hash表0空),则扩容到初始大小 2 << 2 = 4
    条件A:当前实际使用的节点数不小于预期满载数2 << exp0 条件B: 全局变量dict_can_resize为1
    条件C: 负载因子大于高阈值5
    条件D: 与字典相关的type接口集中的expandAllowed(该函数接受两个参数1:预期扩容后的内存大小,当前负载因子)自定义条件
    那么扩容的条件为: A && D && (B || C),若当前used为n0,扩容后节点数为 n1, 满足 n0 < n1=2 << exp_x <= 2 << (exp_x + 1) 例如原used[0] = 5, exp[0]=2, 则扩容后, exp[0]=3, size[0]=8
int dictAdd(dict *d, void *key, void *val)
{
    // 尝试将key添加到字典d中,
    // 1. 若是key已存在则返回NULL;
    // 2. 不存在则返回新节点
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    // 设置新节点的值
    dictSetVal(d, entry, val);
    return DICT_OK;
}

// 向d中添加一个key, 但是不设置其值
// 1. 若key已经存在则返回 NULL, existting为存在节点
// 2. 若key不存在则返回新节点地址
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    int htidx;
    // 可以看到若是字典正在进行rehash,则尝试执行一步rehash
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 计算 key在字典中的索引
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    // 添加新节点的时候,若是正处于rehash过程,新节点插入hash表1
    htidx = dictIsRehashing(d) ? 1 : 0;
    size_t metasize = dictMetadataSize(d);
    entry = zmalloc(sizeof(*entry) + metasize);
    if (metasize > 0) {
        memset(dictMetadata(entry), 0, metasize);
    }
    // 新节点插入到链表表头;1.链表无尾指针,在表头添加更方便 2. 最近添加的元素更容易被访问到
    entry->next = d->ht_table[htidx][index];
    d->ht_table[htidx][index] = entry;
    d->ht_used[htidx]++;

    // 设置新节点的key
    dictSetKey(d, entry, key);
    return entry;
}

static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing)
{
    unsigned long idx, table;
    dictEntry *he;
    if (existing) *existing = NULL;

    // 扩容失败直接返回 -1 
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    for (table = 0; table <= 1; table++) {
        idx = hash & DICTHT_SIZE_MASK(d->ht_size_exp[table]);
        he = d->ht_table[table][idx];
        // 遍历链表,比较hash节点的key,判断是否已存在
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                if (existing) *existing = he;
                return -1;
            }
            he = he->next;
        }
        // 若是处于rehash状态,则继续在hash表1中计算
        if (!dictIsRehashing(d)) break;
    }
    return idx;
}

// 若字典d需要扩容则进行扩容
static int _dictExpandIfNeeded(dict *d)
{
    // 处于rehash状态是不进行扩容的
    if (dictIsRehashing(d)) return DICT_OK;

    // 空字典扩容到默认大小
    if (DICTHT_SIZE(d->ht_size_exp[0]) == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    // 判断是否需要扩容
    if (d->ht_used[0] >= DICTHT_SIZE(d->ht_size_exp[0]) &&
        (dict_can_resize ||
         d->ht_used[0]/ DICTHT_SIZE(d->ht_size_exp[0]) > dict_force_resize_ratio) &&
        dictTypeExpandAllowed(d))
    {
        return dictExpand(d, d->ht_used[0] + 1);
    }
    return DICT_OK;
}

// 根据创建字典时关联的dictType,判断自定义的是否允许扩容的方法是否通过
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]));
}
// 获取一个 n, 满足 2 << n >=size, && n >=2
static signed char _dictNextExp(unsigned long size)
{
    unsigned char e = DICT_HT_INITIAL_EXP;

    if (size >= LONG_MAX) return (8*sizeof(long)-1);
    while(1) {
        if (((unsigned long)1<<e) >= size)
            return e;
        e++;
    }
}

4.5 [删]

从字典中删除指定的key

// 存在的话直接删除key所在节点
int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}
// 若存在,从字典中踢出该节点,但暂不释放该节点
dictEntry *dictUnlink(dict *d, const void *key) {
    return dictGenericDelete(d,key,1);
}

// nofree = 0, 表示需要删除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;
    // 若是处于rehash状态,则执行一步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]);
        he = d->ht_table[table][idx];
        prevHe = NULL;
        while(he) {
            // 若找到该节点
            if (key==he->key || 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) {
                    dictFreeUnlinkedEntry(d, he);
                }
                d->ht_used[table]--;
                return he;
            }
            prevHe = he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL; /* not found */
}

// 释放一个已经从字典中踢出的hash节点
void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
    if (he == NULL) return;
    dictFreeKey(d, he);
    dictFreeVal(d, he);
    zfree(he);
}

删除字典

  • 删除字典需要 1. 删除hash节点 2. 删除两个hash表 3. 删除字典
void dictRelease(dict *d)
{
    _dictClear(d,0,NULL);
    _dictClear(d,1,NULL);
    zfree(d);   // 释放字典头
}
// 释放一个hash表,在释放第一个hash节点时调用回调函数
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;
        // 释放hash节点对应的链表
        while(he) {
            nextHe = he->next;
            dictFreeKey(d, he);
            dictFreeVal(d, he);
            zfree(he);
            d->ht_used[htidx]--;
            he = nextHe;
        }
    }
    // 释放hash表
    zfree(d->ht_table[htidx]);
    // inithash表相关属性
    _dictReset(d, htidx);
    return DICT_OK; /* never fails */
}

4.6 [改]

替换

存在key则为其设置新val,不存在则插入,注意为节点设置新值的时候的顺序,先设置新值在释放老值,官方解释是:对于引用计数而言,
若是先释放,则可能引起引用计数为0,提前销毁的case。

int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, *existing, auxentry;
    // 时新插入的节点
    entry = dictAddRaw(d,key,&existing);
    if (entry) {
        dictSetVal(d, entry, val);
        return 1;
    }

    /* 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. */
    // 浅拷贝一份,因为节点中的cal指针接下来要发生变化
    auxentry = *existing;
    dictSetVal(d, existing, val);
    dictFreeVal(d, &auxentry);
    return 0;
}

同样,利用dictAddRaw可以很方便实现add or find

dictEntry *dictAddOrFind(dict *d, void *key) {
    dictEntry *entry, *existing;
    entry = dictAddRaw(d,key,&existing);
    return entry ? entry : existing;
}

4.7 [查]

查找key对应节点/value

这里的find与上述几个函数有重叠之处。

dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    uint64_t h, idx, table;

    if (dictSize(d) == 0) return NULL; /* dict is empty */
    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]);
        he = d->ht_table[table][idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}

void *dictFetchValue(dict *d, const void *key) {
    dictEntry *he;

    he = dictFind(d,key);
    return he ? dictGetVal(he) : NULL;
}

遍历字典

  1. dict迭代器结构
  • d 迭代器绑定的字典
  • index 当前索引
  • table, 当前遍历的是哪个hash表
  • safe, 安全遍历, 安全遍历的情况下可以执行增删改查的操作,非安全遍历只能调用Next函数进行遍历
  • fingerprint 标记字典的唯一标识,用于验证字典状态是否发生变化,与hash表地址,exp, used,整数hash算法相关。
  • entry,当前遍历的hash节点, nextEntry下一个hash节点。初始化一个迭代器的时候,index=-1,entry,nextEntry为NULL.
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    unsigned long long fingerprint;
} dictIterator;
  1. Next函数
    对于安全迭代器,该迭代器在创建之后会执行一次dictPauseRehashing,相当于计数++,销毁的时候–;
dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
        if (iter->entry == NULL) {
            // 迭代器刚初始化完成
            if (iter->index == -1 && iter->table == 0) {
                if (iter->safe) // 安全迭代器,对pause计数自增
                    dictPauseRehashing(iter->d);
                else // 获取迭代之前的字典状态
                    iter->fingerprint = dictFingerprint(iter->d);
            }
            iter->index++;
            if (iter->index >= (long) DICTHT_SIZE(iter->d->ht_size_exp[iter->table])) {
                // hash表遍历到尾部,若是处于rehash状态则继续遍历hash[1]
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    iter->table++;
                    iter->index = 0;
                } else {
                    break;
                }
            }
        // 对entry和nextEntry进行赋值
            iter->entry = iter->d->ht_table[iter->table][iter->index];
        } else {
            iter->entry = iter->nextEntry;
        }
    
        if (iter->entry) {
            iter->nextEntry = iter->entry->next;
            return iter->entry;
        }
    }
    return NULL;
}
  1. 迭代器释放
    可以看到安全迭代器在释放的时候对pause计数进行自减,非安全计数器则断言字典的状态是否改变
void dictReleaseIterator(dictIterator *iter)
{
    if (!(iter->index == -1 && iter->table == 0)) {
        if (iter->safe)
            dictResumeRehashing(iter->d);
        else
            assert(iter->fingerprint == dictFingerprint(iter->d));
    }
    zfree(iter);
}
  1. scan
    扫描每一个节点,在链表头和每个链表节点执行输入的动作。
  • d scan的字典
  • v 起始扫描的索引值
  • fn 加持在索引节点上的动作
  • bucketfn 加持在链表节点上的动作
  • provdata 配置buketfn的输入参数
  • 返回值,下一个scan的节点值

scan可以理解为一个安全的迭代器,每次扫描至少一个hash节点(包括整个链表),而上述迭代器则是每次遍历一个链表节点。
scan在遍历的同时,对索引节点和链表执行相应的输入函数。
字典中的每个节点都将为扫描到。

  • 当scan过程中字典大小发生变化了会怎么样?例如当前的索引值为b101,hash表掩码为b111, 那么当hash扩容变为b1111时, 新的索引值只可能时b0101,b1101,也就是说末尾的几个二进制是不变化的。
    那么为了应该字典大小发生变化的情况,索引值在自增时需要满足:1.字典大小不变时正常自增 2. 自增大小变化时,末尾2进制不变,高位自增。为此redis字典设计中引入了高位自增的操作。
  • 对于处在rehash状态的字典,总是先从较短的hash表中进行查找,之后在较长hash表中中查找,例如当前索引idx=b0101,较短hash表A掩码b111,较长hash表B掩码b11111,则在A中遍历A-hash[idx]后在B表中查找遍历idx=b01101,b10101,b11101

这个高位自增对于hash表长度没有发生改变的情况,例如v=b101,mask=b111,v经过这么操作后怎么变成了b11???

v |= ~m0;
 v = rev(v);
 v++;
 v = rev(v);
unsigned long dictScan(dict *d,
                       unsigned long v,
                       dictScanFunction *fn,
                       dictScanBucketFunction* bucketfn,
                       void *privdata)
{
    int htidx0, htidx1;
    const dictEntry *de, *next;
    unsigned long m0, m1;

    if (dictSize(d) == 0) return 0;

    /* This is needed in case the scan callback tries to do dictFind or alike. */
    dictPauseRehashing(d);

    if (!dictIsRehashing(d)) {
        htidx0 = 0;
        m0 = DICTHT_SIZE_MASK(d->ht_size_exp[htidx0]);

        /* Emit entries at cursor */
        if (bucketfn) bucketfn(d, &d->ht_table[htidx0][v & m0]);
        de = d->ht_table[htidx0][v & m0];
        while (de) {
            next = de->next;
            fn(privdata, de);
            de = next;
        }

        /* Set unmasked bits so incrementing the reversed cursor
         * operates on the masked bits */
        v |= ~m0;

        /* Increment the reverse cursor */
        v = rev(v);
        v++;
        v = rev(v);

    } else {
        htidx0 = 0;
        htidx1 = 1;

        /* Make sure t0 is the smaller and t1 is the bigger table */
        if (DICTHT_SIZE(d->ht_size_exp[htidx0]) > DICTHT_SIZE(d->ht_size_exp[htidx1])) {
            htidx0 = 1;
            htidx1 = 0;
        }

        m0 = DICTHT_SIZE_MASK(d->ht_size_exp[htidx0]);
        m1 = DICTHT_SIZE_MASK(d->ht_size_exp[htidx1]);

        /* Emit entries at cursor */
        if (bucketfn) bucketfn(d, &d->ht_table[htidx0][v & m0]);
        de = d->ht_table[htidx0][v & m0];
        while (de) {
            next = de->next;
            fn(privdata, de);
            de = next;
        }

        /* Iterate over indices in larger table that are the expansion
         * of the index pointed to by the cursor in the smaller table */
        do {
            /* Emit entries at cursor */
            if (bucketfn) bucketfn(d, &d->ht_table[htidx1][v & m1]);
            de = d->ht_table[htidx1][v & m1];
            while (de) {
                next = de->next;
                fn(privdata, de);
                de = next;
            }

            /* Increment the reverse cursor not covered by the smaller mask.*/
            v |= ~m1;
            v = rev(v);
            v++;
            v = rev(v);

            /* Continue while bits covered by mask difference is non-zero */
        } while (v & (m0 ^ m1));
    }

    dictResumeRehashing(d);

    return v;
}

总结

  • redis的字典使用hash表实现,hash[0]一般是正在使用的hash表,hash[1]用于rehash,处于rehash状态的字典查找时要在两个hash表中进行查找。
  • 字典采用拉链法解决hash冲突。对于rehash的阈值有两个高低阈值1,5.rehash过程采用渐进式rehash,增删改查时执行一步rehash,将hash[0]对应索引节点的整个链表链接到hash[1]新的索引下。
  • 安全迭代器及scan操作在迭代和销毁的时候对pauserehash进行增加和删除,非安全迭代器在迭代前后计算字典的状态值,不一致时assert
  • scan扫描时采用高位自增的方式计算索引值,目的是在rehash时hash表长度变化时,保持低位的2进制不变。(高位自增在掩码未变化时计算新索引值得情况有待搞清楚)
  • redis 字典的源码中还包含一些返回随机节点的api
// 1. 返回字典中的一个随机节点,第一次生成随机数定位在哪个hash表中的哪个索引值,第二次随机获取链表中的一个节点
dictEntry *dictGetRandomKey(dict *d);
// 2. 尽可能返回count个hash节点,存储在des指向的数组中
unsigned int dictGetSomeKeys(dict *d, dictEntry **des, unsigned int count);
// 3. 返回一个分布更好的节点,主要是减轻hash表长度不一的影响
dictEntry *dictGetFairRandomKey(dict *d)