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表结构 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方法定位位置,并进行相应的逻辑操作。
原创文章,转载请注明: 转载自董的博客
作者:Dong,作者介绍:http://dongxicheng.org/about/
本博客的文章集合:http://dongxicheng.org/recommend/
Redis源码研究:哈希表 - 蕫的博客的更多相关文章
- Redis源码研究--字典
计划每天花1小时学习Redis 源码.在博客上做个记录. --------6月18日----------- redis的字典dict主要涉及几个数据结构, dictEntry:具体的k-v链表结点 d ...
- Redis源码解析之跳跃表(三)
我们再来学习如何从跳跃表中查询数据,跳跃表本质上是一个链表,但它允许我们像数组一样定位某个索引区间内的节点,并且与数组不同的是,跳跃表允许我们将头节点L0层的前驱节点(即跳跃表分值最小的节点)zsl- ...
- Redis源码解析之跳跃表(一)
跳跃表(skiplist) 有序集合(sorted set)是Redis中较为重要的一种数据结构,从名字上来看,我们可以知道它相比一般的集合多了一个有序.Redis的有序集合会要求我们给定一个分值(s ...
- Redis源码研究—基础知识
1. Redis 是什么 Redis是一个开源的使用ANSI C语言编写的基于内存的key/value存储系统,与memcache类似,但它支持的value类型更多,包括:字符串(string).链表 ...
- Redis源码研究--字符串
之前看的内容,占个位子,以后补上. ------------8月2日------------- 好久没看了,惭愧,今天抽了点时间重新看了Redis的字符串,一边写博客,一边看. Redis的字符串主要 ...
- Redis源码研究--跳表
-------------6月29日-------------------- 简单看了下跳表这一数据结构,理解起来很真实,效率可以和红黑树相比.我就喜欢这样的. typedef struct zski ...
- Redis源码研究--启动过程
---------------------6月23日--------------------------- Redis启动入口即main函数在redis.c文件,伪代码如下: int main(int ...
- Redis源码研究--redis.h
------------7月3日------------ /* The redisOp structure defines a Redis Operation, that is an instance ...
- Redis源码研究--双向链表
之前看的内容,占个位子,以后补上. ----------8月4日--------------- 双向链表这部分看的比较爽,代码写的中规中矩,心里窃喜,跟之前学的<数据结构>这本书中差不多. ...
随机推荐
- do while 宏定义如何使用?
#define verification(lhs,rhs,str) do { \ if (lhs == rhs) { \ std::cout << rang::style::bold &l ...
- HNOI2019爆零记
HNOI2019真-爆零祭 我怎么这么菜QAQ day-37 从学科溜过来搞OI. 班主任一直在谈论我退役的事情,这就是NOIP挂分的后果...说我没考好就找理由,人家xxxxxxx可不是xxxxxx ...
- 我的Android进阶之旅------>HTTP Content-type 对照表
HTTP Content-type 对照表 文件扩展名 Content-Type(Mime-Type) 文件扩展名 Content-Type(Mime-Type) .*( 二进制流, 不知道下载文件类 ...
- go语言之并发编程同步一
前面介绍了采用go语法的并行操作以及channel.既然是并行操作,那么就涉及到数据原子性以及同步的问题.所以在Go里面也需要采用同步的机制. 互斥锁: 由标准库代码包sync中的Mutex结构体类型 ...
- Java替换字符串中的占位符
在开发中,会有动态配置字符串其中的某些字符,如何使用字符中的占位符,并且在代码动态替换占位符实现动态配置字符串! 1.定义字符串时,再string文件添加字符串: 注意!记得要在字符文件中加上这些: ...
- Python基础-生成器和迭代器
生成器都是迭代器,迭代器不一定是生成器 def fansik(max): n, before, after = 0, 0, 1 while n < max: print(before) befo ...
- HAProxy安装及简单配置
一.HAProxy简介 代理的作用:web缓存(加速).反向代理.内容路由(根据流量及内容类型等将请求转发至特定服务器).转码器(将后端服务器的内容压缩后传输给client端).缓存的作用:减少冗余内 ...
- Python Redis pipeline操作(秒杀实现)
设想这样的一个场景,你要批量的执行一系列redis命令,例如执行100次get key,这时你要向redis请求100次+获取响应100次.如果能一次性将100个请求提交给redis server,执 ...
- 流量分析系统---flume(测试flume+kafka)
1.在flume官方网站下载最新的flume wget http://124.205.69.169/files/A1540000011ED5DB/mirror.bit.edu.cn/apach ...
- python3 多线程编程
python / 并发 / 线程 / 对象 / 编程 0.什么是线程 1. 多线程模块 2. 创建线程的方法 3. join()方法 4.isAlive()方法 5. name属性和daemon属 ...