Redis数据结构之字典

字典经常作为一种数据结构内置在很多高级编程语言中,但是Redis使用C语言实现,没有内置这种数据结构,因此Redis自己构建了字典的实现。

Redis数据库就是使用字典的数据结构来作为底层实现。另外Redis的哈希键对象也是使用了字典的数据结构。

字典的实现

Redis的字典使用了哈希表 作为底层实现,其中一个哈希表包含了多个哈希节点 ,而每个哈希节点又包含多个键值对

字典

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

哈希表

cpp 复制代码
/* 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; // 哈希表的数组,数组中每个元素都是指针,指向 dictEntry 结构
    unsigned long size; // 哈希表的大小,table 数组的大小
    unsigned long sizemask; // 哈希表掩码,用于计算索引值,等于 size-1
    unsigned long used; // 哈希表已有的节点(键值对)数量
} dictht;

哈希节点

cpp 复制代码
typedef struct dictEntry {
    void *key;  // 键 key
    union { // 值 val
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; // 指向下一个哈希表节点,链表法解决hash冲突
} dictEntry;

哈希节点维护一个链表用于解决哈希冲突。

哈希冲突

什么是哈希冲突

哈希的过程就是给定一个输入,然后经过指定的哈希函数,计算出一个输出。输入的值范围大于输出的值范围,所以一定存在某多个输入,经过哈希函数计算的输出是相等的。那么这些不同的输入就是发生了哈希碰撞。

如何解决哈希冲突

常见的哈希冲突解决办法有链地址法、开放寻址法、再哈希法等。redis哈希表使用链地址法解决冲突的问题。

Redis哈希表哈希节点维护一个next指针,将冲突的哈希节点通过单链表的方式串起来。

cpp 复制代码
  if (d->type->no_value) {
  		/* Allocate an entry without value. */
        entry = createEntryNoValue(key, *bucket);
    } else { // 如果哈希节点已经存在元素,则将新的节点添加到哈希节点链表的头部,已冲突的节点链接到新节点的next处
        entry->key = key;
        entry->next = *bucket;
    }
    *bucket = entry;
    d->ht_used[htidx]++

字典扩容

当哈希表存储节点达到一定数量后,会对字典进行扩容,以提升字典效率。

扩容时机

字典的扩容时机主要关注两个指标。

  • 当前can_resize标识
  • 负载因子
    首先查看判定是否需要扩容的源码段。
cpp 复制代码
    if ((dict_can_resize == DICT_RESIZE_ENABLE &&
         d->ht_used[0] >= DICTHT_SIZE(d->ht_size_exp[0])) ||
        (dict_can_resize != DICT_RESIZE_FORBID &&
         d->ht_used[0] / DICTHT_SIZE(d->ht_size_exp[0]) > dict_force_resize_ratio))
    {
        return dictExpand(d, d->ht_used[0] + 1);
    }

当在两种场景下,会对字典进行翻倍的扩容。

  • 当前resize为DICT_RESIZE_ENABLE,且负载因子大于1
  • 当前resize为非DICT_RESIZE_FORBID(resize为DICT_RESIZE_ENABLE),且负载因子大于5

can_resize标识

can_resize有三个枚举

cpp 复制代码
typedef enum {
    DICT_RESIZE_ENABLE,
    DICT_RESIZE_AVOID,
    DICT_RESIZE_FORBID,
} dictResizeEnable;

redis会根据当前进程情况对resize进行更新

cpp 复制代码
/* This function is called once a background process of some kind terminates,
 - as we want to avoid resizing the hash tables when there is a child in order
 - to play well with copy-on-write (otherwise when a resize happens lots of
 - memory pages are copied). The goal of this function is to update the ability
 - for dict.c to resize or rehash the tables accordingly to the fact we have an
 - active fork child running. */
void updateDictResizePolicy(void) {
    if (server.in_fork_child != CHILD_TYPE_NONE)
        dictSetResizeEnabled(DICT_RESIZE_FORBID);
    else if (hasActiveChildProcess())
        dictSetResizeEnabled(DICT_RESIZE_AVOID);
    else
        dictSetResizeEnabled(DICT_RESIZE_ENABLE);
}
  • 默认resize=DICT_RESIZE_ENABLE
  • 当前处于子进程中resize=DICT_RESIZE_FORBID
  • 存在子进程时,resize=DICT_RESIZE_AVOID

负载因子

负载因子 = d->ht_used[0] / DICTHT_SIZE(d->ht_size_exp[0])

  • 当负载因子大于等于 1 ,且 Redis 没有在执行 bgsave 命令、 bgrewiteaof 命令, RDB 快照 或 AOF 重写 时,进行 rehash 操作
  • 当负载因子大于等于 5 时,不管有没有有在执行 RDB 快照 或AOF 重写,都会强制进行 rehash 操作

渐进式rehash

redis在进行rehash时,会将ht[0]中的全部键rehash到ht[1]中。但是redis在rehash动作并不是一次性的完成的。而是通过分治的思想,分多次、渐进式的完成的。

因为redis是单线程模型,如果一个大key在进行一次性rehash时,会有较大的计算量可能导致服务器在一段时间内停止服务,从而引起客户端长时间阻塞不可用。

cpp 复制代码
/* 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[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) {
            uint64_t 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... */
    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;
}
  • 存在rehash的过程中,不允许再次发起rehash
  • 每次最大遍历10个桶,防止遍历过多空桶,导致阻塞线程
  • 每次将键值从ht[0]迁移到ht[1]后,d->ht[0].used-- d->ht[1].used++ d->rehashidx++;
  • rehash完毕后,rehashidx置为-1

哈希键操作

新增

cpp 复制代码
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long 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, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry.
     * Insert the element in top, with the assumption that in a database
     * system it is more likely that recently added entries are accessed
     * more frequently. */
    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;
}
  • 如果在rehash进行中,首先执行一步rehash
  • 如果key已经存在,不执行set操作
  • 新增时只写入ht[1],保证ht[0]的元素只会越来越少,不会新增。

更新

cpp 复制代码
/* Add or Overwrite:
 * Add an element, discarding the old value 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, *existing, auxentry;

    /* Try to add the element. If the key
     * does not exists dictAdd will succeed. */
    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. */
    auxentry = *existing;
    dictSetVal(d, existing, val);
    dictFreeVal(d, &auxentry);
    return 0;
}

读取

cpp 复制代码
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; /* dict is empty */
    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 || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}
  • 如果在rehash进行中,首先执行一步rehash
  • 如果当前在reahsh,遍历ht[0]和ht[1],否则只遍历ht[0]
  • 通过哈希函数找到对应的哈希桶,对桶内的元素进行键值对比,如果键值不相等,会进行对next后续键再次对比

删除

cpp 复制代码
/* Search and remove an element. This is an helper function for
 * dictDelete() and dictUnlink(), please check the top comment
 * of those functions. */
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 || 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 he;
            }
            prevHe = he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL; /* not found */
}
  • 如果在rehash进行中,首先执行一步rehash
  • 如果当前在reahsh,遍历ht[0]和ht[1],否则只遍历ht[0]
  • 通过判断key值,如果是需要删除的key,则会调整哈希节点的next指针,达到删除效果
  • 更新哈希表元素占用指标used参数
相关推荐
蹉跎x29 分钟前
力扣1358. 包含所有三种字符的子字符串数目
数据结构·算法·leetcode·职场和发展
坊钰1 小时前
【Java 数据结构】移除链表元素
java·开发语言·数据结构·学习·链表
计算机毕设定制辅导-无忧学长1 小时前
Redis 初相识:开启缓存世界大门
数据库·redis·缓存
Rverdoser2 小时前
redis延迟队列
数据库·redis·缓存
阿七想学习2 小时前
数据结构《排序》
java·数据结构·学习·算法·排序算法
weisian1512 小时前
Redis篇--常见问题篇6--缓存一致性1(Mysql和Redis缓存一致,更新数据库删除缓存策略)
数据库·redis·缓存
越甲八千3 小时前
总结一下数据结构 树 的种类
数据结构
eternal__day3 小时前
数据结构(哈希表(中)纯概念版)
java·数据结构·算法·哈希算法·推荐算法
OTWOL3 小时前
两道数组有关的OJ练习题
c语言·开发语言·数据结构·c++·算法
不惑_3 小时前
List 集合安全操作指南:避免 ConcurrentModificationException 与提升性能
数据结构·安全·list