redis字典数据结构解析

数据结构

哈希表节点

// 字典中的键值对
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

可以发现, 键值对的值是一个union类型, 既可以表示一个指针, unit64_t, int64_t或者double数据, 都是8字节的数据. 同时存在一个指向哈希表节点的next变量, 以此来解决键的冲突的问题(拉链表解决).

哈希表结构

typedef struct dictht {
    dictEntry **table;        // 哈希表数组
    unsigned long size;       // 哈希表数组的大小
    unsigned long sizemask;   // 等于size - 1, 用于计算节点的索引值
    unsigned long used;       // 哈希表包含的节点个数
} dictht;

字典的数据结构

typedef struct dict {
    dictType *type; // 类型特定函数
    void *privdata;
    dictht ht[2];   // 保存两个哈希列表, 实现渐进式的重散列, ht[0]表示旧哈希列表, ht[1]表示新扩展或压缩新指定的哈希列表
    long rehashidx; // 冲散列索引, 表示当前已经进行到的索引, -1表示没有进行重散列
    int iterators;  // 正在使用的安全模式下iterator的个数, 只有在iterator=0时, 才会进行rehash 
} dict;

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;

由于使用数组存储哈希节点, 所以会存在数组空间不足, 需要动态扩充, 由于扩充时, 需要遍历所有节点重新计算节点的索引, 如果一次性完成, 需要消耗一定的时间, 回导致服务器在一段时间内不可用.为了避免重散列对服务器的性能造成影响, 分多次将ht[0]里的节点重散列到ht[1]中. 将重散列所需的工作军均摊到对字典的每个添加, 删除, 查找和更新的操作上, 避免了集中式重散列带来的庞大计算量.但是代码的开发难度会明显增大.

同时为了支持创建多态字典, dict数据结构中指定了dictType类型变量*type, 用于操作不同类型的键值对

迭代器的数据结构

typedef struct dictIterator {
    dict *d;                // 被迭代的字典
    long index;             // 迭代器当前所指向的哈希表索引位置
    int table;              // 正在被迭代的哈希表, ht[0]和ht[1]
    int safe;               // 标识迭代器是否处于安全模式
    dictEntry *entry;       // 当前迭代的节点指针
    dictEntry *nextEntry;   // 当前迭代节点的下一个节点指针
    long long fingerprint;  // 非安全模式下使用  
} dictIterator;

可以发现, 迭代器的数据结构中含有一个save字段, 就是为了解决渐进式重散列导致的不安全问题而引入的.

采用渐进式重散列, 迭代遍历时可能会存在的问题:

  • 指针entry的失效问题: 因为在遍历列表时, 可能会因为键值过期,而修改列表, 或者插入新的节点, 所以会存在指针安安全问题.
  • 重复遍历问题: 因为遍历整个字典时, 先遍历的哈希列表ht[0], 最后再遍历新的哈希列表ht[1], 如果在遍历ht[1]时, 发生了节点移动, ht[0] —> ht[1], 此时会存在一个节点遍历多次的问题.

安全模式

  1. 遍历过程中可以对字典进行查找和修改,因为查找和修改会触发过期判断,会删除内部元素。
  2. 迭代过程中不会出现重复元素.

非安全模式

  1. 遍历过程中字典是只读的,不能修改字典
  2. 可能会存在重复节点

相关操作

创建新字典

  • dictCreate
static void _dictReset(dictht *ht)
{
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}

/* Create a new hash table */
dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));

    _dictInit(d,type,privDataPtr);
    return d;
}

/* Initialize the hash table */
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;
}

很好理解, 指定类型处理参数, 然后申请dict内存空间, 最后设置字典的初始值. 可以发现,首次创建的字典中哈希列表的节点数为0.

将给定的键值对添加到字典

  • dictAdd
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key);

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}

dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;

    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;
}
  1. 是否正在进行rehash
    • 是: 节点移动
  2. 判断键值是否存在
    1. 判断是否需要扩容
      1. 正处于重散列, 直接返回
      2. 散列表为空, 散列表数组ht[1]大小扩容为4
  3. 计算键的hash值
  4. 遍历两个散列表
    1. 计算索引值
    2. 比较键的值(索引对应的桶中所有的节点)
    3. 如果存在相等的键, 返回-1
    4. 如果不处于重散列进程, 直接退出遍历循环
    5. 不存在相等的键值, 返回索引值
  5. 如果处于重散列进程, 新节点添加到散列表ht[1]

将给定的键值添加到字典里, 如果键值已经存在, 使用新值覆盖旧值

  • dictReplace
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. 直接调用dictAdd添加, 如果添加成功直接返回
  2. 查找键值为key的节点, 使用新值覆盖旧值

从字典中删除给定键对应的键值对

  • dictDelete
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 */
}

int dictDelete(dict *ht, const void *key) {
    return dictGenericDelete(ht,key,0);
}
  1. 若哈希表为空, 直接返回错误
  2. 如果哈希表处于重散列进程, 将ht[0]中的部分节点移动到ht[1]
  3. 计算键的哈希值及索引
  4. 比较键值
    1. 如果存在相等键值, 删除节点并返回DICT_OK
    2. 否则直接返回DICT_ERR

迭代器的实现

  1. 迭代器的初始化

// 默认使用非安全模式
dictIterator *dictGetIterator(dict *d)
{
    dictIterator *iter = zmalloc(sizeof(*iter));

    iter->d = d;              // 指定字典 
    iter->table = 0;          // 默认从ht[0]开始遍历
    iter->index = -1;         // 当前遍历的槽位置,初始化为-1
    iter->safe = 0;           // 默认使用非安全模式
    iter->entry = NULL;       // 迭代器当前指向的对象
    iter->nextEntry = NULL;   // 迭代器下一个指向的对象
    return iter;
}

// 使用安全模式
dictIterator *dictGetSafeIterator(dict *d) {
    dictIterator *i = dictGetIterator(d);
    i->safe = 1;
    return i;
}
  1. 迭代过程
dictEntry *dictNext(dictIterator *iter)
{
    while (1) {
        // 遍历新槽位下的链表 
        if (iter->entry == NULL) {
            dictht *ht = &iter->d->ht[iter->table];
            // 开始遍历ht[0]中的数据
            if (iter->index == -1 && iter->table == 0) {
                if (iter->safe)
                    // 给字典打安全标记,禁止字典进行rehash
                    iter->d->iterators++;
                else
                    // 记录迭代器指纹,就好比字典的md5值
                    // 如果遍历过程中字典有任何变动,指纹就会改变(释放iterator时检查指纹是否发生改变)
                    iter->fingerprint = dictFingerprint(iter->d);
            }
            iter->index++;
            // ht[0]遍历完之后, 遍历ht[1]中的节点
            if (iter->index >= (long) ht->size) {
                if (dictIsRehashing(iter->d) && iter->table == 0) {
                    iter->table++;
                    iter->index = 0;
                    ht = &iter->d->ht[1];
                } else {
                    break;
                }
            }
            iter->entry = ht->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 = iter->entry->next;
            return iter->entry;
        }
    }
    return NULL;  
}

重散列

在往字典插入新键值对的时候, 判断是否是需要动态扩容:

  1. 链表是初始化链表, ht[0].size == 0
  2. ht[0].used >= ht[0].size
  3. 装载因子超过目标因子

在添加,删除,更新的时候都会进行重散列操作(在没有安全模式迭代器时), 将部分ht[0]中的数据迁移到ht[1]中.

调用流程: _dictRehashStep --> dictRehash

每次最多重散列10个槽位, rehashidx表示槽位所在数组的索引位置.

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++;
    }

    /* Check if we already rehashed the whole table... */
    // 如果重散列完成, 将ht[1]赋值给ht[0],然后resetht[1]和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;
    }

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

参考链接

跋山涉水 —— 深入 Redis 字典遍历

你可能感兴趣的:(Redis)