美文网首页redis源码
redis源码5--字典Dict:初始化api 及 rehash

redis源码5--字典Dict:初始化api 及 rehash

作者: QaoKi | 来源:发表于2020-06-01 15:29 被阅读0次

    本文先介绍字典的初始化相关api,以及rehash相关的函数,并以向字典添加key,value为例,介绍rehash如何在其中运行

    和 rehash 相关的定义

    // 指示字典是否启用 rehash 的标识
    static int dict_can_resize = 1;
    // 强制 rehash 的比率
    static unsigned int dict_force_resize_ratio = 5;
    
    dictEnableResize

    手动开启自动 rehash

    void dictEnableResize(void) {
       dict_can_resize = 1;
    }
    
    dictDisableResize

    手动关闭自动 rehash

    void dictDisableResize(void) {
      dict_can_resize = 0;
    }
    

    需要注意的是,并非所有 rehash 都会被 dictDisableResize 阻止:
    如果已使用节点的数量和字典大小之间的比率,大于字典强制 rehash 比率 dict_force_resize_ratio ,那么 rehash 仍然会(强制)进行。

    静态函数

    //传入一个字典的指针,根据需要,初始化字典的哈希表,或者对字典现有的哈希表进行扩展
    static int _dictExpandIfNeeded(dict *ht)
    //计算第一个大于等于 size 的 2 的 N 次方,用作哈希表的大小
    static unsigned long _dictNextPower(unsigned long size);
    //传入字典的指针和键,返回可以将 key 插入到哈希表的索引位置,如果 key 已经存在于哈希表,那么返回 -1
    //如果字典正在进行 rehash ,那么总是返回 1 号哈希表的索引。因为在字典进行 rehash 时,新节点总是插入到 1 号哈希表。
    static int _dictKeyIndex(dict *ht, const void *key);
    //初始化字典
    static int _dictInit(dict *ht, dictType *type, void *privDataPtr);
    //在字典不存在安全迭代器的情况下,对字典进行单步 rehash 
    static void _dictRehashStep(dict *d)
    

    主要函数

    //传入一个哈希表指针,重置(或初始化)给定哈希表的各项属性值
    static void _dictReset(dictht *ht)
    //创建一个新的字典
    dict *dictCreate(dictType *type,void *privDataPtr)
    //缩小给定字典,让它的已用节点数和字典大小之间的比率接近 1:1
    int dictResize(dict *d)
    //创建一个新的哈希表,并根据字典的情况,选择以下其中一个动作来进行:
    //(1),如果字典的 0 号哈希表为空,那么将新哈希表设置为 0 号哈希表
    //(2),如果字典的 0 号哈希表非空,那么将新哈希表设置为 1 号哈希表,并打开字典的 rehash 标识,使得程序可以开始对字典进行 rehash
    int dictExpand(dict *d, unsigned long size)
    //执行 N 步渐进式 rehash。N是处理n个索引
    int dictRehash(dict *d, int n)
    //返回以毫秒为单位的 UNIX 时间戳
    long long timeInMilliseconds(void)
    //在给定毫秒数内,以 100 步为单位,对字典进行 rehash 。
    int dictRehashMilliseconds(dict *d, int ms)
    //尝试将给定键值对添加到字典中
    int dictAdd(dict *d, void *key, void *val)
    //尝试将键插入到字典中,不处理值,只将key插入字典中。并且条件允许会调用单步rehash
    dictEntry *dictAddRaw(dict *d, void *key)
    

    _dictReset

    传入一个哈希表指针,重置(或初始化)给定哈希表的各项属性值

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

    dictCreate

    创建一个新的字典

    dict *dictCreate(dictType *type,void *privDataPtr)
    {
      dict *d = zmalloc(sizeof(*d));
    
      //初始化字典,是一个静态函数
      _dictInit(d,type,privDataPtr);
    
      return d;
    }
    

    _dictInit

    初始化字典,是一个静态函数

     int _dictInit(dict *d, dictType *type, void *privDataPtr)
    {
      // 初始化两个哈希表的各项属性值
      // 但暂时还不分配内存给哈希表数组
      _dictReset(&d->ht[0]);
      _dictReset(&d->ht[1]);
    
      // 设置类型特定函数
      d->type = type;
    
      // 设置私有数据
      d->privdata = privDataPtr;
    
      // 设置哈希表 rehash 状态
      d->rehashidx = -1;
    
      // 设置字典的安全迭代器数量
      d->iterators = 0;
    
      return DICT_OK;
    }
    

    dictResize

    缩小给定字典,让它的已用节点数和字典大小之间的比率接近 1:1。
    并不是将ht[0]的size减小就完事了,因为ht[0].table是一个连续的数组,而数据是散列的排布在上面,直接将ht[0]的size减小,可能会丢失数据。比如ht[0]的size为10,used为2,数据放在ht[0].table[0]和 ht[0].table[9]上,这时候进行dictResize,如果只是减小ht[0]的size值,那么ht[0].table[9]的数据就丢失了,所以正确的做法是进行 rehash,将数据移到到size变小的ht[1]上面
    返回 DICT_ERR 表示字典已经在 rehash ,或者 dict_can_resize 为假
    成功创建体积更小的 ht[1] ,可以开始 resize 时,返回 DICT_OK。

    int dictResize(dict *d)
    {
      int minimal;
    
      // 不能在关闭 rehash 或者正在 rehash 的时候调用
      if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    
      // 计算让比率接近 1:1 所需要的最少节点数量
      minimal = d->ht[0].used;
      if (minimal < DICT_HT_INITIAL_SIZE)
          minimal = DICT_HT_INITIAL_SIZE;
    
      // 调整字典的大小
      // T = O(N)
      return dictExpand(d, minimal);
    }
    

    _dictExpandIfNeeded

    根据需要,初始化字典(的哈希表),或者对字典(的现有哈希表)进行扩展
    此函数,在查找给定的key在字典中下标位置的时候被调用
    T = O(N)

    static int _dictExpandIfNeeded(dict *d)
    {
      // 渐进式 rehash 已经在进行了,直接返回
      //在 dictExpand() 函数中也进行了判断,但是在dictExpand()中,如果 rehash 已经在进行,返回 DICT_ERR,和本函数返回值不同
      if (dictIsRehashing(d)) return DICT_OK;
    
      // 如果字典(的 0 号哈希表)为空,那么创建并返回初始化大小的 0 号哈希表
      // T = O(1)
      if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
    
      // 一下两个条件之一为真时,对字典进行扩展
      // 1)字典已使用节点数和字典大小之间的比率接近 1:1
      //    并且 dict_can_resize 为真
      // 2)已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio
      if (d->ht[0].used >= d->ht[0].size &&
          (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
      {
          // 新哈希表的大小至少是目前已使用节点数的两倍
          // T = O(N)
          return dictExpand(d, d->ht[0].used*2);
      }
    
      return DICT_OK;
    }
    

    dictExpand

    创建一个新的哈希表,并根据字典的情况,选择以下其中一个动作来进行:
    (1),如果字典的 0 号哈希表为空,那么将新哈希表设置为 0 号哈希表
    (2),如果字典的 0 号哈希表非空,那么将新哈希表设置为 1 号哈希表,并打开字典的 rehash 标识,使得程序可以开始对字典进行 rehash。因为0号哈希表非空,并且哈希表的size变了,不管是变大还是变小,都需要进行rehash,将ht[0]的数据移到ht[1]中

    size 参数不够大,或者 rehash 已经在进行时,返回 DICT_ERR 。成功创建 0 号哈希表,或者 1 号哈希表时,返回 DICT_OK 。

    int dictExpand(dict *d, unsigned long size)
    {
      // 新哈希表
      dictht n; /* the new hash table */
    
      // 根据 size 参数,计算新建哈希表的大小
      // T = O(1)
      unsigned long realsize = _dictNextPower(size);
    
      // 不能在字典正在 rehash 时进行。
      // size 的值也不能小于 0 号哈希表的当前已使用节点
      if (dictIsRehashing(d) || d->ht[0].used > size)
         return DICT_ERR;
    
      // 为哈希表分配空间,并将所有指针指向 NULL
      n.size = realsize;
      n.sizemask = realsize-1;
      // T = O(N)
      //table[i] 是一个指针,是dictEntry* 类型,所以为table数组分配内存的时候,大小是 realsize*sizeof(dictEntry*)
      n.table = zcalloc(realsize*sizeof(dictEntry*));
      n.used = 0;
    
      // 如果 0 号哈希表为空,那么这是一次初始化:
      // 程序将新哈希表赋给 0 号哈希表的指针,然后字典就可以开始处理键值对了。
      if (d->ht[0].table == NULL) {
          d->ht[0] = n;
          return DICT_OK;
      }
    
      // 如果 0 号哈希表非空,那么这是一次 rehash :
      // 程序将新哈希表设置为 1 号哈希表,
      // 并将字典的 rehash 标识打开,让程序可以开始对字典进行 rehash
      d->ht[1] = n;
      d->rehashidx = 0;
      return DICT_OK;
    
      /* 顺带一提,上面的代码可以重构成以下形式:
    
      if (d->ht[0].table == NULL) {
          // 初始化
          d->ht[0] = n;
      } else {
          // rehash 
          d->ht[1] = n;
          d->rehashidx = 0;
      }
      return DICT_OK;
      */
    }
    

    _dictNextPower

    计算第一个大于等于 size 的 2 的 N 次方,用作哈希表的值

    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)
            return i;
        i *= 2;
      }
    }
    

    dictRehash

    执行 N 步渐进式 rehash。N是处理n个索引
    返回 1 表示仍有键需要从 0 号哈希表移动到 1 号哈希表,返回 0 则表示所有键都已经迁移完毕。
    注意,每步 rehash 都是以一个哈希表索引(桶)作为单位的,一个桶里可能会有多个节点,被 rehash 的桶里的所有节点都会被移动到新哈希表。
    rehash完成的标志是 ht[0].used = 0

    int dictRehash(dict *d, int n) {
    
      // 只可以在 rehash 进行中时执行
      if (!dictIsRehashing(d)) return 0;
    
      // 进行 N 步迁移
      // T = O(N)
      while(n--) {
        dictEntry *de, *nextde;
    
        // 如果 0 号哈希表为空,那么表示 rehash 执行完毕
        // T = O(1)
        if (d->ht[0].used == 0) {
            // 释放 0 号哈希表
            zfree(d->ht[0].table);
            // 将原来的 1 号哈希表设置为新的 0 号哈希表
            d->ht[0] = d->ht[1];
            // 重置旧的 1 号哈希表
            //此函数只是让哈希表的table指向Null,并没有释放内存
            _dictReset(&d->ht[1]);
            // 关闭 rehash 标识
            d->rehashidx = -1;
            // 返回 0 ,向调用者表示 rehash 已经完成
            return 0;
        }
    
        // 确保 rehashidx 没有越界
        //当前要处理的是 ht[0].table[rehashidx],ht[0].size是table数组的大小,所以rehashidx应始终小于 ht[0].size
        assert(d->ht[0].size > (unsigned)d->rehashidx);
    
        // 略过数组中为空的索引,找到下一个非空索引
        //不可能全都为空,因为ht[0].used 大于0
        while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
    
        // 指向该索引的链表表头节点
        de = d->ht[0].table[d->rehashidx];
    
        // 将链表中的所有节点迁移到新哈希表
        // T = O(1)
        while(de) {
            unsigned int h;
    
            // 保存下个节点的指针
            nextde = de->next;
    
            // 计算新哈希表的哈希值,以及节点插入的索引位置
            //dictHashKey 是 宏定义函数,调用字典中的hash函数
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
    
            // 插入节点到新哈希表
            //d->ht[1].table[h] 是 dictEntry * 类型,指向头结点
            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;
        // 更新 rehash 索引
        d->rehashidx++;
      }
    
      return 1;
    }
    

    timeInMilliseconds

    返回以毫秒为单位的 UNIX 时间戳

    long long timeInMilliseconds(void) {
      struct timeval tv;
    
      gettimeofday(&tv,NULL);
      return (((long long)tv.tv_sec)*1000)+(tv.tv_usec/1000);
    }
    

    dictRehashMilliseconds

    在给定毫秒数内,以 100 步为单位,对字典进行 rehash 。

    int dictRehashMilliseconds(dict *d, int ms) {
      // 记录开始时间
      long long start = timeInMilliseconds();
      int rehashes = 0;
    
      //执行100 步 rehash
      while(dictRehash(d,100)) {
        rehashes += 100;
        // 如果时间已过,跳出
        if (timeInMilliseconds()-start > ms) break;
      }
    
      return rehashes;
    }
    

    _dictRehashStep

    在字典不存在安全迭代器的情况下,对字典进行单步 rehash 。字典有安全迭代器的情况下不能进行 rehash ,因为两种不同的迭代和修改操作可能会弄乱字典。
    这个函数被多个通用的查找、更新操作调用,它可以让字典在被使用的同时进行单步rehash ,将rehash的时间分散,减轻redis压力

    static void _dictRehashStep(dict *d) {
      if (d->iterators == 0) dictRehash(d,1);
    }
    

    dictAdd

    尝试将给定键值对添加到字典中,只有给定键 key 不存在于字典时,添加操作才会成功
    最坏 T = O(N) ,平摊 O(1) 。时间消耗在内部调用dictAddRaw时,会再调用_dictKeyIndex(),查找key可以插入的索引位置,_dictKeyIndex()会再调用_dictExpandIfNeeded(),如果发现字典空间不够了,会再调用dictExpand()进行扩展的话,时间复杂度会加大。但平摊下来时间复杂度还是O(1)
    接受的参数为一个字典类型的指针,一个key和一个val

    int dictAdd(dict *d, void *key, void *val)
    {
      // 尝试添加键到字典,并返回包含了这个键的新哈希节点
      // T = O(N)
      dictEntry *entry = dictAddRaw(d,key);
    
      // 键已存在,添加失败
      if (!entry) return DICT_ERR;
    
      // 键不存在,设置节点的值
      //这是宏定义函数,调用了字典的dictType 类型函数 
      // T = O(1)
      dictSetVal(d, entry, val);
    
      // 添加成功
      return DICT_OK;
    }
    

    dictAddRaw

    尝试将键插入到字典中,接受的参数为一个字典类型的指针,一个key。只是将key插入字典中,并不处理值
    如果键已经在字典存在,那么返回 NULL
    如果键不存在,那么程序创建新的哈希节点,将节点和键关联,并插入到字典,然后返回节点本身。让接收返回值的函数处理该节点值

    dictEntry *dictAddRaw(dict *d, void *key)
    {
      int index;
      dictEntry *entry;
      dictht *ht;
    
      // 如果条件允许的话,进行单步 rehash
      // T = O(1)
      if (dictIsRehashing(d)) _dictRehashStep(d);
    
      // 计算键在哈希表中的索引值
      // 如果值为 -1 ,那么表示键已经存在
      // T = O(N)
      if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;
    
      // T = O(1)
      // 如果字典正在 rehash ,那么将新键添加到 1 号哈希表
      // 否则,将新键添加到 0 号哈希表
      ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
      // 为新节点分配空间
      entry = zmalloc(sizeof(*entry));
      // 将新节点插入到链表表头
      entry->next = ht->table[index];
      ht->table[index] = entry;
      // 更新哈希表已使用节点数量
      ht->used++;
    
      // 设置新节点的键
      // T = O(1)
      dictSetKey(d, entry, key);
    
      return entry;
    }
    

    _dictKeyIndex

    静态函数。返回可以将 key 插入到哈希表的索引位置
    如果 key 已经存在于哈希表,那么返回 -1
    注意,如果字典正在进行 rehash ,那么总是返回 1 号哈希表的索引。 因为在字典进行 rehash 时,新节点总是插入到 1 号哈希表。
    T = O(N)

    static int _dictKeyIndex(dict *d, const void *key)
    {
      unsigned int h, idx, table;
      dictEntry *he;
      
      //如果有需要,对字典进行扩展
      // T = O(N)
      if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;
    
      // 计算 key 的哈希值
      h = dictHashKey(d, key);
      // T = O(1)
      for (table = 0; table <= 1; table++) {
    
        // 计算索引值
        idx = h & d->ht[table].sizemask;
    
        // 查找 key 是否存在
        // T = O(1)
        he = d->ht[table].table[idx];
        while(he) {
            if (dictCompareKeys(d, key, he->key))
                return -1;
            he = he->next;
        }
    
        // 如果运行到这里时,说明 0 号哈希表中所有节点都不包含 key
        // 如果这时 rehahs 正在进行,那么继续对 1 号哈希表进行 rehash
        if (!dictIsRehashing(d)) break;
      }
    
      // 返回索引值
      return idx;
    }
    

    总结

    在调用dictCreate() 创建一个字典时,并没有立刻给字典中两个哈希表中的table数组分配内存,而是让size为0,而当调用dictAdd()向字典中添加元素时,如果发现ht[0].size为0,此时为ht[0].table分配内存,并将数据插入
    在调用dictAdd()时,会调用 _dictKeyIndex() 来查找给定的key插入到哈希表时索引的位置,在_dictKeyIndex中,会调用 _dictExpandIfNeeded(),发现空间不够时,会对字典进行扩展,调用rehash,将数据从ht[0]移动到ht[1]
    当rehash进行时,当对字典进行插入、删除、查找时,会进行安全的单步rehash,分散压力。并且,当rehash进行过程中,插入数据只会插入到ht[1]中

    相关文章

      网友评论

        本文标题:redis源码5--字典Dict:初始化api 及 rehash

        本文链接:https://www.haomeiwen.com/subject/rxetahtx.html