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;
}
相关文章:
redis7.x源码分析:(3) dict字典
dict字典采用经典hash表数据结构实现,由键值对组成,类似于C中的unordered_map。两者在代码实现层面存在一些差异,比如gnustl的unordered_map分配的桶数组个数是(质数n),而dict分配的桶数组个数是࿰…...

连续九届EI稳定|江苏科技大学主办
【九届EI检索稳定|江苏科技大学主办 | IEEE出版 】 🎈【截稿倒计时】!!! ✨徐秘书:gsra_huang ✨往届均已检索,已上线IEEE官网 🎊第九届清洁能源与发电技术国际学术会议(CEPGT 2…...

HarmonyOS NEXT应用开发实战 ( 应用的签名、打包上架,各种证书详解)
前言 没经历过的童鞋,首次对HarmonyOS的应用签名打包上架可能感觉繁琐。需要各种秘钥证书生成和申请,混在一起也分不清。其实搞清楚后也就那会事,各个文件都有它存在的作用。 HarmonyOS通过数字证书与Profile文件等签名信息来保证鸿蒙应用/…...

【CICD】CICD 持续集成与持续交付在测试中的应用
一、什么是CICD? CI/CD 是指持续集成(Continuous Integration)和持续部署(Continuous Deployment)或持续交付(Continuous Delivery) 1.1 持续集成(Continuous Integration…...

Dolby TrueHD和Dolby Digital Plus (E-AC-3)编码介绍
文章目录 1. Dolby TrueHD特点总结 2. Dolby Digital Plus (E-AC-3)特点总结 Dolby TrueHD 与 Dolby Digital Plus (E-AC-3) 的对比 Dolby TrueHD和Dolby Digital Plus (E-AC-3) 是两种高级的杜比音频编码格式,常用于蓝光影碟、流媒体、影院等高品质音频传输场景。它…...

数字频率计的设计-- 基于 HDL 方法
目录 数字频率计的设计 1.计数、锁存与显示译码电路设计 2.主控电路设计 3.分频电路设计 4.顶层电路设计 伪随机序列发生器 的设计 数字频率计的设计 基于HDL设计数字系统时,可以根据需要应用Verilog HDL描述所需要的功能电路,既有利于节约资源&am…...
[程序员] 没有产生core文件的原因
最近和同事一块看一个core文件没有产生的问题,总结了一些在CSDN的专栏里。分析的过程,参考使用了ftrace的功能,感觉非常实用。 如果有需要可以参考。大体上就这么几种情况:信号的特殊处理,coredump相关的配置没有设置正确,文件系统访问权限问题,setuid相关的不匹配问题。…...

【数字图像处理+MATLAB】基于 Sobel 算子计算图像梯度并进行边缘增强:使用 imgradientxy 函数
引言 在图像处理中,边缘通常是图像中像素强度变化最大的地方,这种变化可以通过计算图像的梯度来量化。梯度是一个向量,它的方向指向像素强度增加最快的方向,它的大小(或者说幅度)表示像素强度增加的速度。…...

P10901 [蓝桥杯 2024 省 C] 封闭图形个数
铁子们好呀,今天博主给大家更新一道编程题!!! 题目链接如下: P10901 [蓝桥杯 2024 省 C] 封闭图形个数 好,接下来,我将从三个方面讲解这道例题。分别是 题目解析算法原理代码实现 文章目录 1.题…...

ubuntu-desktop-24.04上手指南(更新阿里源、安装ssh、安装chrome、设置固定IP、安装搜狗输入法)
ubuntu-desktop-24.04上手指南(更新阿里源、安装ssh、安装chrome、设置固定IP、安装搜狗输入法) 一、更新并安装基础软件 #切换root用户 sudo su -#更新 apt update #升级 apt upgrade#install vim apt install vim#install net-tools apt install net-tools二、安装ssh并设置…...

手机直连卫星NTN通信初步研究
目录 1、手机直连卫星之序幕 2、卫星NTN及其网络架构 2.1 NTN 2.2 NTN网络架构 3、NTN的3GPP标准化进程 3.1 NTN需要适应的特性 3.2 NTN频段 3.3 NTN的3GPP标准化进程概况 3.4 NTN的3GPP标准化进程的详情 3.4.1 NR-NTN 3.4.1.1 NTN 的无线相关 SI/WI 3.4.1.2…...

蓝桥杯c++算法学习【2】之搜索与查找(九宫格、穿越雷区、迷宫与陷阱、扫地机器人:::非常典型的必刷例题!!!)
别忘了请点个赞收藏关注支持一下博主喵!!! 关注博主,更多蓝桥杯nice题目静待更新:) 搜索与查找 一、九宫格 【问题描述】 小明最近在教邻居家的小朋友小学奥数,而最近正好讲述到了三阶幻方这个部分,三 …...
Android加载pdf
依赖 implementation com.squareup.okhttp3:okhttp:4.9.1 implementation com.github.barteksc:android-pdf-viewer:3.2.0-beta.1在project.build中添加该源 maven { url "https://repository.liferay.com/nexus/content/repositories/public/" }XML <LinearLa…...

IOT物联网低代码可视化大屏解决方案汇总
目录 参考来源云服务商阿里云物联网平台产品主页产品文档 开源项目DGIOT | 轻量级工业物联网开源平台项目特点项目地址开源许可 IoTGateway | 基于.NET6的跨平台工业物联网网关项目特点项目地址开源许可 IoTSharp | 基于.Net Core开源的物联网基础平台项目特点项目地址开源许可…...

Python的面向对象day7
1、什么是面向对象 面向对象称为OO,他通过将数据和功能封装在一个被称为‘对象’的实体中,来组织和管理代码。面向对象变成(OOP)具有四个特性,封装、继承、多态、抽象 优点:模块化、安全性高、代码重用性…...

计算机网络(11)和流量控制补充
这一篇对数据链路层中的和流量控制进行详细学习 流量控制(Flow Control)是计算机网络中确保数据流平稳传输的技术,旨在防止数据发送方发送过多数据,导致接收方的缓冲区溢出,进而造成数据丢失或传输失败。流量控制通常…...
Rust 所有权机制
Rust 所有权机制 本文示例代码地址 所有权是Rust中最独特的特性,它让Rust无需GC就可以保证内存安全。 什么是所有权? 所有权(ownership)是 Rust 用于如何管理内存的一组规则。所有程序都必须管理其运行时使用计算机内存的方式…...
Pwn VM writeup
国赛期间,做了一个很有意思的pwn题,顺便学了一下现在常见的pwn的板子题是什么样子的,这里做一下记录 Magic VM 题目逻辑 题目本身其实非常的有趣,它实现了一个简易流水线的功能,程序中包含四个结构体,其中三…...

LSTM(长短期记忆网络)详解
1️⃣ LSTM介绍 标准的RNN存在梯度消失和梯度爆炸问题,无法捕捉长期依赖关系。那么如何理解这个长期依赖关系呢? 例如,有一个语言模型基于先前的词来预测下一个词,我们有一句话 “the clouds are in the sky”,基于&…...

机器学习 贝叶斯公式
这是条件概率的计算公式 𝑃(𝐴|𝐵)𝑃(B|A)𝑃(𝐴)/𝑃(𝐵) 全概率公式 𝑃(𝐵)𝑃(𝐵|𝐴)𝑃(𝐴)&am…...

SpringBoot-17-MyBatis动态SQL标签之常用标签
文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...
Admin.Net中的消息通信SignalR解释
定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...

嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...
现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?
现有的 Redis 分布式锁库(如 Redisson)相比于开发者自己基于 Redis 命令(如 SETNX, EXPIRE, DEL)手动实现分布式锁,提供了巨大的便利性和健壮性。主要体现在以下几个方面: 原子性保证 (Atomicity)ÿ…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...

macOS 终端智能代理检测
🧠 终端智能代理检测:自动判断是否需要设置代理访问 GitHub 在开发中,使用 GitHub 是非常常见的需求。但有时候我们会发现某些命令失败、插件无法更新,例如: fatal: unable to access https://github.com/ohmyzsh/oh…...