redis7.x源码分析:(3) dict字典

dict字典采用经典hash表数据结构实现,由键值对组成,类似于C++中的unordered_map。两者在代码实现层面存在一些差异,比如gnustl的unordered_map分配的桶数组个数是(质数n),而dict分配的桶数组个数是(2^n);另外,dict对hash值相同的key采用了常规的开链法存储,而unordered_map在采用开链法的前提下,又使用了_M_before_begin将不同桶中的链表串联成了一个大链表,从而将遍历算法复杂度优化为O(n);还有就是,dict为应对服务器性能上的特殊要求,设计成了双hash表的形式,这也使得它在rehash,各种操作存在一些特殊性,我在下面的代码分析中会说到。

dict在redis里面的用途十分广泛,几乎所有的模块都会用到,其中的两大核心用途是:
 1. 16个数据库空间
 2. hash和zset类型数据的存储

dict相关结构定义:

// 节点
typedef struct dictEntry {// 任意类型键void *key;// 存储的值union {void *val;uint64_t u64;int64_t s64;double d;} v;// 同一个桶中链表的下一个元素struct dictEntry *next;     /* Next entry in the same hash bucket. */void *metadata[];           /* An arbitrary number of bytes (starting at a* pointer-aligned address) of size as returned* by dictType's dictEntryMetadataBytes(). */
} dictEntry;typedef struct dict dict;// 存储不同类型数据的字典,设置不同的处理函数
typedef struct dictType {uint64_t (*hashFunction)(const void *key);void *(*keyDup)(dict *d, const void *key);void *(*valDup)(dict *d, const void *obj);int (*keyCompare)(dict *d, const void *key1, const void *key2);void (*keyDestructor)(dict *d, void *key);void (*valDestructor)(dict *d, void *obj);int (*expandAllowed)(size_t moreMem, double usedRatio);/* Allow a dictEntry to carry extra caller-defined metadata.  The* extra memory is initialized to 0 when a dictEntry is allocated. */size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)struct dict {// 字典类型,不同的类型有不同的hash函数,dup函数dictType *type;// 双哈希表指针数组dictEntry **ht_table[2];// 存放的节点数unsigned long ht_used[2];// rehash索引(哈希表的下标),大于 -1 表示正在rehashlong rehashidx; /* rehashing not in progress if rehashidx == -1 *//* Keep small vars at end for optimal (minimal) struct padding */// rehash暂停标志int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */// 表示2的多少次幂,哈希表的大小=2^ht_size_expsigned char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};

hash表的创建比较简单直接略过,先看下 _dictExpand 的实现,它在hash表扩容缩容和创建时都会用到。当添加dictAdd时,存储的节点数 used / size >= 1,就需要调用它扩容。

int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{if (malloc_failed) *malloc_failed = 0;/* the size is invalid if it is smaller than the number of* elements already inside the hash table */// 正在rehash或者 used / size > 1直接退出if (dictIsRehashing(d) || d->ht_used[0] > size)return DICT_ERR;/* the new hash table */dictEntry **new_ht_table;unsigned long new_ht_used;// 获取第一个 2^N > size 的N的大小signed char new_ht_size_exp = _dictNextExp(size);/* Detect overflows */// 2^N 作为hash数组的长度, 另外判断分配的大小是否合法size_t newsize = 1ul<<new_ht_size_exp;if (newsize < size || newsize * sizeof(dictEntry*) < newsize)return DICT_ERR;/* Rehashing to the same table size is not useful. */if (new_ht_size_exp == d->ht_size_exp[0]) return DICT_ERR;/* Allocate the new hash table and initialize all pointers to NULL */if (malloc_failed) {new_ht_table = ztrycalloc(newsize*sizeof(dictEntry*));*malloc_failed = new_ht_table == NULL;if (*malloc_failed)return DICT_ERR;} elsenew_ht_table = zcalloc(newsize*sizeof(dictEntry*));new_ht_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. */// 如果是第一次创建hash表,则设置完第一个表后直接退出if (d->ht_table[0] == NULL) {d->ht_size_exp[0] = new_ht_size_exp;d->ht_used[0] = new_ht_used;d->ht_table[0] = new_ht_table;return DICT_OK;}/* Prepare a second hash table for incremental rehashing */// 设置第二个表后退出,并且开始rehashd->ht_size_exp[1] = new_ht_size_exp;d->ht_used[1] = new_ht_used;d->ht_table[1] = new_ht_table;d->rehashidx = 0;return DICT_OK;
}
// 检查是否需要缩容
int htNeedsResize(dict *dict) {long long size, used;// 哈希表大小size = dictSlots(dict);// 哈希表已用节点数量used = dictSize(dict);// 当哈希表的大小大于 > 4 并且用量小于 10%时缩容return (size && used && size > DICT_HT_INITIAL_SIZE &&(used*100/size < REDIS_HT_MINFILL));
}

在执行databasesCron时,如果数据库满足 htNeedsResize 会进行缩容,另外hash和zset类型数据在执行删除操作时,也会判断是否需要缩容。

扩容缩容都会触发开启rehash,最终调用 dictRehash 实现rehash的动作,以下是它的代码:

int dictRehash(dict *d, int n) {// 累计访问多少个空桶后退出rehashint empty_visits = n*10; /* Max number of empty buckets to visit. */if (!dictIsRehashing(d)) return 0;// 进行n次rehashwhile(n-- && d->ht_used[0] != 0) {dictEntry *de, *nextde;/* Note that rehashidx can't overflow as we are sure there are more* elements because ht[0].used != 0 */assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);while(d->ht_table[0][d->rehashidx] == NULL) {// 遇到空桶, 索引后移d->rehashidx++;if (--empty_visits == 0) return 1;}// 得到桶中链表第一个节点de = d->ht_table[0][d->rehashidx];/* Move all the keys in this bucket from the old to the new hash HT */// 遍历链表上所有的节点, 添加到hash表2中while(de) {uint64_t h;nextde = de->next;/* Get the index in the new hash table */// 计算hash值对应的索引 = hash & (2^exp - 1)h = dictHashKey(d, de->key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);// 添加到hash表2中de->next = d->ht_table[1][h];d->ht_table[1][h] = de;d->ht_used[0]--;d->ht_used[1]++;de = nextde;}// 清空hash表1的桶d->ht_table[0][d->rehashidx] = NULL;d->rehashidx++;}/* Check if we already rehashed the whole table... */// 如果rehash全部完成了, 则用表2替换表1, 并且释放原来的表1if (d->ht_used[0] == 0) {zfree(d->ht_table[0]);/* Copy the new ht onto the old one */d->ht_table[0] = d->ht_table[1];d->ht_used[0] = d->ht_used[1];d->ht_size_exp[0] = d->ht_size_exp[1];_dictReset(d, 1);d->rehashidx = -1;return 0;}/* More to rehash... */return 1;
}// 执行多少毫秒的rehash操作
int dictRehashMilliseconds(dict *d, int ms) {if (d->pauserehash > 0) return 0;long long start = timeInMilliseconds();int rehashes = 0;// 还没超时的情况下,一次尝试执行100个桶的rehashwhile(dictRehash(d,100)) {rehashes += 100;if (timeInMilliseconds()-start > ms) break;}return rehashes;
}

再分别看一下 dictAddRaw、dictGenericDelete、dictFind的代码:

/* Low level add or find:* This function adds the entry but instead of setting a value returns the* dictEntry structure to the user, that will make sure to fill the value* field as they wish.** This function is also directly exposed to the user API to be called* mainly in order to store non-pointers inside the hash value, example:** entry = dictAddRaw(dict,mykey,NULL);* if (entry != NULL) dictSetSignedIntegerVal(entry,1000);** Return values:** If key already exists NULL is returned, and "*existing" is populated* with the existing entry if existing is not NULL.** If key was added, the hash entry is returned to be manipulated by the caller.*/
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{long index;dictEntry *entry;int htidx;// 正在rehash过程中,则执行一次rehash操作(只把老表中一个桶对应链表内的数据节点重新hash到新表的不同桶中)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的话,新节点只会加到第二个哈希表中htidx = dictIsRehashing(d) ? 1 : 0;size_t metasize = dictMetadataSize(d);entry = zmalloc(sizeof(*entry) + metasize);if (metasize > 0) {memset(dictMetadata(entry), 0, metasize);}// 新节点添加到链表头entry->next = d->ht_table[htidx][index];d->ht_table[htidx][index] = entry;d->ht_used[htidx]++;/* Set the hash entry fields. */// 设置新节点的key, 如果设置了 type->KeyDup 函数, 则复制出一个key的副本保存到节点中dictSetKey(d, entry, key);return entry;
}/* Search and remove an element. This is a helper function for* dictDelete() and dictUnlink(), please check the top comment* of those functions. */
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {uint64_t h, idx;dictEntry *he, *prevHe;int table;/* dict is empty */if (dictSize(d) == 0) return NULL;// 正在rehash过程中,则执行一次rehash操作if (dictIsRehashing(d)) _dictRehashStep(d);h = dictHashKey(d, key);// 在hash表1和2中查找for (table = 0; table <= 1; table++) {// 计算索引获取桶中链表节点idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);he = d->ht_table[table][idx];prevHe = NULL;while(he) {// 遍历链表找到对应key的节点并删除if (key==he->key || dictCompareKeys(d, key, he->key)) {/* Unlink the element from the list */if (prevHe)prevHe->next = he->next;elsed->ht_table[table][idx] = he->next;if (!nofree) {// 释放节点中key/valuedictFreeUnlinkedEntry(d, he);}d->ht_used[table]--;return he;}prevHe = he;he = he->next;}if (!dictIsRehashing(d)) break;}return NULL; /* not found */
}dictEntry *dictFind(dict *d, const void *key)
{dictEntry *he;uint64_t h, idx, table;if (dictSize(d) == 0) return NULL; /* dict is empty */// 执行一次rehashif (dictIsRehashing(d)) _dictRehashStep(d);h = dictHashKey(d, key);// 在hash表1和2中查找for (table = 0; table <= 1; table++) {idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);he = d->ht_table[table][idx];while(he) {if (key==he->key || dictCompareKeys(d, key, he->key))return he;he = he->next;}if (!dictIsRehashing(d)) return NULL;}return NULL;
}

dict的rehash并不是一次完成的,这个是基于性能、响应时间上的考虑。会执行rehash有以下几个函数:

  • databasesCron函数,它会定时执行,对16个db中的dict和expires进行rehash,每次调用dictRehashMilliseconds执行1ms时间
  • dictAddRaw、dictGenericDelete、dictFind、dictGetRandomKey、dictGetSomeKeys函数,它们每次rehash只处理一个桶

最后看下迭代器的定义:

/* If safe is set to 1 this is a safe iterator, that means, you can call* dictAdd, dictFind, and other functions against the dictionary even while* iterating. Otherwise it is a non safe iterator, and only dictNext()* should be called while iterating. */
typedef struct dictIterator {// 迭代器对应的字典dict *d;// 正在迭代的hash索引long index;// table表示迭代的表是两张表中的哪一张;safe表示是否是安全迭代器int table, safe;// entry表示迭代器当前的节点;nextEntry表示下个节点dictEntry *entry, *nextEntry;/* unsafe iterator fingerprint for misuse detection. */// 指纹,判断迭代过程中有没执行被禁止的操作(dictNext以外的函数)unsigned long long fingerprint;
} dictIterator;

迭代器分为安全和不安全迭代器:

  • 安全迭代器,针对字典可以执行 dictAdd 等有可能修改的函数
  • 不安全迭代器,针对字典只能执行 dictNext

迭代的实现:

dictEntry *dictNext(dictIterator *iter)
{while (1) {if (iter->entry == NULL) {// 第一个迭代或者桶中的链表遍历完了if (iter->index == -1 && iter->table == 0) {// 安全模式暂停rehashif (iter->safe)dictPauseRehashing(iter->d);elseiter->fingerprint = dictFingerprint(iter->d);}// 依次轮询两张hash表,查找非空节点iter->index++;if (iter->index >= (long) DICTHT_SIZE(iter->d->ht_size_exp[iter->table])) {if (dictIsRehashing(iter->d) && iter->table == 0) {iter->table++;iter->index = 0;} else {break;}}iter->entry = iter->d->ht_table[iter->table][iter->index];} else {// 取当前节点的下一个节点iter->entry = iter->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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/59762.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL:表设计

表的设计 从需求中获得类&#xff0c;类对应到数据库中的实体&#xff0c;实体在数据库中表现为一张一张的表&#xff0c;类中的属性就对应着表中的字段&#xff08;也就是表中的列&#xff09; 表设计的三大范式&#xff1a; 在数据库设计中&#xff0c;三大范式&#xff0…

使用 Azure OpenAI 服务对数据进行联合 SharePoint 搜索

作者&#xff1a;来自 Elastic Gustavo Llermaly 使用 Azure OpenAI 服务处理你的数据&#xff0c;并使用 Elastic 作为向量数据库。 在本文中&#xff0c;我们将探索 Azure OpenAI 服务 “On Your Data”&#xff0c;使用 Elasticsearch 作为数据源。我们将使用 Elastic Shar…

chat2db调用ollama实现数据库的操作。

只试了mysql的调用。 其它的我也不用&#xff0c;本来想充钱算了。最后一看单位是美刀。就放弃了这分心。于是折腾了一下。 本地运行chat2db 及chat2db ui https://gitee.com/ooooinfo/Chat2DB clone 后运行起来 chat2db的java端&#xff0c;我现在搞不清这一个项目是有没有…

【搜狐简单AI-注册/登录安全分析报告-无验证方式导致安全隐患】

前言 由于网站注册入口容易被机器执行自动化程序攻击&#xff0c;存在如下风险&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露&#xff0c;不符合国家等级保护的要求。短信盗刷带来的拒绝服务风险 &#xff0c;造成用户无法登陆、注册&#xff0c;大量收到垃圾短信的…

微服务day09

DSL查询 快速入门 GET /items/_search {"query": {"match_all": {}} } 叶子查询 GET /items/_search {"query": {"match_all": {}} }GET /items/_search {"query": {"multi_match": {"query": "脱…

Linux驱动开发第2步_“物理内存”和“虚拟内存”的映射

“新字符设备的GPIO驱动”和“设备树下的GPIO驱动”都要用到寄存器地址&#xff0c;使用“物理内存”和“虚拟内存”映射时&#xff0c;非常不方便&#xff0c;而pinctrl和gpio子系统的GPIO驱动&#xff0c;非常简化。因此&#xff0c;要重点学习pinctrl和gpio子系统下的GPIO驱…

force stop和pm clear的区别

前言&#xff1a;因为工作中遇到force stop和pm clear进程后&#xff0c;进程不能再次挂起&#xff0c;谷歌系统共性问题&#xff0c;服务类应用经清缓存后当下服务就会挂掉&#xff0c;需要系统重启才能恢复。为了更好的“丢锅”&#xff0c;需要进一步学习force stop和pm cle…

【大数据学习 | flume】flume Sink Processors与拦截器Interceptor

1. Failover Sink Processor 故障转移处理器可以同时指定多个sink输出&#xff0c;按照优先级高低进行数据的分发&#xff0c;并具有故障转移能力。 需要修改第一台服务器agent a1.sourcesr1 a1.sinksk1 k2 a1.channelsc1 a1.sources.r1.typenetcat a1.sources.r1.bindworker…

如何从头开始构建神经网络?(附教程)

随着流行的深度学习框架的出现&#xff0c;如 TensorFlow、Keras、PyTorch 以及其他类似库&#xff0c;学习神经网络对于新手来说变得更加便捷。虽然这些框架可以让你在几分钟内解决最复杂的计算任务&#xff0c;但它们并不要求你理解背后所有需求的核心概念和直觉。如果你知道…

Conda安装与使用中的若干问题记录

Conda安装与使用中的若干问题记录 1.Anaconda 安装失败1.1.问题复述1.2.问题解决&#xff08;安装建议&#xff09; 2.虚拟环境pip install未安装至本虚拟环境2.1.问题复述2.2.问题解决 3.待补充 最近由于工作上的原因&#xff0c;要使用到Conda进行虚拟环境的管理&#xff0c;…

『OpenCV-Python』视频的读取和保存

点赞 + 关注 + 收藏 = 学会了 推荐关注 《OpenCV-Python专栏》 上一讲介绍了 OpenCV 的读取图片的方法,这一讲简单聊聊 OpenCV 读取和保存视频。 视频的来源主要有2种,一种是本地视频文件,另一种是实时视频流,比如手机和电脑的摄像头。 要读取这两种视频的方法都是一样的…

字节青训-字符串字符类型排序问题、小C点菜问题

目录 一、字符串字符类型排序问题 题目 样例 输入&#xff1a; 输出&#xff1a; 输入&#xff1a; 输出&#xff1a; 输入&#xff1a; 输出&#xff1a; 解题思路&#xff1a; 问题理解 数据结构选择 算法步骤 最终代码&#xff1a; 运行结果&#xff1a; ​…

深入理解接口测试:实用指南与最佳实践5.0(二)

✨博客主页&#xff1a; https://blog.csdn.net/m0_63815035?typeblog &#x1f497;《博客内容》&#xff1a;.NET、Java.测试开发、Python、Android、Go、Node、Android前端小程序等相关领域知识 &#x1f4e2;博客专栏&#xff1a; https://blog.csdn.net/m0_63815035/cat…

CSS基础知识05(弹性盒子、布局详解,动画,3D转换,calc)

目录 0、弹性盒子、布局 0.1.弹性盒子的基本概念 0.2.弹性盒子的主轴和交叉轴 0.3.弹性盒子的属性 flex-direction row row-reverse column column-reverse flex-wrap nowrap wrap wrap-reverse flex-dirction和flex-wrap的组合简写模式 justify-content flex-s…

任务调度工具Spring Test

Spring Task 是Spring框架提供的任务调度工具&#xff0c;可以按照约定的时间自动执行某个代码逻辑。 作用&#xff1a;定时自动执行某段Java代码 应用场景&#xff1a; 信用卡每月还款提醒 银行贷款每月还款提醒 火车票售票系统处理未支付订单 入职纪念日为用户发送通知 一.…

嵌入式硬件杂谈(二)-芯片输入接入0.1uf电容的本质(退耦电容)

引言&#xff1a;对于嵌入式硬件这个庞大的知识体系而言&#xff0c;太多离散的知识点很容易疏漏&#xff0c;因此对于这些容易忘记甚至不明白的知识点做成一个梳理&#xff0c;供大家参考以及学习&#xff0c;本文主要针对芯片输入接入0.1uf电容的本质的知识点的进行学习。 目…

数据结构(单向链表——c语言实现)

链式存储的优缺点&#xff1a; 优点&#xff1a; 1、动态分配内存&#xff1a; 链式存储不需要在数据插入之前分配固定大小的数组或内存块&#xff0c;因此它更适合存储动态变化的数据 2、高效的插入和删除操作&#xff1a; 在链表中插入或删除元素只需要调整相邻节点的指…

基于Spring Boot的电子商务平台架构

2 相关技术 2.1 SpringBoot框架介绍 Spring Boot是一种不需要代码生成的一种框架&#xff0c;并且可以不需要配置任何的XML文件就可以&#xff0c;因为Spring Boot里面自带了很多接口&#xff0c;只需要配置不同的接口就会自动的应用并且识别需要的依赖&#xff0c;在配置方面非…

Level DB --- Block

class Block class Block是Level DB里面的重要数据结构&#xff0c;该数据结构用来承载已经存储到文件中的数据。已经被存储的数据当需要再次加载、应用&#xff08;例如搜索&#xff09;&#xff0c;这时首先要把数据加载、初始化到class Block里面。 数据的组织形式&#x…

记录大学Linux运维上机考试题目和流程

备注&#xff1a;今年的Linux操作系统考试已经全部结束&#xff0c;仅作为一个记录和留念 前提&#xff1a;配置环回网卡和环境和nat网卡 1、搭建dns服务器 2、Apache和http服务 3、搭建postfix邮件服务器实现邮件发送 4、搭建vsftpdFTP服务器实现文件上传 题目如下&…