redis学习笔记(3)---字典dict

字典dict

  redis中的字典,即hash表,其实现与Java中的HashMap基本类似。同样是基于数组和链表的,通过“拉链法”来处理碰撞。
  字典中的每一个key都是独一无二的,当要加入一个key-value对时,如果key在字典中已经存在,则会直接返回,而不会重新将其加入到字典中。

字典的实现  

hash节点的定义如下: 

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
  • key:元素的key
  • v:元素的值
  • next:处理碰撞,所有分配到同一索引的元素通过next指针链接起来形成链表
  • key和v都可以保存多种类型的数据

hash表的定义如下:  

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
  • table:一个二级指针,真正存储数据的地方。可以将table看做一个指向数组的指针,而数组就是hash表最基本的结构。通过数组和hash节点中的next指针形成完整的hash表。
  • size:数组的大小,通常为2的整数次方
  • sizemask:hash表大小掩码,用于计算索引,当size非0时为(size-1)
  • used:hash表中已有节点的数量

处理函数  

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;

  redis中通过dictType这样的一个结构用来存储针对不同类型的键值对的处理函数,这样对于不同类型的键值对,就可以有不同的处理了。即通过函数指针实现多态。

字典定义如下:   

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;
  • type:处理函数表,通过其中保存的函数指针对不同类型的数据进行不同的处理,实现多态
  • privdata:私有数据
  • ht[2]:hash表,可以发现一个字典中有两个hash表
  • rehashidx:ht[0]中正在rehash的桶的索引,当rehash=-1时,表明此时没有在进行rehash操作
  • iterator:正在运行的迭代器的数量

示意图 

  一个字典的示意图如下:
  这里写图片描述

rehash  

  hash表利用负载因子loadfactor = used/size来表明hash表当前的存储情况。
  当负载因子过大时操作的时间复杂度增大,负载因子过小时说明hash表的填充率很低,浪费内存。redis中的数据都是存储在内存中的,因此我们必须尽量的节省内存。
  因此我们必须将loadfactor控制在一定的范围内,同时保证操作的时间复杂度接近O(1)和内存尽量被占用。即rehash操作分为扩展和收缩两种情况。
 
  dict中有两个hash表,ht[0]和ht[1]。所有的数据都是存在放dict的ht[0]中,ht[1]只在rehash的时候使用。dict进行rehash的时候,将ht[0]中的所有数据rehash到ht[1]中。
  dict的rehash并不是一次性完成的,而是分多次、渐进式的完成的
  每一步的大小分为两种:
  1)在一步中将ht[0]的table中的一个元素,也就是一个哈希桶所对应的链表中的所有元素进行rehash。(在调用dict的一些操作函数时,如add,find等时进行)
  2)在一步中执行一段固定的时间,当时间到达后,暂停rehash。 (数据库周期性执行databasesCron()时进行)
  这两种方法对应的函数分别是_dictRehashStep和dictRehashMilliseconds。

  rehash过程的基本步骤如下:
  1) 首先调用dictExpand为ht[1]分配空间  

static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE; 

    if (size >= LONG_MAX) return LONG_MAX;
    while(1) {
        if (i >= size) //size为第一个大于等于size的2的幂次
            return i;
        i *= 2;
    }
}
int dictExpand(dict *d, unsigned long size)
{
    dictht n; 
    unsigned long realsize = _dictNextPower(size); //计算ht[1]所需大小

    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;
    if (realsize == d->ht[0].size) return DICT_ERR;

    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    if (d->ht[0].table == NULL) { //表明为第一次加入元素时创建hash表
        d->ht[0] = n; //直接将新创建的hash表赋给ht[0]
        return DICT_OK;
    }

    d->ht[1] = n;  //否则为rehash,将新创建的hash表赋给ht[1]
    d->rehashidx = 0;//表明正在处于rehash过程中
    return DICT_OK;
}

  2)调用dictRehash逐步完成rehash操作

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; //每次rehash最多可访问的空桶的个数
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
         //rehashidx用于指示正在对ht[0]中的第几个桶进行rehash操作,确保不会越界
        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]; //ht[0]中正在rehash的桶元素的头节点
        while(de) {
            unsigned int h;

            nextde = de->next;
            h = dictHashKey(d, de->key) & d->ht[1].sizemask; //计算ht[0]中元素进行rehash后在ht[1]中的索引
            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]剩余元素个数为0,表明ht[0]中的元素已经全部rehash到ht[1]中,因此rehash过程已经完成。
    if (d->ht[0].used == 0) { 
        //可以释放ht[0],并将ht[1]赋给ht[0]后重置ht[1]
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1; //表明rehash已经结束
        return 0;
    }

    return 1; //否则还处于rehash过程中
}

  将ht[0]中的所有元素全部转移到ht[1]中,释放原来的ht[0],将ht[1]赋给ht[0],并重置ht[1]为下次rehash做准备  
  

创建一个字典: 

dict *dictCreate(dictType *type,void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d)); //分配内存

    _dictInit(d,type,privDataPtr); //字典初始化
    return d;
} 
int _dictInit(dict *d, dictType *type,void *privDataPtr)
{
    _dictReset(&d->ht[0]); //初始化两个hash表
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;
    d->iterators = 0;
    return DICT_OK;
}
static void _dictReset(dictht *ht)
{  
    //hash表的初始化
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}

向字典中加入一个元素: 

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

    if (!entry) return DICT_ERR; //表明key已存在
    dictSetVal(d, entry, val);
    return DICT_OK;
}
dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;

    if (dictIsRehashing(d)) _dictRehashStep(d); //当正在rehash时,先进行rehash操作

    if ((index = _dictKeyIndex(d, key)) == -1)  ///计算key在hash表中的桶的索引,-1时表示key已存在
        return NULL;

    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];  //当正在rehash时,则加入ht[1]中,否则加入到ht[0]
    entry = zmalloc(sizeof(*entry));
    entry->next = ht->table[index];  //将新元素加入到桶中链表的头节点
    ht->table[index] = entry;
    ht->used++;

    dictSetKey(d, entry, key);
    return entry;
}

  可以发现,在一个字典中,每一个key都是独一无二的。当加入一个key-value对时,如果字典中已存在该key,则会直接返回而不会继续执行加入操作,既不会执行dictSetKey(),也不会执行dictSetVal()。


本文所引用的源码全部来自Redis3.0.7版本

redis学习参考资料:
https://github.com/huangz1990/redis-3.0-annotated
Redis 设计与实现(第二版)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值