Redis源码解析——字典遍历
之前两篇博文讲解了字典库的基础,本文将讲解其遍历操作。之所以将遍历操作独立成一文来讲,是因为其中的内容和之前的基本操作还是有区别的。特别是高级遍历一节介绍的内容,充满了精妙设计的算法智慧。(转载请指明出于breaksoftware的csdn博客)
迭代器遍历
由于Redis字典库有rehash机制,而且是渐进式的,所以迭代器操作可能会通过其他特殊方式来实现,以保证能遍历到所有数据。但是阅读完源码发现,其实这个迭代器是个受限的迭代器,实现方法也很简单。我们先看下其基础结构:
typedef struct dictIterator {dict *d;long index;int table, safe;dictEntry *entry, *nextEntry;/* unsafe iterator fingerprint for misuse detection. */long long fingerprint;
} dictIterator;
成员变量d指向迭代器处理的字典。index是dictht中table数组的下标。table是dict结构中dictht数组的下标,即标识ht[0]还是ht[1]。safe字段用于标识该迭代器是否为一个安全的迭代器。如果是,则可以在迭代过程中使用dictDelete、dictFind等方法;如果不是,则只能使用dictNext遍历方法。entry和nextEntry分别指向当前的元素和下一个元素。fingerprint是字典的指纹,我们可以先看下指纹算法的实现:
long long dictFingerprint(dict *d) {long long integers[6], hash = 0;int j;integers[0] = (long) d->ht[0].table;integers[1] = d->ht[0].size;integers[2] = d->ht[0].used;integers[3] = (long) d->ht[1].table;integers[4] = d->ht[1].size;integers[5] = d->ht[1].used;/* We hash N integers by summing every successive integer with the integer* hashing of the previous sum. Basically:** Result = hash(hash(hash(int1)+int2)+int3) ...** This way the same set of integers in a different order will (likely) hash* to a different number. */for (j = 0; j < 6; j++) {hash += integers[j];/* For the hashing step we use Tomas Wang's 64 bit integer hash. */hash = (~hash) + (hash << 21); // hash = (hash << 21) - hash - 1;hash = hash ^ (hash >> 24);hash = (hash + (hash << 3)) + (hash << 8); // hash * 265hash = hash ^ (hash >> 14);hash = (hash + (hash << 2)) + (hash << 4); // hash * 21hash = hash ^ (hash >> 28);hash = hash + (hash << 31);}return hash;
}
可以见得,它使用了ht[0]和ht[1]的相关信息进行Hash运算,从而得到该字典的指纹。我们可以发现,如果dictht的table、size和used任意一个有变化,则指纹将被改变。这也就意味着,扩容、锁容、rehash、新增元素和删除元素都会改变指纹(除了修改元素内容)。
生成一个迭代器的方法很简单,该字典库提供了两种方式:
dictIterator *dictGetIterator(dict *d)
{dictIterator *iter = zmalloc(sizeof(*iter));iter->d = d;iter->table = 0;iter->index = -1;iter->safe = 0;iter->entry = NULL;iter->nextEntry = NULL;return iter;
}dictIterator *dictGetSafeIterator(dict *d) {dictIterator *i = dictGetIterator(d);i->safe = 1;return i;
}
然后我们看下遍历迭代器的操作。如果是初次迭代,则要查看是否是安全迭代器,如果是安全迭代器则让其对应的字典对象的iterators自增;如果不是则记录当前字典的指纹
dictEntry *dictNext(dictIterator *iter)
{while (1) {if (iter->entry == NULL) {dictht *ht = &iter->d->ht[iter->table];if (iter->index == -1 && iter->table == 0) {if (iter->safe)iter->d->iterators++;elseiter->fingerprint = dictFingerprint(iter->d);}
因为要遍历的时候,字典可以已经处于rehash的中间状态,所以还要遍历ht[1]中的元素
iter->index++;if (iter->index >= (long) ht->size) {if (dictIsRehashing(iter->d) && iter->table == 0) {iter->table++;iter->index = 0;ht = &iter->d->ht[1];} else {break;}}iter->entry = ht->table[iter->index];} else {iter->entry = iter->nextEntry;}
往往使用迭代器获得元素后,会让字典删除这个元素,这个时候就无法通过迭代器获取下一个元素了,于是作者设计了nextEntry来记录当前对象的下一个对象指针
if (iter->entry) {/* We need to save the 'next' here, the iterator user* may delete the entry we are returning. */iter->nextEntry = iter->entry->next;return iter->entry;}}return NULL;
}
遍历完成后,要调用下面方法释放迭代器。需要注意的是,如果是安全迭代器,就需要让其指向的字典的iterators自减以还原;如果不是,则需要检测前后字典的指纹是否一致
void dictReleaseIterator(dictIterator *iter)
{if (!(iter->index == -1 && iter->table == 0)) {if (iter->safe)iter->d->iterators--;elseassert(iter->fingerprint == dictFingerprint(iter->d));}zfree(iter);
}
最后我们探讨下什么是安全迭代器。源码中我们看到如果safe为1,则让字典iterators自增,这样dict字典库中的操作就不会触发rehash渐进,从而在一定程度上(消除rehash影响,但是无法阻止用户删除元素)保证了字典结构的稳定。如果不是安全迭代器,则只能使用dictNext方法遍历元素,而像获取元素值的dictFetchValue方法都不能调用。因为dictFetchValue底层会调用_dictRehashStep让字典结构发生改变。
static void _dictRehashStep(dict *d) {if (d->iterators == 0) dictRehash(d,1);
}
我查了下该库在Redis中的应用,遍历操作不是为了获取值就是为了删除值,而没有增加元素的操作,如
void clusterBlacklistCleanup(void) {dictIterator *di;dictEntry *de;di = dictGetSafeIterator(server.cluster->nodes_black_list);while((de = dictNext(di)) != NULL) {int64_t expire = dictGetUnsignedIntegerVal(de);if (expire < server.unixtime)dictDelete(server.cluster->nodes_black_list,dictGetKey(de));}dictReleaseIterator(di);
}
高级遍历
高级遍历允许ht[0]和ht[1]之间数据在迁移过程中进行遍历,通过相应的算法可以保证所有的元素都可以被遍历到。我们先看下功能的实现:
unsigned long dictScan(dict *d,unsigned long v,dictScanFunction *fn,void *privdata)
参数d是字典的指针;v是迭代器,这个迭代器初始值为0,每次调用dictScan都会返回一个新的迭代器。于是下次调用这个函数时要传入新的迭代器的值。fn是个函数指针,每遍历到一个元素时,都是用该函数对元素进行操作。
typedef void (dictScanFunction)(void *privdata, const dictEntry *de);
Redis中这个方法的调用样例是:
do {cursor = dictScan(ht, cursor, scanCallback, privdata);} while (cursor &&maxiterations-- &&listLength(keys) < (unsigned long)count);
对于不在rehash状态的字典,则只要对ht[0]中迭代器指向的链表进行遍历就行了
dictht *t0, *t1;const dictEntry *de;unsigned long m0, m1;if (dictSize(d) == 0) return 0;if (!dictIsRehashing(d)) {t0 = &(d->ht[0]);m0 = t0->sizemask;/* Emit entries at cursor */de = t0->table[v & m0];while (de) {fn(privdata, de);de = de->next;}
如果在rehash状态,就要遍历ht[0]和ht[1]。遍历前要确定哪个dictht.table长度短(假定其长度为len=8),先对短的中该迭代器(假定为iter=4)对应的链进行遍历,然后遍历大的。然而不仅要遍历大的dictht中迭代器(iter=4)对应的链,还要遍历比iter大len的迭代器(4+8=12)对应的链表。
} else {t0 = &d->ht[0];t1 = &d->ht[1];/* Make sure t0 is the smaller and t1 is the bigger table */if (t0->size > t1->size) {t0 = &d->ht[1];t1 = &d->ht[0];}m0 = t0->sizemask;m1 = t1->sizemask;/* Emit entries at cursor */de = t0->table[v & m0];while (de) {fn(privdata, de);de = de->next;}/* Iterate over indices in larger table that are the expansion* of the index pointed to by the cursor in the smaller table */do {/* Emit entries at cursor */de = t1->table[v & m1];while (de) {fn(privdata, de);de = de->next;}/* Increment bits not covered by the smaller mask */v = (((v | m0) + 1) & ~m0) | (v & m0);/* Continue while bits covered by mask difference is non-zero */} while (v & (m0 ^ m1));}
最后要重新计算下次使用的迭代器并返回
/* Set unmasked bits so incrementing the reversed cursor* operates on the masked bits of the smaller table */v |= ~m0;/* Increment the reverse cursor */v = rev(v);v++;v = rev(v);return v;
}
从上面的设计来看,调用dictScan时不能有多线程操作该字典,否则会出现遗漏遍历的情况。但是在每次调用dictScan之间可以对字典进行操作。
其实这个遍历中最核心的是迭代器v的计算方法,我们只要让v从0开始,执行“或操作”最短ht.table(~m0)大小、二进制翻转、加1、再二进制翻转就可以实现0到~m0的遍历。我们看个例子:(下图有笔误,第一行十进制是4,第三行十进制是6)
我一直想不出这套算法为什么能满足这样的特点,还是需要数学大神解释一下。同时也可见这种算法的作者Pieter Noordhuis数学有一定功底。
关键这样的算法不仅可以完成遍历,还可以在数组大小动态变化时保证元素被全部遍历到。我把代码提炼出来,模拟了长度为8的数组向长度为16的数组扩容,和长度为16的数组向长度为8的数组缩容的过程。为了让问题简单化,我们先不考虑两个数组的问题,只认为数组在一瞬间被扩容和缩容。
我们先看下扩容前的遍历过程
假如第8次迭代后,数组瞬间扩容,这个时候遍历过程是
此时多了一次对下标为15的遍历,可以想象这次遍历应该会重复下标为15%8=7遍历(即第8次)的元素。所以dictScan具有潜在对一个元素遍历多次的问题。我们再看第7次迭代时发生瞬间扩容的情况
此时数组下标为11的遍历(即第8次遍历)会部分重复下标为3的遍历(即第7次遍历)元素。而之后的遍历就不会重复了。
我们再看下数组的缩容。为缩容前的状态是
如果第16次遍历时突然缩容,则遍历过程是
可见第16次遍历的是新数组下标为7的元素,和第15次遍历老数组下标为7的元素不同,本次遍历的结果包含前者(因为它还包含之前下标为15的元素)。所以也存在元素重复遍历的问题。
我们看下第15次遍历时突然缩容的遍历过程
因为缩容到8,所以最后一次遍历下标7的情况,既包括之前老数组下标为7的元素,也包含老数组下标为15的元素。所以本次遍历不会产生重复遍历元素的问题。
我们再看下第14次遍历突然缩容的遍历过程
第14次本来是要遍历下标为11的元素。由于发生缩容,就遍历新的数组的下标为3的元素。所以第14的遍历包含第13次的遍历元素。
一个数组如此,像dict结构中有两个dictht的情况,则稍微复杂点。我们通过下图可以发现,不同时机ht[0]扩容或者缩容,都可以保证元素被全遍历
上面测试的代码是:
#define TWO_FOUR_MASK 15
#define TWO_THREE_MASK 7static unsigned long rev(unsigned long v) {unsigned long s = 8 * sizeof(v);unsigned long mask = ~0;while ((s >>= 1) > 0) {mask ^= (mask <<s);v = ((v >> s) & mask) | ((v << s) & ~mask);}return v;
}unsigned long loop_single_expand_shrinks(unsigned long v, int change, int expand) {unsigned long m0 = 0;if (expand) {if (change) {m0 = TWO_FOUR_MASK;}else {m0 = TWO_THREE_MASK;}}else {if (change) {m0 = TWO_THREE_MASK;}else {m0 = TWO_FOUR_MASK;}}unsigned long t0idx = t0idx = v & m0; printf(" t0Index: %lu ", t0idx);v |= ~m0;v = rev(v);v++;v = rev(v);return v;
}unsigned long loop(unsigned long v) {unsigned long m0 = TWO_THREE_MASK;unsigned long m1 = TWO_FOUR_MASK;unsigned long t0idx = v & m0;printf(" t0Index: %lu ", t0idx);printf(" t1Index: ");do {unsigned long t1idx = v & m1;printf("%lu ", t1idx);v = (((v | m0) + 1) & ~ m0) | (v & m0);} while (v & (m0 ^ m1));v |= ~m0;v = rev(v);v++;v = rev(v);return v;
}unsigned long loop_expand_shrinks(unsigned long v, int change, int expand) {unsigned long m0 = 0;unsigned long m1 = 0;if (!change) {m0 = TWO_THREE_MASK;m1 = TWO_FOUR_MASK;unsigned long t0idx = v & m0;if (expand) {printf(" t0Index: %lu ", t0idx);printf(" t1Index: ");}else {printf(" t1Index: %lu ", t0idx);printf(" t0Index: ");}do {unsigned long t1idx = v & m1;printf("%lu ", t1idx);v = (((v | m0) + 1) & ~ m0) | (v & m0);} while (v & (m0 ^ m1));}else {if (expand) {m0 = TWO_FOUR_MASK;}else {m0 = TWO_THREE_MASK;}unsigned long t0idx = v & m0;printf(" t0Index: %lu ", t0idx);}v |= ~m0;v = rev(v);v++;v = rev(v);return v;
}void print_binary(unsigned long v) {char s[128] = {0};_itoa_s(v, s, sizeof(s), 2);printf("0x%032s", s);
}void check_loop_normal() {unsigned long v = 0;do {print_binary(v);v = loop(v);printf("\n");} while (v != 0);
}void check_loop_expand_shrinks(int expand) {int loop_count = 9;for (int n = 0; n < loop_count; n++) {unsigned long v = 0;int change = 0;int call_count = 0;do {if (call_count == n) {change = 1;}print_binary(v);v = loop_expand_shrinks(v, change, expand);call_count++;printf("\n");} while (v != 0);printf("\n");}
}void check_loop_single_expand_shrinks(int expand) {int loop_count = 17;for (int n = 0; n < loop_count; n++) {unsigned long v = 0;int change = 0;int call_count = 0;do {if (call_count == n) {change = 1;}print_binary(v);v = loop_single_expand_shrinks(v, change, expand);call_count++;printf("\n");} while (v != 0);printf("\n");}
}
相关文章:
开发者在行动!中国防疫开源项目登上GitHub TOP榜
整理 | 唐小引出品 | CSDN(ID:CSDNnews)【导读】用开发者们的方式支援这场没有硝烟的战争!截止北京时间 1 月 28 日下午 15:47,全国确诊新型冠状病毒的数字已经到达了 4586 例,疑似高达 6973 例,…

像童话一样学习OSPF原理
可以把整个网络(一个自治系统AS)看成一个王国,这个王国可以分成几个 区(area),现在我们来看看区域内的某一个人(你所在的机器root)是怎样得到一张 世界地图(routing table)的。 首先,你得跟你周围的人(…

队列——PowerShell版
继续读啊哈磊《啊哈!算法》感悟系列——队列 地铁售票处排队,先来的人先到队首先买完先走,后来的人排在队尾等候后买完后走。 想买票,必须排在队尾;买完票,只能从队首离开。 这种先进先出(First…

Redis源码解析——双向链表
相对于之前介绍的字典和SDS字符串库,Redis的双向链表库则是非常标准的、教科书般简单的库。但是作为Redis源码的一部分,我决定还是要讲一讲的。(转载请指明出于breaksoftware的csdn博客) 基本结构 首先我们看链表元素的结构。因为…

12月第三周安全要闻回顾:浏览器安全不容忽视,SSL弱点影响网站安全
本周(081215至081221)安全方面的新闻众多,主要集中在***与威胁趋势方面。浏览器安全方向波澜起伏,微软推出了针对上周公开的IE7新漏洞的紧急安全补丁,但目前互联网上针对该漏洞的大规模***仍在继续,******的…
GPT2文本生成有问题?这里有些潜在解决思路
作者 | Leo Gao译者 | 凯隐编辑 | 夕颜出品 | AI科技大本营(ID: rgznai100) 【导读】在过去的一年中,人们对文本生成模型的兴趣重新燃起,这在很大程度上要归功于GPT2,它主要展示了使用更大模型、更大数据和更大计算量的…

HTML5学习之二:HTML5中的表单2
(本内容部分节选自《HTML 5从入门到精通》) 对表单的验证 ———————————————————————————————————————————————————————— •1、required属性 required属性主要目的是确保表单控件中的值已填写。在提交时&…
Redis源码解析——有序整数集
有序整数集是Redis源码中一个以大尾(big endian)形式存储,由小到大排列且无重复的整型集合。它存储的类型包括16位、32位和64位的整型数。在介绍这个库的实现前,我们还需要先熟悉下大小尾内存存储机制。(转载请指明出于…
GitHub标星1.2w+,Chrome最天秀的插件都在这里
作者 | Rocky0429来源 | Python空间(ID: Devtogether)大家好,我是 Rocky0429,一个沉迷 Chrome 不能自拔的蒟蒻...作为一个在远古时代用过什么 IE、360、猎豹等浏览器的资深器哥,当我第一次了解 Chrome 的时候ÿ…

基础篇 第四节 项目进度计划编辑 之 任务关联性设定
1.任务关联性的类型 ◎完成 —— 开始 FS ◎开始 —— 开始 SS ◎开始 —— 完成 SF 完成 —— 完成 FF 2.设定任务关联性 三种方法: ◎在条形图中直接拖拽 ◎在“前置任务”列中编辑 ◎在“任务信息”中的“前置任务”选项卡中编辑 3.设定“延隔时间” 延隔时间小于…
开坑,写点Polymer 1.0 教程第3篇——组件注册与创建
之前一篇算是带大家大致领略了一下Polymer的风采。这篇我们稍微深入一丢丢,讲下组件的注册和创建。 创建自定义组件的几种方式 这里我们使用Polymer函数注册了一个自定义组件"my-element" // register an element Polymer({is: my-element,created: funct…
Redis源码解析——Zipmap
本文介绍的是Redis中Zipmap的原理和实现。(转载请指明出于breaksoftware的csdn博客) 基础结构 Zipmap是为了实现保存Pair(String,String)数据的结构,该结构包含一个头信息、一系列字符串对(之后把一个“字符串对”称为一个“元素…

IIS7入门之旅:(3)CGI application和FastCGI application的区别
前言: 一如既往地,IIS支持通过提供pluggable module来提供对第3方script的支持,例如php等。在IIS7中,对于CGI的支持有了一个新的变化,就是同时提供了2种CGI支持模块,分别为:CGIModule (cgi.dll)和FastCGIMo…

抗击疫情!阿里云为加速新药疫苗研发提供免费AI算力
1月29日,阿里云正式宣布:疫情期间,向全球公共科研机构免费开放一切AI算力,以加速本次新型肺炎新药和疫苗研发。 目前,中国疾控中心已成功分离病毒,疫苗研发和药物筛选仍在争分夺秒地进行。新药和疫苗研发期…
SpriteBuilder中如何平均拉伸精灵帧动画的距离
首先要在Timeline中选中所有的精灵帧,可以通过如下2种的任意一种办法达成: 1按下Shift键的同时鼠标单击它们 2鼠标在Timeline空白区拖拽直到拉出的矩形包围住所有精灵帧方块后放开鼠标。 你可以通过查看Timeline中精灵帧方块上是否有阴影来辨识是否选中…

C++拾趣——类构造函数的隐式转换
之前看过一些批判C的文章,大致意思是它包含了太多的“奇技淫巧”,并不是一门好的语言。我对这个“奇技淫巧”的描述颇感兴趣,因为按照批判者的说法,C的一些特性恰巧可以让一些炫耀技术的同学有了炫耀的资本——毕竟路人皆知的东西…
数十名工程师作战5天,阿里达摩院连夜研发智能疫情机器人
作者 | Just出品 | AI科技大本营(ID:rgznai100)新型肺炎疫情防控战在各大互联网科技公司拉响,阿里、百度等公司陆续对外提供相应技术和产品。当前,疫情当前防控一线人员紧缺,多地政务热线迎来大波问询市民,…

路由器互联端口处于不同网段的路由方法和原理
如下图:两台cisco路由器相连接的两个端口不在同一个网络,如何实现两台路由器的互联?初看似乎绝对不可能,但是这是可行的,而且我已经把这个变成了现实。这里讲述配置的方法,以及解释原理。这个就要讲到路由原…

上网行为管理产品选型简单考量
信息化浪潮汹涌向前,人们的生活、工作、学习越来越离不开IT,离不开网络。 对于很多组织来讲,网络就意味着效率、甚至生产力,协同办公、决策、科研、资金划拨等等都对网络有了前所未有的依赖。网络应用日益复杂、多变、动态特征发展…
码农技术炒股之路——配置管理器、日志管理器
配置管理器和日志管理器是项目中最为独立的模块。我们可以很方便将其剥离出来供其他Python工程使用。文件的重点将是介绍Python单例和logging模块的使用。(转载请指明出于breaksoftware的csdn博客) 配置管理器 在《码农技术炒股之路——架构和设计》中我…
“数学不好,干啥都不行!”资深程序员:别再瞎努力了!
之前很多程序员读者向我们反馈:1)做算法优化时,只能现搬书里的算法,遇到不一样的问题,就不会了。2)面试一旦涉及到算法和数据结构,如果数学不行,面试基本就凉凉了。3)算法…

受限列表 队列与栈
2019独角兽企业重金招聘Python工程师标准>>> 队列与栈为受限列表,队列为先入先出型列表,而栈为先入后出型列表,有关列表的实现可以查看 http://my.oschina.net/u/2011113/blog/514713 。 结构图为 Queue实现了IQueue接口ÿ…
码农技术炒股之路——数据库管理器、正则表达式管理器
我选用的数据库是Mysql。选用它是因为其可以满足我的需求,而且资料多。因为作为第三方工具,难免有一些配置问题。所以本文也会讲解一些和Mysql配置及开发相关的问题。(转载请指明出于breaksoftware的csdn博客) 数据库管理器 Mysq…

Overview of ISA and TMG Networking and ISA Networking Case Study (Part 1)
老方说:此篇文章摘自ISASERVER.ORG网站,出自Thomas Shinder达人之手。严重建议ISA爱好者看看。Published: Dec 16, 2008 Updated: Jan 21, 2009 Author: Thomas Shinder What ISA/TMG firewall Networks are about and how the firewall uses these ne…
阿里云免费开放一切AI算力,加速新型冠状病毒新药和疫苗研发
近日,阿里云宣布,为了帮助加速新药和疫苗研发,将向全球公共科研机构免费开放一切AI算力。目前,中国疾控中心已成功分离病毒,疫苗研发和药物筛选仍在争分夺秒地进行。新药和疫苗研发期间,需要进行大量的数据…

ASP.net(C#)批量上传图片(完整版)
来自:http://blog.itpub.net/9869521/viewspace-667955/ 这篇关于ASP.Net批量上传图片的文章写得非常好,偶尔在网上看到想转载到这里,却费劲了周折。为了更新这篇文章,我用了近半个小时,网上的转载都残缺不全ÿ…

码农技术炒股之路——任务管理器
系统任务和普通任务都是通过任务管理器调度的。它们的区别是:系统任务在程序运行后即不会被修改,而普通任务则会被修改。(转载请指明出于breaksoftware的csdn博客) 为什么要有这样的设计?因为我希望它是一个可以不用停…
面对新型肺炎疫情,AI能做什么?
作者 | 马超出品 | AI科技大本营(ID:rgznai100)根据最新的新型冠状病毒疫情通报,截至1月30日24时,国家卫生健康委公布确诊病例9692例,重症病例1527例,累计死亡病例213例,另有疑似病例15238例。为…

大家帮忙.谢谢!..(急急急急急)
大家帮忙.谢谢!..(急急急急急) Delphi / Windows SDK/APIhttp://www.delphi2007.net/DelphiDB/html/delphi_20061218224617231.htmlprocedure TForm1.Button4Click(Sender: TObject); var P : pstring; i, j : integer; begin GetMem(p, sizeof(stri…