Redis源码分析之字典dict
一、dict的使用场景
redis数据结构set, zset, hash, string类型的会用到dict。zset, hash类型在数据量少的时候会在ziplist, 数据量大时,zset使用dict + skiplist的数据结构,dict来存储value和score的映射关系。set在集合元素全部为int这种特殊情况下会使用intset, 通过二叉查找来查找数据。
其他场景例如集群模式中用来存储ip:port与clusterNode的映射关系。 二、dict的数据结构
核心数据结构定义如下有:
2.1 dictEntry typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry;
2.2 dictht typedef struct dictht { dictEntry **table; // table size unsigned long size; //用于将哈希值映射到table的位置索引 = size - 1, hash(key) % sizemask 来计算落到table的那个bucket上 unsigned long sizemask; //dict中数据个数,used / size的比值就是装载因子 } dictht;
2.3 dict typedef struct dict { dictType *type; void *privdata; dictht ht[2]; /* 表示rehash的步骤,rehashidx == -1 当前没有进行rehash */ long rehashidx; /* number of iterators currently running */ unsigned long iterators; } dict;
2.4 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;
层属关系代码看着不太直观,我们换成图:
redis dict核心数据结构模型三、dict的核心API
dict在需要扩展内存时避免一次性对所有key进行rehash,而是将rehash操作分散到对于dict的各个增删改查的操作中去。这种方法能做到每次只对一小部分key进行重哈希,而每次重哈希之间不影响dict的操作。这是为了避免rehash期间单个请求的响应时间剧烈增加。
判断是否进行rehashstatic int _dictExpandIfNeeded(dict *d) { /* 判定是否当前处于rehash的状态. */ if (dictIsRehashing(d)) return DICT_OK; /* 初始设置,table size = 4 */ if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* If we reached the 1:1 ratio, and we are allowed to resize the hash * table (global setting) or we should avoid it but the ratio between * elements/buckets is over the "safe" threshold, we resize doubling * the number of buckets. */ if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || //数据量超过table size的5倍 d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { //根据当前dict元素数量的2倍进行扩展 return dictExpand(d, d->ht[0].used*2); } return DICT_OK; }
dict扩展逻辑 int dictExpand(dict *d, unsigned long size) { /* the size is invalid if it is smaller than the number of * elements already inside the hash table */ if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR; dictht n; /* the new hash table */ unsigned long realsize = _dictNextPower(size); /* Rehashing to the same table size is not useful. */ if (realsize == d->ht[0].size) return DICT_ERR; /* 根据新的size重新分配一个hashtable */ n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; /* Is this the first initialization? If so it"s not really a rehashing * we just set the first hash table so that it can accept keys. */ if (d->ht[0].table == NULL) { d->ht[0] = n; return DICT_OK; } /* 重新分配的hashtable置于ht[1] */ d->ht[1] = n; //开始渐进rehash的标记 d->rehashidx = 0; return DICT_OK; } //如果2^n >= used * 2 返回 2^n, hashtable的size一定是2^n static unsigned long _dictNextPower(unsigned long size) { unsigned long i = DICT_HT_INITIAL_SIZE; if (size >= LONG_MAX) return LONG_MAX + 1LU; while(1) { if (i >= size) return i; i *= 2; } }
开启渐进式rehash
什么时候开启渐进式rehash, 在每次从hashtable中定位key的时候,代码如下 static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing) { unsigned long idx, table; dictEntry *he; if (existing) *existing = NULL; /* 判断是否需要扩展 */ if (_dictExpandIfNeeded(d) == DICT_ERR) return -1; /* 拉链法遍历hashtable,遍历两个hashtable */ /* ht[0], ht[1], 如果当前没有在rehash,则只遍历ht[0] */ for (table = 0; table <= 1; table++) { //计算数据所在bucket位置,找到链表第一个entry idx = hash & d->ht[table].sizemask; /* Search if this slot does not already contain the given key */ he = d->ht[table].table[idx]; //遍历链表, 比较key值 while(he) { if (key==he->key || dictCompareKeys(d, key, he->key)) { if (existing) *existing = he; return -1; } he = he->next; } if (!dictIsRehashing(d)) break; } return idx; }
分步渐进rehash
什么时候进行分步渐进rehash, 在每一个add, get操作中: dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) { long index; dictEntry *entry; dictht *ht; //进行分步rehash if (dictIsRehashing(d)) _dictRehashStep(d); /* Get the index of the new element, or -1 if * the element already exists. */ if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) return NULL; /* Allocate the memory and store the new entry. * Insert the element in top, with the assumption that in a database * system it is more likely that recently added entries are accessed * more frequently. */ /** rehash时, 数据存入ht[1] */ ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; entry = zmalloc(sizeof(*entry)); entry->next = ht->table[index]; ht->table[index] = entry; ht->used++; /* 设置key, 返回entry*/ dictSetKey(d, entry, key); return entry; }
分步rehash过程 static void _dictRehashStep(dict *d) { if (d->iterators == 0) dictRehash(d,1); } 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; /* Note that rehashidx can"t overflow as we are sure there are more * elements because ht[0].used != 0 */ assert(d->ht[0].size > (unsigned long)d->rehashidx); /* 如果table中存在bucket为空时,检查10个bucket后都为空则返回,等下一次再处理 */ while(d->ht[0].table[d->rehashidx] == NULL) { d->rehashidx++; if (--empty_visits == 0) return 1; } de = d->ht[0].table[d->rehashidx]; /* 一次处理table中的一个bucket */ /* Move all the keys in this bucket from the old to the new hash HT */ while(de) { uint64_t 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++; } /* Check if we already rehashed the whole table... */ 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; } /* More to rehash... */ return 1; }
redis dict的缩小的触发存在两种情况 hash,set,zset这三类数据使用的dict, 在删除元素的时候会尝试rehash 另一种是定时尝试进行rehash
尝试触发的代码如下 //当used / size < 10%时进行开启缩容rehash。 int htNeedsResize(dict *dict) { long long size, used; size = dictSlots(dict); used = dictSize(dict); return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL)); } /* If the percentage of used slots in the HT reaches HASHTABLE_MIN_FILL * we resize the hash table to save memory */ void tryResizeHashTables(int dbid) { if (htNeedsResize(server.db[dbid].dict)) dictResize(server.db[dbid].dict); if (htNeedsResize(server.db[dbid].expires)) dictResize(server.db[dbid].expires); }
由于rehash时需要分配内存,此部分内存是从redis maxmemory中的一部分,因此当bucket比较大时,rehash生成ht[1]时会使用大量内存。如果ht[1]占用的内存 + 原来的内存超过maxmeory,则会发生key逐出。由于sizeof(dictEntry*) = 8byte,因此当ht[0] size为4时,ht[1]扩展2倍,ht[1]占用内存为8 * 4 * 2 = 64byte,当size = 2^n 时,ht[1]占用的内存为8 * 2 * 2^n。
三、dict scan
redis中dict是有状态的,dict存在四种状态:
1 正常状态
2 正在rehash状态
3 rehash扩容完成状态
4 rehash缩容完成状态
由于redis中dict是有状态的,只有在正常状态下可以完整的扫描字典中的所有的key。当dict在进行扩容和缩容时可能会存在扫描key的遗漏或者重复扫描。由于redis的dict中存在两个hashtable,分别为ht[0], ht[1]。rehash的过程是将ht[0]的key重新hash到ht[1]。这时redis的所有key其实存在于两个hashtable中。redis针对四种情况的方案如下: Dict tablesize保持不变,稳定的状态下,直接顺序遍历即可 Dict Resize,dict扩大了,如果还是按照顺序遍历,就会导致扫描大量重复Key。比如tablesize从8变成了16,假设之前访问的是3号桶,那么表扩展后则是继续访问415号桶;但是,原先的03号桶中的数据在Dict长度变大后被迁移到811号桶中,因此,遍历811号桶的时候会有大量的重复Key被返回。 Dict Resize,dict缩小了,如果还是按照顺序遍历,就会导致大量的Key被遗漏。比如tablesize从8变成了4,假设当前访问的是3号桶,那么下一次则会直接返回遍历结束了;但是之前47号桶中的数据在缩容后迁移带 可03号桶中,因此这部分Key就无法扫描到。 字典正在rehash,这种情况如(2)和(3)情况一下,要么大量重复扫描、要么遗漏很多Key。
在redis正在rehash时,采用hash桶掩码的高位序访问来解决。如下图
hash桶掩码的高位序访问
高位序访问即按照dict sizemask(掩码),在有效位(上图中dict sizemask为00000111)上从高位开始加一枚举(右边图的);低位则按照有效位的低位逐步加一枚举(左边的图)。
高位序遍历路径 : 0->4->2->6->1->5->3->7
低位序遍历路径:0->1->2->3->4->5->6->7
redis采用掩码高位序访问的原因是在rehash时尽量少的重复扫描key。为什么从高位掩码访问会减少重复扫描。由于rehash最终计算bucket位置时是取模操作(v & size_mask),举个例子:
针对 v = 00101011,原来size_mask为00000111,取模 00000111 00101011 = 00000011
当size_mask为00001111,取模 00001111 00101011 00001011
可以看出取模后的两个值存在相同的后缀011。size_mask变大后,生成的结果只是高位发生变化。 原来落到bucket[00000011]的key,table size从8->16时,size_mask从00000111->00001111时 会落到 [00000011] 和 [00001011]中,高位变化,低位不变,此时再看一遍从高位序访问的图
看一个redis进行scan操作的例子,如下图。
redis从左边hashtable bucket0开始遍历,到遍历bucket6时发生了rehash。bucket长度为8扩展到16,redis的scan路径就会按照 6→14→1→9→5→13→3→11→7→15来完成,这里存在一个隐藏逻辑,一定是先遍历小hashtable,再变了大的hashtable。
hashtable bucket
从图上可以看出高位序scan在dict rehash时即可以避免重复遍历,又能完整返回原始的所有Key。同理,字典缩容时也一样,字典缩容可以看出是反向扩容。
来看一下redis scan源码 unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, dictScanBucketFunction* bucketfn, void *privdata) { dictht *t0, *t1; const dictEntry *de, *next; unsigned long m0, m1; if (dictSize(d) == 0) return 0; //没有进行rehash的情况 if (!dictIsRehashing(d)) { t0 = &(d->ht[0]); m0= t0->sizemask; /* Emit entries at cursor */ if (bucketfn) bucketfn(privdata, &t0->table[v & m0]); de = t0->table[v & m0]; while (de) { next = de->next; fn(privdata, de); de = next; } /* 高位序遍历计算出下一个cursor */ v |= ~m0; /* Increment the reverse cursor */ v = rev(v); v++; v = rev(v); } else { //进行rehash的情况 t0 = &d->ht[0]; t1 = &d->ht[1]; /* 确保t0为小hashtable,t1为大的hashtable */ if (t0->size > t1->size) { t0 = &d->ht[1]; t1 = &d->ht[0]; } m0 = t0->sizemask; m1 = t1->sizemask; /* 先根据cursor遍历小的hashtable */ if (bucketfn) bucketfn(privdata, &t0->table[v & m0]); de = t0->table[v & m0]; while (de) { next = de->next; fn(privdata, de); de = next; } /* 再变了大的hashtable,大的hashtable会遍历多个bucket */ do { /* Emit entries at cursor */ if (bucketfn) bucketfn(privdata, &t1->table[v & m1]); de = t1->table[v & m1]; while (de) { next = de->next; fn(privdata, de); de = next; } /* 反向二进制迭代算法来计算出下一个cursor */ v |= ~m1; v = rev(v); v++; v = rev(v); /* Continue while bits covered by mask difference is non-zero */ } while (v & (m0 ^ m1)); } return v; } // 反向二进制迭代算法 1. 对游标进行二进制翻转(原来的高位变成低位) 2. 低位+1 (对原来的高位进行+1,进位等操作) 3. 再进行一次二进制翻转 (恢复原来的高位,此时完成高位+1迭代)
作者介绍: 互联网十年交易/支付/搜索等架构和研发经验,擅长架构设计百亿级流量系统和高并发性能调优,目前专注做卫星通信/卫星遥感的研发工作,努力拥抱商业航天的星辰大海。 公众号:无量云颠
欢迎一起交流技术 !~
WCBA全明星正赛北区胜南区王丽丽当选最有价值球员新华社深圳2月5日电20222023赛季中国女子篮球联赛(WCBA)全明星周末正赛5日在深圳市龙华文体中心举行,北区明星队以8479战胜南区明星队,北区的王丽丽获得最有价值球员(M
北区明星队夺冠王丽丽当选MVP原标题北区明星队夺冠王丽丽当选MVP(主题)WCBA全明星周末落幕(副题)天津日报讯(记者苏娅辉)经过两天的激烈比拼,20222023赛季WCBA全明星周末昨天在深圳市龙华区文体中
新华全媒丨从厂区到地头河南化肥生产运输提速备春耕2月8日,在河南省商丘市宁陵县柳河镇桃园关村一家农资超市,村民将购买的化肥运到三轮车上。新华社记者张浩然摄时值春耕备耕时节,河南统筹做好化肥生产运输销售农技指导等工作,保障农资市场
湘潭市二中开展2023年春季开学德育系列活动2023年新学期伊始,湘潭市二中开展了2023年春季开学德育系列活动,以活动推动新学期教学工作开好局起好步。为了切实保障未成年人的合法权益,加强未成年人防性侵知识教育,进一步提升全
交广会客厅火车上的扫雷女孩2月6日凌晨5时许,夜色甚浓,空气透寒,此时的包头站站台上,仅有寥寥几位旅人,一列挂载着两节和平日车厢形色不大相同的火车头在众人注目下快速驶停到站台边。一个脸蛋冻的通红,身着电务对
民营经济规模壮大,履责绩效整体向好,山西省民营企业社会责任报告(2022)发布2月10日,山西省民营企业社会责任报告(2022)(以下简称报告)在省工商联十三届二次执委会议上发布。报告有效调研数据和样本覆盖全省11个市18个行业1083家不同类型民营企业,具
狂飙不被重用的小虎,死前的镜头,是狠狠打了强哥的脸我是原创作者小董乘风破浪。阿强为何不重用小虎,可能有很多人质疑,觉得阿强对小龙和小虎都是一样的,没有重用轻用这一说,那你就错了,用是分很多种的,小虎其实都知道,在最后的时刻,是小虎
新民艺评毛时安透过破晓东方迎来新世界的曙光2022年年末,作为国家广电总局庆祝党的二十大我们的新时代展播重点项目,由上海出品,中共上海市委宣传部直接命题并指导创作的重大革命历史题材电视剧破晓东方,在央视一套黄金时段播出。据
一叶茶香飘千年来源经济日报日前,联合国颁布人类非物质文化遗产代表作名录,作为中国文化名茶的代表,杭州市余杭区径山茶宴名列其中。余杭径山镇,历来以茶闻名。唐代茶圣陆羽曾在此游历,著写茶经。千年之后
德拉蒙德2115武切维奇2212,公牛大胜马刺在今日的常规赛中,公牛主场128104大胜马刺。双方开局交替得分,马刺连续篮下得分一度领先5分。武切维奇回应三分又补篮得分,随后拉文连得7分,公牛一波162反超9分。此后双方交替得
约什111胜爵士在今日的常规赛中,独行侠客场124111胜爵士。双方开局交替得分打成14平,随后马尔卡宁篮下得分克拉克森连续得分,塞克斯顿21盖伊比斯利各得分,爵士一波194领先了15分。此后双方
3050照样能玩肉鸽光追游戏,技嘉RTX3000系列试玩生死轮回你很难想像得到,一款肉鸡游戏火爆了国内沉静的游戏市场,不单是因为此款游戏使用了虚幻4引擎游戏,内置了光线追踪技术,NVIDIADLSS,以及NVIDIAReflex技术。更多的是此
长得太丑,孔子一出生就被扔进荒山亲近孔子的时刻又到了。9月26日至28日,2022中国(曲阜)国际孔子文化节第八届尼山世界文明论坛将在曲阜举办。你知道孔子的诞生地尼山,有着怎么样的故事吗?快来看看吧。孔子诞生的神
一碗牛肉干面火遍四方,屡登央视,这座城为什么不火这两天在盘点江浙沪宝藏小城的时候,感叹了无数遍宁波真的太好玩了!宁波,绝对是一座被狠狠低估的宝藏城市,不仅拥有小丽江宁海,还有可以实现海鲜自由的象山现在,又给大家找到了一座私藏宁波
上了年纪的朋友少喝绿茶咖啡,多喝这4种茶叶,茶味香醇好处多上了年纪的朋友少喝绿茶咖啡,多喝这4种茶叶,茶味香醇好处多。喝茶是很好的习惯,对于很多朋友来说,每天起床后第一件事情就是喝一杯茶。早上一杯茶,提神又醒脑,一整天活力满满,还有的朋友
交广会客厅广西桂林千年古镇乘龙腾飞吃香旅游饭九月的兴坪,蓝天澄净,碧水温柔,美如诗画。在网红景点二十元人民币背景图取景地,车如流水马如龙,游客拍照打卡,记录美好时光。白云朵朵,青山簇簇,人在画中,画在景中。广州籍游客姜女士是
策划指挥参加平型关战斗的开国将帅文欧阳青今年是平型关大捷85周年。1937年9月25日,抗日战争之初,刚刚由红军第一方面军部队整编的八路军第115师,在山西省灵丘县西南平型关一带的深壑沟谷中进行伏击战,痛歼侵华日
泰国十分重视2022年度APEC会议,已为晚宴设计好特色菜单9月26日,泰国总理办公室发言人阿努查透露了2022年度亚太经合组织(APEC)领导人非正式会议的准备进度,他表示,本届APEC会议将于11月1819日期间在诗丽吉国家会议中心举办
水景观碧水杜鹃红大滥滩水库大滥滩水库位于大山镇境内,主要功能是供水灌溉,总库容25万立方米,属于小(2)型水库,灌溉面积950亩。大坝为土坝,最大坝高14。2米,坝顶长51。2米。始建于1992年,于200
孤独的美食家(漫画7)孤独的美食家,享受一个人寻找和享受美食的过程,即使独来独往,即使一个人吃饭,也不马虎而是认真享受的生活状态,是对生活最朴素的尊重。好孤单,也好有仪式感,而这种人情味有着自己的节奏,
既喜欢孤独又害怕孤独,我是一个纠结体毕业快10年了,已经不怎么会写文章了,但是有感而发停不下来我从小学5年级就开始一个人住,初中在亲戚家,高中住校,大学在离家很远的城市,我习惯了一个人生活,一个人面对困难,无论难过的
杨幂好惨!含冤7年,被传婚内出轨李易峰堕胎,生日发文疑有所指没想到李易峰嫖娼事件,已经过去了十多天还没有落下帷幕,面对偶像跌落神坛,李易峰的粉丝也跳出来爆料自己的偶像,更是直接承认当年造谣杨幂与李易峰的绯闻。当年杨幂与李易峰二度合作男女主角