【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/

Redis源码研究:哈希表 - 蕫的博客的更多相关文章

  1. Redis源码研究--字典

    计划每天花1小时学习Redis 源码.在博客上做个记录. --------6月18日----------- redis的字典dict主要涉及几个数据结构, dictEntry:具体的k-v链表结点 d ...

  2. Redis源码解析之跳跃表(三)

    我们再来学习如何从跳跃表中查询数据,跳跃表本质上是一个链表,但它允许我们像数组一样定位某个索引区间内的节点,并且与数组不同的是,跳跃表允许我们将头节点L0层的前驱节点(即跳跃表分值最小的节点)zsl- ...

  3. Redis源码解析之跳跃表(一)

    跳跃表(skiplist) 有序集合(sorted set)是Redis中较为重要的一种数据结构,从名字上来看,我们可以知道它相比一般的集合多了一个有序.Redis的有序集合会要求我们给定一个分值(s ...

  4. Redis源码研究—基础知识

    1. Redis 是什么 Redis是一个开源的使用ANSI C语言编写的基于内存的key/value存储系统,与memcache类似,但它支持的value类型更多,包括:字符串(string).链表 ...

  5. Redis源码研究--字符串

    之前看的内容,占个位子,以后补上. ------------8月2日------------- 好久没看了,惭愧,今天抽了点时间重新看了Redis的字符串,一边写博客,一边看. Redis的字符串主要 ...

  6. Redis源码研究--跳表

    -------------6月29日-------------------- 简单看了下跳表这一数据结构,理解起来很真实,效率可以和红黑树相比.我就喜欢这样的. typedef struct zski ...

  7. Redis源码研究--启动过程

    ---------------------6月23日--------------------------- Redis启动入口即main函数在redis.c文件,伪代码如下: int main(int ...

  8. Redis源码研究--redis.h

    ------------7月3日------------ /* The redisOp structure defines a Redis Operation, that is an instance ...

  9. Redis源码研究--双向链表

    之前看的内容,占个位子,以后补上. ----------8月4日--------------- 双向链表这部分看的比较爽,代码写的中规中矩,心里窃喜,跟之前学的<数据结构>这本书中差不多. ...

随机推荐

  1. js split 的用法和定义 js split分割字符串成数组的实例代码

    关于js split的用法,我们经常用来将字符串分割为数组方便后续操作,今天写一段广告判断代码的时候,竟然忘了split的用法了,特整理下,方便需要的朋友, 关于js split的用法其它也不多说什么 ...

  2. TFS二次开发-基线文件管理器(2)-TFS登录

    首先需要做一个TFS的登录. 以前的文章是使用的DomainProjectPicker 最新的VS里面使用的是TeamProjectPicker 首先可以在WinForm程序上写一个Button,然后 ...

  3. 字符串 转 时间戳 unix_timestamp('2018-07-01 0:0:0')

    SELECT FROM_UNIXTIME(createtime,'%Y%m%d') AS d, s.app,SUM(o.val) FROM orders o LEFT JOIN app_type s ...

  4. HDU 3591 多重背包

    给出N种钱币和M 给出N种钱币的面值和个数 NPC拿着这N些钱币去买价值M的物品,能够多付.然后被找零,找零的钱也为这些面值.但没有数量限制 问最少经手的钱币数量 对于NPC做一个付款多重背包 然后对 ...

  5. 二值法方法综述及matlab程序

    在某些图像处理当中一个关键步是二值法,二值化一方面能够去除冗余信息,另一方面也会使有效信息丢失.所以有效的二值化算法是后续的处理的基础.比如对于想要最大限度的保留下面图的中文字,以便后续的定位处理. ...

  6. console、JSON兼容问题

    console在ie8上面竟然有兼容问题,JSON.stringify()在ie10下竟然会报错,再页面上引用一个json2.js能解决此问题.

  7. 2017最全的php面试题目及答案总结

    最近在网上看到很多的小伙伴们都在询问如何应对php面试,这个对于有工作经验和实战项目的小伙伴来说是没什么问题的,但是对于刚刚学习完php的小伙伴们.php面试却是一个很重要的一步,那么今天php中文网 ...

  8. Xib与Storyboard相关知识点

    相同点 都用来描述软件界面 都用Interface Builder工具来编辑 本质都是转换成代码去创建控件 不同点 Xib是轻量级的,用来描述局部的UI界面 Storyboard是重量级的,用来描述整 ...

  9. INSPIRED启示录 读书笔记 - 第38章 打造企业级产品的经验

    十大要点 1.可用性:很少有企业开发这类软件时会进行交互设计.视觉设计.可用性测试,因此产品才会表现得如此糟糕 2.产品正常工作:多数企业级产品根本没法使用,或者还需花大量的时间和资金开发临时补丁,产 ...

  10. INSPIRED启示录 读书笔记 - 第31章 苹果公司给我的启示

    苹果公司值得学习的经验 1.硬件为软件服务:苹果公司明白硬件必须为软件服务,软件直接服务用户,满足用户需求.采用多点触控显示屏.重力加速器.距离传感器这些硬件技术是为了配合软件满足用户需求 2.软件为 ...