redis dict字典的源码分析

       Redis的字典用哈希表作为底层实现,一个哈希表里可以有很多哈希表节点,而每个哈希表节点就保存了字典的一个键值对。

 一、字典的基本实现

1、哈希表节点

typedef struct dictEntry {
    void *key;//键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;//值
    struct dictEntry *next;//指向下一个节点,用于解决键冲突
} dictEntry;

2、哈希表

typedef struct dictht {
    dictEntry **table;//哈希表数组,每个元素都是指向dictEntry结构体指针
    unsigned long size;//哈希表的大小
    unsigned long sizemask;//哈希表大小掩码(等于size-1),用于计算索引值
    unsigned long used;//哈希表已有节点数量
} dictht;	

3、字典

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

二、字典的基本操作

dict *dictCreate(dictType *type, void *privDataPtr);//创建新的字典
int dictAdd(dict *d, void *key, void *val);//将给定键值对添加到字典
int dictReplace(dict *d, void *key, void *val);
//将给定键值对添加到字典,如果键已存在,则用新值替换旧值
void *dictFetchValue(dict *d, const void *key);//返回给定键的值
dictEntry *dictGetRandomKey(dict *d);//从字典中随机返回一个键值对
int dictDelete(dict *d, const void *key);//从字典中删除指定键值对
void dictRelease(dict *d);//释放给定字典及所有键值对

三、rehash

        为了让哈希表的负载因子维持在一个合理范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行扩展或者收缩。

      调整哈希表的工作通过rehash(重新散列)来完成,rehash步骤如下:

      1、为字典的ht[1]分配空间,空间大小取决于要执行的操作和ht[0]当前的包含的键值对数量(ht[0].used)

         1)扩展操作,ht[1].size为第一个大于等于ht[0].used*2的2的n次方;

         2)收缩操作,ht[1].size为第一个大于等于ht[0].used的2的n次方;

      2、将保存在ht[0]中的键值对rehash到ht[1]上面:重新计算键的哈希值和索引值,然后将键值对放到ht[1]哈希表的指定位置上。

      3、当ht[0]的所有键值对都迁移到ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新建空白哈希表。

      哈希表的收缩或者扩展

     1、 当满足以下任一条件,程序将对哈希表进行扩张:

         1)当服务器没有执行BGSAVE或者BGREWRITEAOF,并且负载因子大于1;

          2)当服务器在执行BGSAVE或者BGREWRITEAOF,并且负载因子大于5;

//判断是否要扩展字典空间
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))
    {/*dict_can_resize为1表示服务器不在执行BGSAVE或者BGREWRITEAOF
    dict_can_resize为0表示服务器正在执行BGSAVE或者BGREWRITEAOF	*/
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}//给ht[1]分配第一个大于等于size的2的n次方大小的空间
int dictExpand(dict *d, unsigned long size)
{
    dictht n; 
    unsigned long realsize = _dictNextPower(size);//获取第一个大于等于size的2的n次方的值;
    ……
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;
    ……
    //为ht[1]分配空间,启动rehash
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

     2、当负载因子小于0.1时,程序将对哈希表进行收缩(主要为了节省空间)。

//尝试收缩字典空间节省内存
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);
}
//判断字典释放需要收缩空间
int htNeedsResize(dict *dict) {
    long long size, used;
    size = dictSlots(dict);
    used = dictSize(dict);
    //空间大于DICT_HT_INITIAL_SIZE,负载因子小于0.1
    return (size > DICT_HT_INITIAL_SIZE &&
            (used*100/size < HASHTABLE_MIN_FILL));
}//重设字典的空间
int dictResize(dict *d)
{
    int minimal;
    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    minimal = d->ht[0].used;
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);
}

        渐进式rehash

      在redis中,rehash操作不是一次性完成的,而是分多次完成。这样做的目的主要是在redis中的dict可能会保存百万级、千万级甚至亿级别个键值对,如果一次性将所有的键值对全部rehash到ht[1],巨大的的运算量将会导致redis在期间停止对外服务。渐进式的rehash的实现关键是dict维持一个索引计数器rehashidx,并将它设为0,表示rehash开始。在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序会顺带从ht[0]上rehashidx索引开始遍历将第一个非空的哈希桶的所有的键值对rehash到ht[1],rehash完成后,rehashidx加一。

     在进行渐进式rehash过程中,dict同时使用ht[0]和ht[1]两个哈希表,所以字典的删除,查找,更新都会在两个哈希表上进行。新添加的键值对都是一律保存到ht[1]里面,以保证ht[0]的键值对数量只减不增;其余操作都是先在ht[0]上查找,找不到再到ht[1]查找进行操作。

//将d->ht[0]中从索引d->rehashid开始的n个非空哈希桶的键值对迁移到ht[1]
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;
        //从d->rehashidx开始遍历获取第一个非空哈希桶
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;//遍历了n*10个空桶,就停止迁移
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        //遍历哈希桶,将键值对重新哈希到ht[1]
        while(de) {
            unsigned int 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++;
    }
    //判断ht[0]上所有键值对是否迁移到ht[1]
    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;
}
static void _dictRehashStep(dict *d) {
    if (d->iterators == 0) dictRehash(d,1);
}

选几个函数,具体看了一下渐进式rehash的键值对迁移和期间字典操作实现

dictEntry *dictAddRaw(dict *d, void *key)
{
    ……//顺带迁移一个非空哈希桶上的所有键值对到ht[1]
    if (dictIsRehashing(d)) _dictRehashStep(d);
    ……//如果在rehash,直接保存到ht[1]
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];
    ht->table[index] = entry;
    ht->used++;
    ……
}
static int dictGenericDelete(dict *d, const void *key, int nofree)
{
    ……//顺带迁移一个非空哈希桶上的所有键值对到ht[1]
    if (dictIsRehashing(d)) _dictRehashStep(d);
    ……//先在ht[0]上查找,找不到再到ht[1]查找进行操作
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        ……//遍历he,删除指定的键值对
    }
    return DICT_ERR; /* not found */
}
dictEntry *dictFind(dict *d, const void *key)
{
    ……//顺带迁移一个非空哈希桶上的所有键值对到ht[1]
    if (dictIsRehashing(d)) _dictRehashStep(d);
    ……//先在ht[0]上查找,找不到再到ht[1]查找进行操作
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        ……//遍历he,查找指定键值对
    }
    return NULL;
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值