[Redis源码阅读]dict字典的实现
dict的用途
dict是一种用于保存键值对的抽象数据结构,在redis中使用非常广泛,比如数据库、哈希结构的底层。
当执行下面这个命令:
> set msg "hello"
以及使用哈希结构,如:
> hset people name "hoohack"
都会使用到dict作为底层数据结构的实现。
结构的定义
先看看字典以及相关数据结构体的定义:
字典
/* 字典结构 每个字典有两个哈希表,实现渐进式哈希时需要用在将旧表rehash到新表 */
typedef struct dict {
dictType *type; /* 类型特定函数 */
void *privdata; /* 保存类型特定函数需要使用的参数 */
dictht ht[2]; /* 保存的两个哈希表,ht[0]是真正使用的,ht[1]会在rehash时使用 */
long rehashidx; /* rehashing not in progress if rehashidx == -1 rehash进度,如果不等于-1,说明还在进行rehash */
unsigned long iterators; /* number of iterators currently running 正在运行中的遍历器数量 */
} dict;
哈希表
/* 哈希表结构 */
typedef struct dictht {
dictEntry **table; /* 哈希表节点数组 */
unsigned long size; /* 哈希表大小 */
unsigned long sizemask; /* 哈希表大小掩码,用于计算哈希表的索引值,大小总是dictht.size - 1 */
unsigned long used; /* 哈希表已经使用的节点数量 */
} dictht;
哈希表节点
/* 哈希表节点 */
typedef struct dictEntry {
void *key; /* 键名 */
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; /* 值 */
struct dictEntry *next; /* 指向下一个节点, 将多个哈希值相同的键值对连接起来*/
} dictEntry;
dictType
/* 保存一连串操作特定类型键值对的函数 */
typedef struct dictType {
uint64_t (*hashFunction)(const void *key); /* 哈希函数 */
void *(*keyDup)(void *privdata, const void *key); /* 复制键函数 */
void *(*valDup)(void *privdata, const void *obj); /* 复制值函数 */
int (*keyCompare)(void *privdata, const void *key1, const void *key2); /* 比较键函数 */
void (*keyDestructor)(void *privdata, void *key); /* 销毁键函数 */
void (*valDestructor)(void *privdata, void *obj); /* 销毁值函数 */
} dictType;
把上面的结构定义串起来,得到下面的字典数据结构:

根据数据结构定义,把关联图画出来后,看代码的时候就更加清晰。
从图中也可以看出来,字典的哈希表里,使用了链表解决键冲突的情况,称为链式地址法。
rehash(重新散列)
当操作越来越多,比如不断的向哈希表添加元素,此时哈希表需要分配了更多的空间,如果接下来的操作是不断地删除哈希表的元素,那么哈希表的大小就会发生变化,更重要的是,现在的哈希表不再需要那么大的空间了,在redis的实现中,为了保证哈希表的负载因子维持在一个合理范围内,当哈希表保存的键值对太多或者太少时,redis对哈希表大小进行相应的扩展和收缩,称为rehash(重新散列)。
执行rehash的流程图

负载因子解释
负载因子 = 哈希表已保存节点数量 / 哈希表大小
负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,一般来说,当负载因子大于某个常数(可能是 1,或者 0.75 等)时,哈希表将自动扩容。
渐进式rehash
在上面的rehash流程图里面,rehash的操作不是一次性就完成了的,而是分多次,渐进式地完成。
原因是,如果需要rehash的键值对较多,会对服务器造成性能影响,渐进式地rehash避免了对服务器的影响。
渐进式的rehash使用了dict结构体中的rehashidx属性辅助完成。当渐进式哈希开始时,rehashidx会被设置为0,表示从dictEntry[0]开始进行rehash,每完成一次,就将rehashidx加1。直到ht[0]中的所有节点都被rehash到ht[1],rehashidx被设置为-1,此时表示rehash结束。
结合代码再深入理解
/* 实现渐进式的重新哈希,如果还有需要重新哈希的key,返回1,否则返回0
*
* 需要注意的是,rehash持续将bucket从老的哈希表移到新的哈希表,但是,因为有的哈希表是空的,
* 因此函数不能保证即使一个bucket也会被rehash,因为函数最多一共会访问N*10个空bucket,不然的话,函数将会耗费过多性能,而且函数会被阻塞一段时间
*/
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
assert(d->ht[0].size > (unsigned long)d->rehashidx);
/* 找到非空的哈希表下标 */
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx];
/* 实现将bucket从老的哈希表移到新的哈希表 */
while(de) {
unsigned int h;
nextde = de->next;
/* Get the index in the new hash table */
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;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* 如果已经完成了,释放旧的哈希表,返回0 */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* 继续下一次rehash */
return 1;
}
在渐进式rehash期间,所有对字典的操作,包括:添加、查找、更新等等,程序除了执行指定的操作之外,还会顺带将ht[0]哈希表索引的所有键值对rehash到ht[1]。比如添加:
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
int index;
dictEntry *entry;
dictht *ht;
/* 如果正在rehash,顺带执行rehash操作 */
if (dictIsRehashing(d)) _dictRehashStep(d);
/* 获取新元素的下标,如果已经存在,返回-1 */
if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
return NULL;
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; // 如果正在进行rehash操作,返回ht[1],否则返回ht[0]
entry = zmalloc(sizeof(*entry));
entry->next = ht->table[index];
ht->table[index] = entry;
ht->used++;
/* Set the hash entry fields. */
dictSetKey(d, entry, key);
return entry;
}
总结
使用一个标记值标记某项操作正在执行是编程中常用的手段,比如本文提到的rehashidx,多利用此手段可以解决很多问题。
我在github有对Redis源码更详细的注解。感兴趣的可以围观一下,给个star。Redis4.0源码注解。可以通过commit记录查看已添加的注解。
原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。
更多精彩内容,请关注个人公众号。

[Redis源码阅读]dict字典的实现的更多相关文章
- [Redis源码阅读]sds字符串实现
初衷 从开始工作就开始使用Redis,也有一段时间了,但都只是停留在使用阶段,没有往更深的角度探索,每次想读源码都止步在阅读书籍上,因为看完书很快又忘了,这次逼自己先读代码.因为个人觉得写作需要阅读文 ...
- [Redis源码阅读]redis持久化
作为web开发的一员,相信大家的面试经历里少不了会遇到这个问题:redis是怎么做持久化的? 不急着给出答案,先停下来思考一下,然后再看看下面的介绍.希望看了这边文章后,你能够回答这个问题. 为什么需 ...
- [PHP源码阅读]number_format函数
上次讲到PHP是如何解析大整数的,一笔带过了number_format的处理,再详细阅读该函数的源码,以下是小分析. 函数原型 string number_format ( float $number ...
- Redis源码阅读(五)集群-故障迁移(上)
Redis源码阅读(五)集群-故障迁移(上) 故障迁移是集群非常重要的功能:直白的说就是在集群中部分节点失效时,能将失效节点负责的键值对迁移到其他节点上,从而保证整个集群系统在部分节点失效后没有丢失数 ...
- Redis源码阅读(三)集群-连接初始化
Redis源码阅读(三)集群-连接建立 对于并发请求很高的生产环境,单个Redis满足不了性能要求,通常都会配置Redis集群来提高服务性能.3.0之后的Redis支持了集群模式. Redis官方提供 ...
- Redis源码阅读(二)高可用设计——复制
Redis源码阅读(二)高可用设计-复制 复制的概念:Redis的复制简单理解就是一个Redis服务器从另一台Redis服务器复制所有的Redis数据库数据,能保持两台Redis服务器的数据库数据一致 ...
- Redis源码阅读(六)集群-故障迁移(下)
Redis源码阅读(六)集群-故障迁移(下) 最近私人的事情比较多,没有抽出时间来整理博客.书接上文,上一篇里总结了Redis故障迁移的几个关键点,以及Redis中故障检测的实现.本篇主要介绍集群检测 ...
- Redis源码阅读(四)集群-请求分配
Redis源码阅读(四)集群-请求分配 集群搭建好之后,用户发送的命令请求可以被分配到不同的节点去处理.那Redis对命令请求分配的依据是什么?如果节点数量有变动,命令又是如何重新分配的,重分配的过程 ...
- Redis源码阅读(一)事件机制
Redis源码阅读(一)事件机制 Redis作为一款NoSQL非关系内存数据库,具有很高的读写性能,且原生支持的数据类型丰富,被广泛的作为缓存.分布式数据库.消息队列等应用.此外Redis还有许多高可 ...
随机推荐
- 【NOIP2012提高组】借教室
90分暴力解法: 用线段树,初始值为该天的教室数,每个人来申请的时候在这段区间减去借走的数,然后查询最小值是否小于0,是就输出-1,否则继续. (其实在vijos是可以直接A的,他们的评测机太快了) ...
- PHP开发中需要注意几点事项,新手少走弯路必备知识
这篇文章主要介绍了PHP开发需要注意的几点事项总结,非常详细,需要的朋友可以参考下.新手多看看避免走弯路. 1.使用内嵌的HTML代码,而不是PHP的echo语句. 因为PHP是一门嵌入式Web编程语 ...
- BST性能分析&改进思路——平衡与等价
极端退化 前面所提到的二叉搜索树,已经为我们对数据集进行高效的静态和动态操作打开了一扇新的大门.正如我们所看到的,BST从策略上可以看作是将之前的向量(动态数组)和链表结构的优势结合起来,不过多少令我 ...
- Java爬虫——B站弹幕爬取
如何通过B站视频AV号找到弹幕对应的xml文件号 首先爬取视频网页,将对应视频网页源码获得 就可以找到该视频的av号aid=8678034 还有弹幕序号,cid=14295428 弹幕存放位置为 h ...
- NFS介绍和安装
NFS简单介绍 NFS 是Network File System的缩写,即网络文件系统. 一种使用于分散式文件系统的协定,由Sun公司开发,于1984年向外发布.功能是通过网络让不同的机器.不同的操作 ...
- java 可变參数
我们在某些特定的需求环境下,可能要对某一个方法中的參数进行一些操作,并且这些方法中的參数是不规定的,那么问题来了,我们该怎么办呢? java事实上就为我们考虑了这样的情况,那就是使用可变參数 可变參数 ...
- 【Java集合源代码剖析】LinkedHashmap源代码剖析
转载请注明出处:http://blog.csdn.net/ns_code/article/details/37867985 前言:有网友建议分析下LinkedHashMap的源代码.于是花了一晚上时间 ...
- Linux禁用显示“缓冲调整”
Linux禁用显示"缓冲调整" youhaidong@youhaidong-ThinkPad-Edge-E545:~$ free -o total used free shared ...
- Ubuntu下关闭防火墙
默认情况下ubuntu无firewall,除非你自己安装了,怎么装的就怎么删呗. . 假设是已启用的自备的iptables 删了即可了 sudo apt-get remove iptables.
- Esri:为Web GIS注入新内涵
纵观近些年IT与空间技术的发展,云计算.大数据.实时信息.LBS.无人机.倾斜摄影等新技术层出不穷:互联网基础设施建设成绩瞩目,宽带成为国家战略性公共基础设施. GIS(地理信息系统)作为空间信息分析 ...