Redis源码研究—哈希表

Redis源码研究—哈希表

Category: NoSQL数据库 View:
10,980 Author: Dong


作者:Dong | 新浪微博:西成懂 | 可以转载,
但必须以超链接形式标明文章原始出处和作者信息及版权声明

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

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


本博客微信公共账号:hadoop123(微信号为:hadoop-123),分享hadoop技术内幕,hadoop最新技术进展,发布hadoop相关职位和求职信息,hadoop技术交流聚会、讲座以及会议等。二维码如下:


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表结构

typedef

struct

dictht {

  dictEntry
**table;
//hash
表中的数据,以key/value形式,通过单链表保存

  unsigned
long

size;
//桶个数

  unsigned
long

sizemask;
//size-1,方便定位

  unsigned
long

used;
//实际保存的元素数

}
dictht;


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

//hash表结构,含有两个hash表,以实现增量再hash算法。

typedef

struct

dict {

  dictType
*type;
//hash表的类型,可以是string,
list等

  void

*privdata;
//该hash表的一些private数据

  dictht
ht[2];

  int

rehashidx;
/*
rehashing not in progress if rehashidx == -1 */

  int

iterators;
/*
number of iterators currently running */

}
dict;


1

2

3

4

5

6

7

8

9

10

11

//hash表中每一项key/value,若key的映射值,以单链表的形式保存

typedef

struct

dictEntry {

  void

*key;

  void

*val;

  struct

dictEntry *next;

}
dictEntry;


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

//每种hash
table的类型,里面既有成员函数,又有成员变量,完全是模拟的C++类,注意,每个函数带有的privdata均为预留参数

typedef

struct

dictType {

  unsigned
int

(*hashFunction)(
const

void

*key);
//要采用的hash函数

  void

*(*keyDup)(
void

*privdata,
const

void

*key);
//对key进行拷贝

  void

*(*valDup)(
void

*privdata,
const

void

*obj);
//对value进行拷贝

  int

(*keyCompare)(
void

*privdata,
const

void

*key1,
const

void

*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))

{

  return

dictExpand(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

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;

}

下面分析一下dictAdd函数:

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


1

if

(dictIsRehashing(d)) _dictRehashStep(d);

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


1

2

3

if

((index = _dictKeyIndex(d, key)) == -1)

  return

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

时间: 2024-08-04 17:32:50

Redis源码研究—哈希表的相关文章

Redis源码研究:哈希表 - 蕫的博客

[http://dongxicheng.org/nosql/redis-code-hashtable/] 1. Redis中的哈希表 前面提到Redis是个key/value存储系统,学过数据结构的人都知道,key/value最简单的数据结果就是哈希表(当然,还有其他方式,如B-树,二叉平衡树等),hash表的性能取决于两个因素:hash表的大小和解决冲突的方法.这两个是矛盾的:hash表大,则冲突少,但是用内存过大:而hash表小,则内存使用少,但冲突多,性能低.一个好的hash表会权衡这两个

memcached源码分析-----哈希表基本操作以及扩容过程

        转载请注明出处:http://blog.csdn.net/luotuo44/article/details/42773231 温馨提示:本文用到了一些可以在启动memcached设置的全局变量.关于这些全局变量的含义可以参考<memcached启动参数详解>.对于这些全局变量,处理方式就像<如何阅读memcached源代码>所说的那样直接取其默认值. assoc.c文件里面的代码是构造一个哈希表.memcached快的一个原因是使用了哈希表.现在就来看一下memca

redis源码分析4---结构体---跳跃表

redis源码分析4---结构体---跳跃表 跳跃表是一种有序的数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的: 跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点.性能上和平衡树媲美,因为事先简单,常用来代替平衡树. 在redis中,只在两个地方使用了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构. 1 跳跃表节点 1.1 层 层的数量越多,访问其他节点的速度越快: 1.2 前进指针 遍历举例

redis源码分析3---结构体---字典

redis源码分析3---结构体---字典 字典,简单来说就是一种用于保存键值对的抽象数据结构: 注意,字典中每个键都是独一无二的:在redis中,内部的redis的数据库就是使用字典作为底层实现的: 1 字典的实现 在redis中,字典是使用哈希表作为底层实现的,一个hash表里面可以有多个hash表节点,而每个hash表节点就保存了字典中的一个键值对: hash表定义 table属性是一个数组,数组中的每个元素都是一个指向dictEntry结构的指针,每个dictEntry结构保存着一个键值

Redis源码解析:15Resis主从复制之从节点流程

Redis的主从复制功能,可以实现Redis实例的高可用,避免单个Redis 服务器的单点故障,并且可以实现负载均衡. 一:主从复制过程 Redis的复制功能分为同步(sync)和命令传播(commandpropagate)两个操作: 同步操作用于将从节点的数据库状态更新至主节点当前所处的数据库状态: 命令传播操作则用于在主节点的数据库状态被修改,导致主从节点的数据库状态不一致时,让主从节点的数据库重新回到一致状态: 1:同步 当客户端向从节点发送SLAYEOF命令,或者从节点的配置文件中配置了

redis源码分析(1)--makefile和目录结构分析

一.redis源码编译 redis可以直接在官网下载(本文使用版本 3.0.7):https://redis.io/download 安装: $ tar xzf redis-3.0.7.tar.gz $ cd redis-3.0.7 $ make make执行以后主要编译产物在src/redis-server src/redis-cli 如果想把redis-server直接install到可执行目录/usr/local/bin,还需要执行: $ make install Run Redis wi

【Redis源码剖析】 - Redis数据类型之列表List

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51166709 今天为大家带来Redis五大数据类型之一 – List的源码分析. Redis中的List类型是一种双向链表结构,主要支持以下几种命令: lpush.rpush.lpushx.rpushx lpop.rpop.lrange.ltrim.lrem.rpoplpush linsert.llen.lindex.lset blpop.brpop.brpoplpush Li

Redis源码解析之ziplist

Ziplist是用字符串来实现的双向链表,对于容量较小的键值对,为其创建一个结构复杂的哈希表太浪费内存,所以redis 创建了ziplist来存放这些键值对,这可以减少存放节点指针的空间,因此它被用来作为哈希表初始化时的底层实现.下图即ziplist 的内部结构. Zlbytes是整个ziplist 所占用的空间,必要时需要重新分配. Zltail便于快速的访问到表尾节点,不需要遍历整个ziplist. Zllen表示包含的节点数. Entries表示用户增加上去的节点. Zlend是一个255

【Redis源码剖析】 - Redis内置数据结构值压缩字典zipmap

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51111230 今天为大家带来Redis中zipmap数据结构的分析,该结构定义在zipmap.h和zipmap.c文件中.我把zipmap称作"压缩字典"(不知道这样称呼正不正确)是因为zipmap利用字符串实现了一个简单的hash_table结构,又通过固定的字节表示节省空间.zipmap和前面介绍的ziplist结构十分类似,我们可以对比地进行学习: Redis中