Redis字典(dict)--含源码

字典,是一种保存键值对(key-value)的数据结构。字典的键是唯一的,程序可以通过键来对值进行修改,或者根据键来删除整个键值对。
字典作为一种常用的数据结构,被很多高级编程语言内置,如PHP的array,python的dict。但Redis由C语言实现,并没有内置这种数据结构,因此Redis构建了自己的字典结构。Redis键值对形式的数据库就是由字典实现的。
Redis底层是有哈希表实现的,一个哈希表中可以有多个哈希节点,每个哈希节点保存了一组字典中的键值对。

字典定义
哈希节点
typedef struct dictEntry {
    void *key;	// 键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;	// 值
    struct dictEntry *next;	// 指向下一个哈希节点,形成链表
} dictEntry;

key保存着键值对中的键,v保存着键值对中的值,其中值可以是一个指针,或者是一个uint64_t u64型或者int64_t s64型的整数或者double型的浮点数。
next指向另一个哈希节点,目的是将多个哈希值相同的键值对连接在一起,以此来解决哈希冲突(collision)问题。
在这里插入图片描述

哈希表
typedef struct dictht {
    dictEntry **table;	// 哈希节点数组
    unsigned long size;	// 哈希表大小
    unsigned long sizemask;	// 哈希表大小掩码,用于计算索引值。大小为size-1
    unsigned long used;	// 已有节点数量
} dictht;

table是一个数组,数组中每个元素都指向一个dictEntry
size记录了哈希表的大小,也是table数组的大小。
used记录哈希表目前已有节点数量。
sizemask的值总等于size-1,这个属性和哈希值一起决定了一个键应该被放在哪个table索引上。

下图展示一个哈希表,并且存储两个哈希值相同的key1key2的哈希节点
在这里插入图片描述

字典
typedef struct dict {
    dictType *type;	// 一些字典操作函数
    void *privdata;	// 私有数据
    dictht ht[2];	// 两个哈希表,一个用来存储数据,一个用来rehash
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

type是一个指向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;

privdata保存了需要传递给上述函数的可选参数。
ht包含两个数组,数组中的每个元素都是dictht哈希表,一般情况下,字典只使用ht[0]ht[1]只会在rehash时用到。
rehashidx记录rehash的进度。
iterators是Redis字典自己实现的迭代器,用于遍历,该变量用于记录迭代器的运行数量。
在这里插入图片描述

字典初始化
/* 重置初始化的哈希表 */
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]);	// 重置哈希表h[1]
    _dictReset(&d->ht[1]);	// 重置哈希表h[1]
    d->type = type;
    d->privdata = privDataPtr; 
    d->rehashidx = -1;	// rehash标志位
    d->iterators = 0;
    return DICT_OK;
}

上面dictCreate代码实现初始化字典,完成之后会得到一个dict的指针。然后就可以想字典中插入键值对。

字典插入键值对
// 使用字段设置的哈希函数,计算key的哈希值
#define dictHashKey(d, key) (d)->type->hashFunction(key)

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

    if (dictIsRehashing(d)) _dictRehashStep(d);	// 如果rehash在执行,则执行rehash

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)	// 获取当前key的hash值,如果存在直接返回NULL
        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];	// rehash正在执行,则插入到ht[1]中
    entry = zmalloc(sizeof(*entry));	// 分配内存空间
    entry->next = ht->table[index];		// 插入到index头部
    ht->table[index] = entry;
    ht->used++;	// 已有节点数加一

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);	// 设置key值
    return entry;
}

当要将一个新的键值对添加到字典中,程序需要先根据键值对的键值计算出哈希值和索引值,然后再根据索引值,将包含新键值对的节点放到哈希表数组的指定索引上面。

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

    /* Expand the hash table if needed */
    if (_dictExpandIfNeeded(d) == DICT_ERR)	// 在需要扩展dict
        return -1;
    for (table = 0; table <= 1; table++) {	// 在两个hash表中查找是否存在相同key
        idx = hash & d->ht[table].sizemask;	// 使用哈希表的sizemask属性和哈希值,计算出索引值
        /* Search if this slot does not already contain the given key */
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {	// 是否存在相同key的节点
                if (existing) *existing = he; 
                return -1;
            }
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;	// 如果没有rehash,直接break,不需要在操作ht[1]
    }
    return idx;
}

返回可以使用给定key的哈希条目填充的空闲槽的索引。如果key已存在,则返回-1并且可以填充可选的输出参数。函数主要完成3个工作:1、字典扩容(需要时进行);2、计算索引值;3、检查字典中是否存在相同的key
举例,如果现在有一个空的字典,我们需要将k0v0键值对添加到字典中程序首先执行:
hash = d->type->hashFunction(k0);
计算出哈希值。
假设哈希值为8,程序继续执行:
idx = hash & d->ht[0].sizemask = 8 & 3 = 0;
计算出k0的索引值为0,将键值对k0v0的节点放置到哈希表数组索引值为0的位置上,如下图:
在这里插入图片描述

何时需要扩展字典
/* This is the initial size of every hash table */
#define DICT_HT_INITIAL_SIZE     4

/* 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;

void dictEnableResize(void) {
    dict_can_resize = 1;
}

void dictDisableResize(void) {
    dict_can_resize = 0;
}

static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;	// 如果已经在执行rehash,直接返回

    /* 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);	// 哈希表大小为0,扩展为初始默认大小

    /* 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))	// 当userd大于size并且  字典处于可以rehash或者负载5时进行扩展
    {
        return dictExpand(d, d->ht[0].used*2);	// used*2
    }
    return DICT_OK;
}

DICT_HT_INITIAL_SIZE为每个哈希表的初始大小,
我们可以根据需要使用dictEnableResize()/ dictDisableResize(),对哈希表的大小是否可调整做enable/disable操作。

rehash

随着操作不断执行,哈希表保存的键值对会增加或者减少,为了让哈希表的负载因子(load_factor)保持在一个合理的范围。需要对哈希表进行扩展或者收缩。
扩展或者收缩有rehash(重新散列)来实现。rehash操作步骤如下:

  • 为字典的ht[1]哈希表分配空间,ht[1]分配空间的大小取决于所需要执行的操作,以及ht[0]当前所包含的键值对数量。
    • 如果是执行扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的 2 n 2^n 2n(2的n次幂)
    • 如果是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used 2 n 2^n 2n
  • 将保存的ht[0]中所有键值对rehash到ht[1]上面:重新计算哈希值和索引值,然后将键值对放置到ht[1]哈希表制定的位置。
  • ht[0]中所有的键值对都迁移到了ht[1]之后(此时ht[0]为空),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空的哈希表,为下一次rehash准备。

字典rehash代码:

int dictRehash(dict *d, int n) {
	// 最多读n*10个空,防止rehash长时间阻塞
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;	// 正在执行rehash,直接返回

    while(n-- && d->ht[0].used != 0) {	// n为需要rehash节点个数
        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);	// 保证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;	// 放置到ht[1]哈希表
            d->ht[0].used--;	// ht[0]哈希表拥有节点数减一
            d->ht[1].used++;	// ht[1]哈希表拥有节点数减一
            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) {	// 整个哈希表都已经被rehash
        zfree(d->ht[0].table);	// 释放ht[0]哈希节点数组
        d->ht[0] = d->ht[1];	// 将ht[1]置为hht[0]
        _dictReset(&d->ht[1]);	// 释放ht[1]
        d->rehashidx = -1;	// 重置rehash标志位
        return 0;
    }

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

扩展字典
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long 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)	// 扩展的size比已有的节点数量小时不被允许的
        return DICT_ERR;

    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(size);	// 根据size获取真实扩展的大小,第一个大于size的2的n次幂

    /* 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;
    d->rehashidx = 0;
    return DICT_OK;
}

/* 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 + 1LU;
    while(1) {
        if (i >= size)	// 返回第一个比size大的2的n次幂
            return i;
        i *= 2;
    }
}

当一下条件任一被满足时,程序会自动对字典进行扩展:
2. 服务器目前没有执行BGSAVE或者BGWRITEAOF命令,并且哈希的负载因子大于等于1。
3. 服务器目前正在执行BGSAVE或者BGWRITEAOF命令,并且哈希的负载印在大于等于5。

其中,负载因子公式为:
# 负载因子 = 哈希表以保存节点个数 / 哈希表大小
load_factor = ht[0].used / ht[0].size
根据BGSAVEBGWRITEAOF是都在执行,选择不同的负载因子,是因为在执行GBSAVE或者BGWRITEAOF过程中,Redis需要创建子进程,通常操作系统都采用写时复制(copy-on-write)来优化子进程的使用效率,在子进程存在期间,服务器提高执行扩展操作的负载因子,从而尽可能的避免在子进程期间对字典进行扩展。避免不必要的写入操作,节省内存。

字典收缩

当负载因子小于0.1时会对字典size进行收缩,收缩操作并不是在dict的底层函数定义调用的,而是由具体应用时决定、对应的htNeedsResize函数也是在server.c中。
src/server.c

/* Hash table parameters */
#define HASHTABLE_MIN_FILL        10      /* Minimal hash table fill 10% */

int htNeedsResize(dict *dict) {
    long long size, used;

    size = dictSlots(dict);
    used = dictSize(dict);
    return (size > DICT_HT_INITIAL_SIZE &&
            (used*100/size < HASHTABLE_MIN_FILL));	//负载因子小于0.1
}

/* If the percentage of used slots in the HT reaches HASHTABLE_MIN_FILL
 * we resize the hash table to save memory */
void tryResizeHashTables(int dbid) {
    if (htNeedsResize(server.db[dbid].dict))
        dictResize(server.db[dbid].dict);
    if (htNeedsResize(server.db[dbid].expires))
        dictResize(server.db[dbid].expires);
}
渐进式rehash

上面说到,哈希表的扩展与收缩是将ht[0]的键值对rehash到ht[1]上,但是这个动作不是一次性、集中性的完成的,而是分多次,渐进式完成。这是因为,一般情况下,哈希表中的数据量是比较多的,如果是一次性将数据从ht[0]转移到ht[1]上,庞大的计算量可能会使服务器在一段时间内停止服务。
渐进式rehash步骤如下:

  1. ht[1]分配空间,是字典同时具有ht[0]ht[1]两个哈希表。
  2. 在字典中维持一个rehashidx所以计数器变量,将它置为0,表示rehash开始。
  3. 在rehash期间,如果对字典进行添加、删除、修改和查找操作时,程序会将ht[0]哈希表上rehashidx索引上的键值对rehash到ht[1]上,rehash完成后,rehashidx自增一。
  4. ht[0]上的节点全部转移到ht[1]上,rehashidx设置为-1,表示rehash已经完成。

一下rehash的两种方式,都是渐进式rehash的体现:

  1. dictRehashMilliseconds:按照ms计时的rehash操作,是databasesCron中针对redis的DB进行rehash。databasesCron是redis的时间事件之一,每隔一段时间就会执行。
/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */
int dictRehashMilliseconds(dict *d, int ms) {
    long long start = timeInMilliseconds();
    int rehashes = 0;

    while(dictRehash(d,100)) {	// 每次rehash100个键值对
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}


  1. _dictRehashStep:一步的rehash操作,该函数在dict的增删改查操作中都会被调用。例如在上面dictAddRaw函数代码中if (dictIsRehashing(d)) _dictRehashStep(d);
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值