zoukankan      html  css  js  c++  java
  • Redis源码研究:哈希表

    http://dongxicheng.org/nosql/redis-code-hashtable/

    1. Redis中的哈希表

    前面提到Redis是个key/value存储系统,学过数据结构的人都知道,key/value最简单的数据结果就是哈希表(当然,还有其他方式,如B-树,二叉平衡树等),hash表的性能取决于两个因素:hash表的大小和解决冲突的方法。这两个是矛盾的:hash表大,则冲突少,但是用内存过大;而hash表小,则内存使用少,但冲突多,性能低。一个好的hash表会权衡这两个因素,使内存使用量和性能均尽可能低。在Redis中,哈希表是所有其他数据结构的基础,对于其他所有数据结构,如:string,set,sortedset,均是保存到hash表中的value中的,这个可以很容易的通过设置value的类型为void*做到。本文详细介绍了Redis中hash表的设计思想和实现方法。

    【注】 本文的源代码分析是基于redis-2.4.3版本的。

    2. Redis哈希表的设计思想

    下图是从淘宝《Redis内存存储结构分析》中摘得的图片,主要描述Redis中hash表的组织方式。

    在Redis中,hash表被称为字典(dictionary),采用了典型的链式解决冲突方法,即:当有多个key/value的key的映射值(每对key/value保存之前,会先通过类似HASH(key) MOD N的方法计算一个值,以便确定其对应的hash table的位置)相同时,会将这些value以单链表的形式保存;同时为了控制哈希表所占内存大小,redis采用了双哈希表(ht[2])结构,并逐步扩大哈希表容量(桶的大小)的策略,即:刚开始,哈希表ht[0]的桶大小为4,哈希表ht[1]的桶大小为0,待冲突严重(redis有一定的判断条件)后,ht[1]中桶的大小增为ht[0]的两倍,并逐步(注意这个词:”逐步”)将哈希表ht[0]中元素迁移(称为“再次Hash”)到ht[1],待ht[0]中所有元素全部迁移到ht[1]后,再将ht[1]交给ht[0](这里仅仅是C语言地址交换),之后重复上面的过程。

    3. Redis哈希表实现

    3.1  基本数据结构

    Redis哈希表的实现位于文件dict.h和dict.c中,主要数据结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //hash表结构
     
    typedefstructdictht {
     
      dictEntry **table; //hash 表中的数据,以key/value形式,通过单链表保存
     
      unsigned longsize; //桶个数
     
      unsigned longsizemask; //size-1,方便定位
     
      unsigned longused; //实际保存的元素数
     
    } dictht;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //hash表结构,含有两个hash表,以实现增量再hash算法。
     
    typedefstructdict {
     
      dictType *type; //hash表的类型,可以是string, list等
     
      void*privdata; //该hash表的一些private数据
     
      dictht ht[2];
     
      intrehashidx; /* rehashing not in progress if rehashidx == -1 */
     
      intiterators; /* number of iterators currently running */
     
    } dict;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //hash表中每一项key/value,若key的映射值,以单链表的形式保存
     
    typedefstructdictEntry {
     
      void*key;
     
      void*val;
     
      structdictEntry *next;
     
    } dictEntry;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //每种hash table的类型,里面既有成员函数,又有成员变量,完全是模拟的C++类,注意,每个函数带有的privdata均为预留参数
     
    typedefstructdictType {
     
      unsigned int(*hashFunction)(constvoid*key); //要采用的hash函数
     
      void*(*keyDup)(void*privdata, constvoid*key); //对key进行拷贝
     
      void*(*valDup)(void*privdata, constvoid*obj); //对value进行拷贝
     
      int(*keyCompare)(void*privdata, constvoid*key1, constvoid*key2);//key比较器
     
      void(*keyDestructor)(void*privdata, void*key);//销毁key,一般为释放空间
     
      void(*valDestructor)(void*privdata, void*obj);//销毁value,一般为释放空间
     
    } dictType;

    3.2  基本操作

    Redis中hash table主要有以下几个对外提供的接口:dictCreate、dictAdd、dictReplace、dictDelete、dictFind、dictEmpty等,而这些接口调用了一些基础操作,包括:_dictRehashStep,_dictKeyIndex等。下面分析一下_dictRehashStep函数:

    该函数主要完成rehash操作。Hash Table在一定情况下会触发rehash操作,即:将第一个hash table中的数据逐步转移到第二个hash table中。

    【1】触发条件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //dict.c, _dictExpandIfNeeded()
     
    if(d->ht[0].used >= d->ht[0].size &&
     
      (dict_can_resize ||
     
        d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
     
    {
     
      returndictExpand(d, ((d->ht[0].size > d->ht[0].used) ?
     
        d->ht[0].size : d->ht[0].used)*2);
     
    }

    当第一个表的元素数目大于桶数目且元素数目与桶数目比值大于5时,hash 表就会扩张,扩大后新表的大小为旧表的2倍。

    【2】转移策略

    为了避免一次性转移带来的开销,Redis采用了平摊开销的策略,即:将转移代价平摊到每个基本操作中,如:dictAdd、dictReplace、dictFind中,每执行一次这些基本操作会触发一个桶中元素的迁移操作。在此,有读者可能会问,如果这样的话,如果旧hash table非常大,什么时候才能迁移完。为了提高前移速度,Redis有一个周期性任务serverCron,每隔一段时间会迁移100个桶。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //redis.c
     
    intdictRehashMilliseconds(dict *d, intms) {
     
      longlongstart = timeInMilliseconds();
     
      intrehashes = 0;
     
      while(dictRehash(d,100)) {
     
        rehashes += 100;
     
        if(timeInMilliseconds()-start > ms) break;
     
      }
     
      returnrehashes;
     
    }

    下面分析一下dictAdd函数:

    首先,检查hash table是否正在rehash操作,如果是,则分摊一个rehash开销:

    1
    if(dictIsRehashing(d)) _dictRehashStep(d);

    然后,检查该key/value的key是否已经存在,如果存在,则直接返回:

    1
    2
    3
    if((index = _dictKeyIndex(d, key)) == -1)
     
      returnDICT_ERR;

    需要注意的是,决定是否需要进行rehash是在查找操作(_dictKeyIndex)中顺便做的:

    1
    2
    3
    4
    5
    //_dictKeyIndex()
     
    if(_dictExpandIfNeeded(d) == DICT_ERR)
     
      return-1;

    接着,会通过hash算法定位该key的位置,并创建一个dictEntry节点,插入到对应单链表中:

    1
    2
    3
    4
    5
    6
    7
    entry = zmalloc(sizeof(*entry));
     
    entry->next = ht->table[index];
     
    ht->table[index] = entry;
     
    ht->used++;

    最后将key/value对填充到该entry中:

    1
    2
    3
    dictSetHashKey(d, entry, key);
     
    dictSetHashVal(d, entry, val);

    这就是整个dictAdd函数的流程。其他操作类似,均是刚开始分摊rehash开销(如果需要),然后通过hash方法定位位置,并进行相应的逻辑操作。

    原创文章,转载请注明: 转载自董的博客

    本文链接地址: http://dongxicheng.org/nosql/redis-code-hashtable/

    作者:Dong,作者介绍:http://dongxicheng.org/about/

    本博客的文章集合:http://dongxicheng.org/recommend/

  • 相关阅读:
    SAP字符串处理
    ABAP更换请求
    Odoo安装教程12-创建新的插件模块之设置视图层第二讲
    Odoo安装教程12-创建新的插件模块之设置视图层第一讲
    Odoo安装教程11-创建新的插件模块之设置访问权限
    SAP是什么?-SAP 各模块概览
    SAP是什么?-SAP 各产品释义
    SAP版本演进及区别-S/4 HANA 与R3(ECC)
    Odoo安装教程12-创建新的插件模块之设置视图层第二讲
    Odoo安装教程12-创建新的插件模块之设置视图层第一讲
  • 原文地址:https://www.cnblogs.com/lsx1993/p/4632430.html
Copyright © 2011-2022 走看看