redis 源码系列(1):船新的渐进式 hash 表 --- dict

这个系列将会将整个 redis 的源码读一遍。希望可以帮助大家弄懂 redis。前面会从 redis 底层最为重要的一些数据结构作为突破口。这些相关模块的代码高内聚,依赖很少,容易读懂。而且对于不想深入了解 redis 工作机制的同学也有帮助。

今天我们来分析一下 redis 中 dict (哈希表)的实现。 redis 的代码组织相当平坦,除了第三方库以外,所有代码均在 src 目录下。今天涉及到的文件有 src/dict.h src/dict.c

redis dict

首先来看几个重要的结构体定义

// hash table entry
typedef struct dictEntry {
	void *key;
  union {
    void *val;
    uint64_t u64;
    int64_t s64;
  } v;
  // redis 使用拉链法解决 hash 冲突,next 用来构成单向链表 
  struct dictEntry *next;
} dictEntry;

// redis 通过函数指针实现多态,通过抽象出 dictType,可以让我们“定制”自己的 hashtable
typedef struct dictType {
  unsigned int (*hashFunction)(const void *key);
  void *(*keyDup)(void *privdata, const void *key);
  void *(*valDup)(void *privdata, const void *val);
  void *(*keyCompare)(void *privdata, const void *k1, const void *k2);
  void *(*keyDestructor)(void *privdata, const void *key);
  void *(*valDestructor)(void *privdata, const void *val);
} dictType;

typedef struct hashht {
  // buckets
	dictEntry **table;
  // bucket number
  unsigned long size;
  // redis 通过将 bucket num 设置为 2 的幂次
  // 来使用 & 操作替代 %,sizemask == size - 1
  unsigned long sizemask;
  // hash table 内的元素数量
  unsigned long used;
} hashht;

typedef struct dict {
  // 类型特定函数,作用相当于面向对象编程中的 virtual method
  dictType *type;
  // 类型特定的私有数据
  void *privdata;
  // 两个 hashtable,实现 incremental rehashing 的关键
  dictht ht[2];
  // 如果在进行 rehash 的话,rehashindex 为将要迁移的 bucket index
  int rehashindex;
  // dict **正在运行** 的安全迭代器的数量,
  int iterators;
} dict;

下面我们来看看几个主要接口:

dictCreate

dict *dictCreate(dictType *type, void *privdata) {
  dict *d = zmalloc(sizeof(*d));
  // _dictInit 主要就是对 dict 对象赋值,这里不再展开
  _dictInit(d, type, privdata);
  return d;
}

Dict 的构造接口,主要是将其成员进行合适的初始化

dictResize

一旦 hash table 的 load factor(表内的元素数量和 bucket 数量之比)大于1,往往就意味着 hash table 的 hash 冲突开始增加,性能开始下降。所以 resize 必不可少。来看下 redis 中是怎么做的

int dictResize(dict *d) {
  int minimal;
	// rehash flag 被设为关闭、或者该 dict 正在进行 rehash 的话返回错误
  if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
  minimal = d->ht[0].used;
  // 保证 load factor <= 1
  if (minimal < DICT_HT_INITIAL_SIZE) minimal = DICT_HT_INITIAL_SIZE;
  return dictExpand(d, minimal);
}

// 初始化 dict 或者让一个 dict 开始 rehash
int dictExpand(dict *d, unsigned long size) {
  dictht n;
  // bucket 数必须是 2 的幂次
  unsigned long realsize = _dictNextPower(size);
  // 不能重复 rehash,load factor 不能大于1
  if (dictIsRehashing(d) || d->ht[0].used > realsize) return DICT_ERR;
  // 构造新的 hashtable 用于初始化或者 rehash
  n.size = realsize;
  n.sizemask = realsize - 1;
  n.table = zmalloc(realsize*sizeof(dictEntry));
  n.used = 0;
  
  // 一次初始化操作
  if (d->ht[0].table == NULL) {
    d->ht[0] = n;
    return DICT_OK;
  }
  
  // rehash,在后面的 rehash 例程中,会将 dict 的数据从 ht[0] 
  // 搬运到 ht[1] 内
  d->ht[1] = n;
  // rehash 处理的 bucket index
  d->rehashindex = 0;
  return DICT_OK;
}

常见的 hashtable 其 resize 和搬运数据往往是在一个函数内部完成的,也就是说这个过程一般在单线程中可以认为是原子的。但是 redis 把 resize 和 rehash 中数据搬运分开,并且提供按照步数(bucket number)和时间来 rehash:

/*
	执行 incremental rehashing
	n 代表的是要搬运几个 bucket 的数据,而不是 entry
	返回 0 表示 rehash 已经完成,1 表示还有数据需要搬运
*/
int dictRehash(dict *d, int n) {
  if (!dictIsRehashing(d)) return 0;
  while (n--) {
    dictEntry *de, *nextde;
    // rehash 完成,所有 ht[0] 的数据均已经转移到 ht[1]
    if (d->ht[0].used == 0) {
      // 释放 ht[0] 原来的内存,将 dict 恢复成 ht[0] 装数据,而
      // ht[1] 是一个不可用的 hashht 的状态
      zfree(d->ht[0].table);
      d->ht[0] = d->ht[1];
      _dictReset(&d->ht[1]);
      d->rehashindex = -1;
    }
    assert(d->ht[0].size > (unsigned)d->rehashindex);
    // 跳过 ht[0] 内的所有空 bucket
    while (d->ht[0].table[d->rehashindex] == NULL) d->rehashindex++;
    
    de = d->ht[0].table[d->rehashindex];
    // 搬运 bucket 数据到 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;
    }
    // 将已经搬运的 bucket 指针置为 NULL
    d->ht[0].table[d->rehashindex++] = NULL;
  }
  return 1;
}

/*
	按照时间进行 incremental rehahsing,计时粒度是 100 个 bucket 迁移时间
*/
int dictRehashMilliseconds(dict *d, int ms) {
  long long start = timeInMilliseconds();
  int rehashes = 0;
  while (dictRehash(d, 100)) {
    rehashes += 100;
    if (timeInMilliseconds()->start > ms) break;
  }
  return rehashes;
}

可以看到,我们的 dict 在 rehash 过程中,会存在两个 hashtable 同时存有数据的情况,这种情况必然导致了我们对 dict 的读写操作要考虑 rehash 状态

dictAdd

int dictAdd(dict *d, void *key, void *val) {
  dictEntry *entry = dictAddRaw(d, key);
  if (!entry) return DICT_ERR;
  dictSetVal(d, entry, val);
  return DICT_OK;
}

dictEntry *dictAddRaw(dict *d, void *key) {
  int index;
  dictEntry *entry;
  dictht *ht;
  // 如果没有安全迭代器,就在查找之前进行单步 rehash
  if (dictIsRehasing(d)) _dictRehashStep(d);
  // 如果 key 已经存在,则返回 NULL
  if ((index = _dictKeyIndex(d, key)) == -1) return NULL;
  // 如果 dict 正在进行 rehash,需要将 entry 加入到 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++;
  // dictSetKey 是一个简单的宏,如果 dictType 中 dupKey 函数设置
  // 为非 NULL,则调用其复制 key 的值
  dictSetKey(d, entry, key);
  return entry;
}

dictAdd 中,如果发现 dict 正在进行 rehash,需要将新 entry 加入到 ht[1],否则可能出现数据丢失。

dictFind

dictEntry *dictFind(dict *d, const void *key) {
  dictEntry *he;
  unsigned int h, idx, table;
  // 不管是不是在 rehash,一个非空 dict 的 ht[0].size 都不应该是0
  if (d->ht[0].size == 0) return NULL;
  
  if (dictIsRehashing(d)) _dictRehashStep(d);
  // 计算 hash 值
  h = dictHashKey(d, key);
  // 分别查找 ht[0], ht[1]
  for (table = 0; table <= 1; table++) {
    idx = h & d->ht[table].sizemask;
    he = d->ht[table].table[idx];
    while (he) {
      if (dictCompareKeys(d, key, he->key)) return he;
      he = he->next
    }
    // 如果 ht[0] 里面没找到,而且没有 rehash 的话,可以直接返回 NULL
    // 因为我们在 rehash 结束以后,将 ht[1] 的 table 置为 NULL,如果
    // 不进行判断,会导致 core dump,也可以在取 idx 前判断 ht[table].size != 0
    if (!dictIsRehashing(d)) return NULL;
  }
  // key 不存在
  return NULL;
}

dictFind 中,在 rehash 正在进行的情况下,我们需要分别查找 ht[0],ht[1]。同时还要注意因为如果没有进行 rehash,d->ht[1].table 是 NULL,我们要小心访问空指针。

dictDelete

int dictGenericDelete(dict *d, const void *key, int nofree) {
  unsigned int h, idx;
  dictEntry *he, prevHe;
  int table;
  // 不能在空 dict 上进行删除
  if (d->ht[0].size == 0) return DICT_ERR;
  if (dictIsRehashing(d)) _dictRehashStep(d);
  
  h = dictHashKey(d, key);
  
  for (table = 0; table <= 1; table++) {
    idx = h & d->ht[table].sizemask;
    he = d->ht[table].table[idx];
    prevHe = NULL;
    while (he) {
      if (dictCompareKeys(d, key, he->key)) {
        // 从链表中删除
        if (prevHe)
          prevHe->next = he->next;
        else
          d->ht[table].table[idx] = he->next;
        // 需要释放 key,val 的资源
        if (!nofree) {
          dictFreeKey(d, he);
          dictFreeVal(d, he);
        }
        
        zfree(de);
        d->ht[table].used--;
        return DICT_OK;
      }
      prevHe = he;
      he = he->next;
    }
    // 没有在 rehash 的话,不要继续查找 ht[1]
    if (!dictRehasing(d))
      break;
  }
  // 没有对应的 key
  return DICT_ERR;
}

int dictDelete(dict *d, const void *key){
  // dictDelete 删除 key,val 对应的资源
	return dictGenericDelete(d, key, 0);
}

如果 dict 在 rehash 那么删除时我们也需要查找 ht[0],ht[1]。

dictRelease

int _dictClear(dict *d, dictht *ht, void(*callback)(void*)) {
  unsigned long i;
  // 释放所有 entry 
  for (i = 0; i < ht->size && ht->used > 0; i++) {
    dictEntry *he, *nextHe;
    // ???
    if (callback && (i & 65535) == 0) callback(d->privdata);
    if ((he = ht->table[i]) == NULL) continue;
    while (he) {
      nextHe = he->next;
      dictFreeKey(d, he);
      dictFreeVal(d, he);
      zfree(he);
      ht->used--;
      he = nextHe;
    }
  }
  // 释放 bucekts
  zfree(ht->table);
  _dictReset(ht);
  return DICT_OK;
}
void dictRelease(dict *d) {
  _dictClear(d, &d->ht[0], NULL);
  _dictClear(d, &d->ht[1], NULL);
  zfree(d);
}

在 dict 析构时,我们需要释放两个底层 hashtable 的资源。

上面我们分析了 dict 的构造、析构接口以及增、删、查接口。相信大家已经明白了 incremental rehashing 是什么意思:将 hashtable 的 rehash 不再是一个原子函数,我们可以在搬运了部分数据以后暂停 rehash 流程。

当 rehash 尚未完成时,dict 中的两个 hashtable 成员中均有数据,dict 的增、删、查接口均需要作出相应的改变。

why incrent rehashing

那么为什么 redis 要不辞新劳的将一个 dict rehash 的过程拆分成开呢?我认为有如下原因:

  1. 因为 redis 的 rdb 和 aof 机制都是通过子进程 dump 数据实现的。而在常见的操作系统中,父子进程的内存都有 cow(copy on write 机制来进行优化)。通过尽量避免父进程修改数据,可以让父子进程共享相同的物理页而无需为子进程拷贝物理页。所以 redis 在启动 rdb 或者 aof 子进程以后,就会 disable resize(相关代码见 redis.c: updateDictResizePolicy)。
  2. redis 是单进程架构,其应该尽量避免在一次 eventloop 进行任何耗时过长的操作。通过将 resize 过程分拆,可以避免一个超大 dict 进行 resize 的时候导致其他请求无法得到响应。

最后需要注意的是,及时我们已经 disable resize,如果一个 dict 的 load factor 大于 5,redis 仍然会对这个 dict 进行 resize。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值