redis源码

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

2020-06-01  本文已影响0人  QaoKi

本文先介绍字典的初始化相关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]中

上一篇下一篇

猜你喜欢

热点阅读