Redis源码精读:哈希表

文章目录

  • 前言
  • 代码位置
  • 哈希表
  • 核心代码
  • rehash
  • 最后

前言

哈希表是Redis中非常重要的数据结构,这篇博客我们就一起来探索一下Redis中哈希表的奥秘😁

代码位置

src/dict.h
src/dict.c

哈希表

  • 原理

哈希表用于键值对的存储和查找,通过哈希函数将键映射到一个的索引上,来保存相应的值

  • 优势

哈希表的优势是增删改查的时间复杂度都是O(1),哈希表在大量的数据中也能保持良好的性能,因为哈希函数会将键均匀地分散在整个数组中

  • 问题

多个键通过哈希函数映射到同一个索引时,就会产生哈希冲突

  • 解决问题

常见的解决哈希冲突的方法有链式哈希法或开放寻址法

在链式哈希法中,每个索引位置上都存储一个桶,每个桶是个链表,用来链接冲突的键值对

在开放寻址法中,当发生冲突时,会继续向后探测数组,直到找到一个空闲的位置来存储冲突的键值对

  • redis选型

redis使用了链式哈希法来实现hash表,使用渐进式 rehash 方法来减少哈希冲突

  • rehash

就是创建一个更大的hash表,然后将原本的hash表迁移过去,因为新hash表更大,造成哈希冲突的几率也就更小

  • 渐进式rehash

因为rehash操作需要迁移整个hash表,代价很大,我们可以在不影响redis对外正常服务的情况下逐步的进行迁移
在迁移过程中新的数据写入只会发生在新哈希表中,旧哈希表仅用于读取操作。这样可以避免写入操作复杂度的增加
当所有数据都完成迁移后,Redis会将新哈希表替换旧哈希表,完成rehash过程

核心代码

// 哈希表的行为
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);// hash表扩展int (*expandAllowed)(size_t moreMem, double usedRatio);// 开启rehash,此时新旧hash表已经创建过了void (*rehashingStarted)(dict *d);// rehash完成后的钩子函数,通常是一些清理工作,比如释放临时分配的内存或者更新哈希表的状态信息void (*rehashingCompleted)(dict *d);// 获取hash表中元数据所占用的字节数size_t (*dictMetadataBytes)(dict *d);// 标识是否使用值unsigned int no_value:1;/* If no_value = 1 and all keys are odd (LSB=1), setting keys_are_odd = 1* enables one more optimization: to store a key without an allocated* dictEntry. */// 如果 no_value = 1,且所有键都是奇数,则设置keys_are_odd = 1可以启用优化:存储未分配dictEntry的键unsigned int keys_are_odd:1;/* TODO: Add a 'keys_are_even' flag and use a similar optimization if that* flag is set. */
} dictType;
// 哈希表中的元素
struct dictEntry {// 键void *key;// 值,小技巧:如果值是uint64_t、int64_t、double中的,就直接存储对应内容,无需使用指针,减少内存开销union {void *val;uint64_t u64;int64_t s64;double d;} v;// 下一个元素struct dictEntry *next;
};
// 哈希表
struct dict {// 指定hash表的行为dictType *type;// 两张hash表,在rehash时交替使用,每张hash表里面是二维的dictEntrydictEntry **ht_table[2];// 两张哈希表中键值对的使用数量unsigned long ht_used[2];// 标识是否正在进行rehash, -1表示没有进行rehashlong rehashidx;// 标识是否暂停rehash,>0表示暂停rehash,<0表示编码错误int16_t pauserehash;// 大小的指数,size = 1 << expsigned char ht_size_exp[2];// 元数据void *metadata[];
};

以上实现我们不难看出,如果哈希冲突过多会使dictEntry链表变长,导致操作该位置的hash表在性能上减弱

rehash

rehash是扩充hash表的一个操作,它可以减少哈希冲突的概率,Redis中rehash操作是渐进式的,当触发rehash操作时,逐渐地将旧hash表的数据放入新hash表中,最终当数据转移完成之后旧hash表的空间会被释放

// 如果需要进行扩容
static int _dictExpandIfNeeded(dict *d)
{// 如果已经在进行rehash操作就直接退出if (dictIsRehashing(d)) return DICT_OK;// 若hash表为空,就扩容成初始大小if (DICTHT_SIZE(d->ht_size_exp[0]) == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);// 启用了rehash且哈希表的大小达到或超过当前容量 或者 未禁止rehash且当前hash表的使用率大于比率阈值if ((dict_can_resize == DICT_RESIZE_ENABLE &&d->ht_used[0] >= DICTHT_SIZE(d->ht_size_exp[0])) ||(dict_can_resize != DICT_RESIZE_FORBID &&d->ht_used[0] / DICTHT_SIZE(d->ht_size_exp[0]) > dict_force_resize_ratio)){/// 如果hash表中表示已经扩容过,就退出if (!dictTypeExpandAllowed(d))return DICT_OK;// 触发扩容return dictExpand(d, d->ht_used[0] + 1);}return DICT_OK;
}
// 扩容
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{if (malloc_failed) *malloc_failed = 0;// 如果正在进行rehash 或者 当前使用的hash表的大小大于将要分配的容量,直接退出if (dictIsRehashing(d) || d->ht_used[0] > size)return DICT_ERR;// 新hash表dictEntry **new_ht_table;// 新hash表中元素的使用数量unsigned long new_ht_used;// 新hash表的大小指数signed char new_ht_size_exp = _dictNextExp(size);// 计算新hash表的大小size_t newsize = DICTHT_SIZE(new_ht_size_exp);// 如果新大小不够,则直接返回if (newsize < size || newsize * sizeof(dictEntry*) < newsize)return DICT_ERR;// 若大小指数未变化,则返回if (new_ht_size_exp == d->ht_size_exp[0]) return DICT_ERR;if (malloc_failed) {// 检查分配是否会失败// 尝试进行分配新的hash表new_ht_table = ztrycalloc(newsize*sizeof(dictEntry*));// 标识分配是否失败*malloc_failed = new_ht_table == NULL;if (*malloc_failed)// 分配失败,直接返回return DICT_ERR;} else// 分配新的hash表new_ht_table = zcalloc(newsize*sizeof(dictEntry*));// 新的hash表使用量为0new_ht_used = 0;// 新hash表初始化d->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;// 执行rehash启动的钩子函数if (d->type->rehashingStarted) d->type->rehashingStarted(d);// 如果hash表为空if (d->ht_table[0] == NULL || d->ht_used[0] == 0) {// 执行rehash结束后的钩子函数if (d->type->rehashingCompleted) d->type->rehashingCompleted(d);// 如果旧hash表未释放,则释放掉if (d->ht_table[0]) zfree(d->ht_table[0]);// 旧hash表重新指向分配并迁移完成的新hash表d->ht_size_exp[0] = new_ht_size_exp;d->ht_used[0] = new_ht_used;d->ht_table[0] = new_ht_table;// 重置hash表,将旧表中的所有元素都会被释放,确保新的哈希表不会包含旧的元素,保证哈希表的效率和一致性_dictReset(d, 1);// 标志rehash结束d->rehashidx = -1;return DICT_OK;}return DICT_OK;
}// rehash,不考虑内存分配是否成功
int dictExpand(dict *d, unsigned long size) {return _dictExpand(d, size, NULL);
}// rehash,考虑内存分配是否成功
int dictTryExpand(dict *d, unsigned long size) {int malloc_failed;_dictExpand(d, size, &malloc_failed);return malloc_failed? DICT_ERR : DICT_OK;
}
int dictRehash(dict *d, int n) {// 最多访问n*10个空桶int empty_visits = n*10;// hash表0的大小unsigned long s0 = DICTHT_SIZE(d->ht_size_exp[0]);// hash表1的大小unsigned long s1 = DICTHT_SIZE(d->ht_size_exp[1]);// 若禁止resize或者未进行rehash,则返回if (dict_can_resize == DICT_RESIZE_FORBID || !dictIsRehashing(d)) return 0;// 如果避免resize,且s1大于s0且s1 / s0的比率小于resize的阈值 或者 s1小于s0且s0 / s1的比率小于resize的阈值,则返回if (dict_can_resize == DICT_RESIZE_AVOID && ((s1 > s0 && s1 / s0 < dict_force_resize_ratio) ||(s1 < s0 && s0 / s1 < dict_force_resize_ratio))){return 0;}// 主循环,根据要拷贝的bucket数量n,循环n次后停止或ht[0]中的数据迁移完停止while(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);// 遍历旧hash表,找到第一个不为空的桶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 */// 遍历该桶while(de) {// 新节点在新hash表中的索引位置uint64_t h;// 保存下一个哈希节点的指针,因为重新散列过程中当前哈希节点可能会被释放或者重新分配位置nextde = dictGetNext(de);// 获取该节点的keyvoid *key = dictGetKey(de);// 计算新哈希节点在新哈希表中的索引位置if (d->ht_size_exp[1] > d->ht_size_exp[0]) {h = dictHashKey(d, key) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);} else {h = d->rehashidx & DICTHT_SIZE_MASK(d->ht_size_exp[1]);}// 判断当前节点是否存有valueif (d->type->no_value) {// 若所有键都是奇数且新hash表在h位置的桶不存在,则存储未分配dictEntry的键if (d->type->keys_are_odd && !d->ht_table[1][h]) {assert(entryIsKey(key));// 若当前节点是dictEntry,则进行优化,只存储未分配dictEntry的键if (!entryIsKey(de)) zfree(decodeMaskedPtr(de));de = key;} else if (entryIsKey(de)) { // 判断当前节点是否只存在键/* We don't have an allocated entry but we need one. */// 只存储未分配dictEntry的键de = createEntryNoValue(key, d->ht_table[1][h]);} else {/* Just move the existing entry to the destination table and* update the 'next' field. */assert(entryIsNoValue(de));// 将当前节点的下一个指针指向新hash表中h槽位的头指针dictSetNext(de, d->ht_table[1][h]);}} else {// 将当前节点的下一个指针指向新hash表中h槽位的头指针dictSetNext(de, d->ht_table[1][h]);}// 设置新hash表中h槽位的元素为当前的桶d->ht_table[1][h] = de;// 更新两个哈希表的节点数量d->ht_used[0]--;d->ht_used[1]++;// de指向下一个节点,用于下次循环de = nextde;}// 从旧hash表中移除当前元素d->ht_table[0][d->rehashidx] = NULL;// 遍历下一个元素d->rehashidx++;}// 检查是否已经完成整个hash表的rehash了if (d->ht_used[0] == 0) {if (d->type->rehashingCompleted) d->type->rehashingCompleted(d);// 释放旧hash表zfree(d->ht_table[0]);// 让旧hash表指向新hash表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);// 关闭hash表的渐进式 rehash 标志d->rehashidx = -1;// 返回0,表示rehash完成return 0;}// 返回1,表示rehash未完成return 1;
}

最后

恭喜我们一起看完了redis中哈希表的核心源码,渐进式rehash的源码,希望你能有所收获😉

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

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

相关文章

网传鸿蒙月薪达到40-70K,是真是假......

据消息称&#xff0c;华为将于明年发布不兼容安卓的鸿蒙版本&#xff0c;这意味着未来鸿蒙将独立开发&#xff0c;成为华为的核心操作系统。 现在提出观点一和观点二供大家讨论&#xff1a; 观点一 认为鸿蒙不再兼容安卓&#xff0c;会导致华为失去大量用户和市场份额 观点…

liunx安装Docker Compose

你可以按照以下步骤安装 Docker Compose&#xff1a; 首先&#xff0c;确保你已经安装了 Docker。Docker Compose 是 Docker 的一个独立组件&#xff0c;通常不会随 Docker 一起安装。 1&#xff0c;使用以下命令下载 Docker Compose 的二进制文件&#xff1a; bash sudo c…

Autosar CAN开发05(从实际应用认识CAN波特率)

建议同时阅读本专栏的&#xff1a; Autosar CAN开发03&#xff08;从实际应用认识CAN总线的物理层&#xff09; Autosar CAN开发04&#xff08;从实际应用认识CAN报文&#xff09; Autosar CAN开发05&#xff08;从实际应用认识CAN波特率&#xff09; 前言 当知道了CAN的物…

R语言【dplyr】——arrange() 按所选列的值对数据集的行重新排序

Package dplyr version 1.1.4 Parameters arrange(.data, ..., .by_group FALSE)## S3 method for class data.frame arrange(.data, ..., .by_group FALSE, .locale NULL) 参数【.data】&#xff1a;数据集、数据集扩展&#xff08;如 tibble&#xff09;或 lazy data fr…

Spring 依赖注入概述、使用以及原理解析

前言 源码在我github的guide-spring仓库中&#xff0c;可以克隆下来 直接执行。 我们本文主要来介绍依赖注入的使用示例及其原理 依赖注入 什么是依赖注入 依赖注入&#xff08;Dependency Injection&#xff0c;简称DI&#xff09;是一种设计模式&#xff0c;它用于实现对…

嵌入式 C 语言大神的进阶之路

C语言可以说是一种"古老"的编程语言&#xff0c;也是目前嵌入式中主流的编程语言&#xff0c;没有C语言就没有今天的各种嵌入式系统以及操作系统等等。 C语言虽然说是编程开发的基础&#xff0c;那到底你掌握到了什么程度呢&#xff1f; 下面我们一起看看C语言熟练到…

第十三节TypeScript 元组

1、简介 我们知道数组中元素的数据类型一般都是相同的&#xff08;any[]类型的数组可以不同&#xff09;&#xff0c;如果存储的元素类型不同&#xff0c;则需要使用元组。 元组中允许存储不同类型的元素&#xff0c;元组可以作为参数传递给函数。2、创建元组的语法格式&#x…

python:改进型鳟海鞘算法(SSALEO)求解23个基本函数

一、改进型鳟海鞘算法SSALEO 改进型鳟海鞘算法&#xff08;SSALEO&#xff09;由Mohammed Qaraad等人于2022年提出。 参考文献&#xff1a;M. Qaraad, S. Amjad, N. K. Hussein, S. Mirjalili, N. B. Halima and M. A. Elhosseini, "Comparing SSALEO as a Scalable Larg…

阻抗控制中的弹簧与阻尼影响分析

阻抗控制是一种机器人控制方法&#xff0c;通过调整机器人的阻抗来实现对机器人的精准控制。在阻抗控制中&#xff0c;弹簧和阻尼是两个重要的参数&#xff0c;它们对机器人的性能和稳定性有很大的影响。 弹簧代表机器人的刚度和弹性&#xff0c;而阻尼代表机器人的阻尼特性&a…

rabbitmq的事务实现、消费者的事务实现

RabbitMQ提供了事务机制&#xff0c;可以确保消息在发送和确认过程中的一致性。使用事务机制可以将一系列的消息操作&#xff08;发送、确认、回滚&#xff09;作为一个原子操作&#xff0c;要么全部执行成功&#xff0c;要么全部回滚。 下面是使用RabbitMQ事务的一般步骤&…

DRF从入门到精通四(视图基类、GenericAPIView的视图扩展类、视图子类、视图集父类、子类)

文章目录 前言一、视图基类APIView基类GenericAPIView通用视图基类 二、GenericAPIView的视图拓展类1.ListModelMixin2.CreateModelMixin3.RetrieveModelMixin4.UpdateModelMixin5.DestroyModelMixin 三、GenericAPIView的视图子类ListCreateAPIViewRetrieveUpdateDestroyAPIVi…

中庸 原文与译文

《中庸》是中国古代论述人生修养境界的一部道德哲学专著&#xff0c;是儒家经典著作之一&#xff0c;原属《礼记》第三十一篇&#xff0c;相传为战国时期子思所作。 其内容肯定“中庸”是道德行为的最高标准&#xff0c;认为“至诚”则达到人生的最高境界&#xff0c;并提出“…

C语言中关于if else的理解

if else我们可以理解为 if(条件1) //如果条件1成立 语句1&#xff1b; //执行语句1 else //如果条件1不成立 语句2; //执行语句2 这是一个经典的if els…

大数据技术学习笔记(十一)—— Flume

目录 1 Flume 概述1.1 Flume 定义1.2 Flume 基础架构 2 Flume 安装3 Flume 入门案例3.1 监控端口数据3.2 实时监控单个追加文件3.3 实时监控目录下多个新文件3.4 实时监控目录下的多个追加文件 4 Flume 进阶4.1 Flume 事务4.2 Flume Agent 内部原理4.3 Flume 拓扑结构4.3.1 简单…

1861_什么是H桥

Grey 全部学习内容汇总&#xff1a; GitHub - GreyZhang/g_hardware_basic: You should learn some hardware design knowledge in case hardware engineer would ask you to prove your software is right when their hardware design is wrong! 1861_什么是H桥 H桥电路可以…

蓝桥杯c/c++程序设计——数位排序

数位排序【第十三届】【省赛】【C组】 题目描述 小蓝对一个数的数位之和很感兴趣&#xff0c;今天他要按照数位之和给数排序。 当两个数各个数位之和不同时&#xff0c;将数位和较小的排在前面&#xff0c;当数位之和相等时&#xff0c;将数值小的排在前面。 例如&#xff0…

reactive和TypeScript标注数据类型-ts使用方法

一、vite项目中<script setup lang"ts"> : lang"ts" 是表明支持ts校验&#xff08;ts 全称typescript,是es6语法&#xff0c;是javascript的超集强类型编程语言&#xff0c;类似java&#xff0c;定义变量类型后&#xff0c;赋值类型不一致&#xff0…

Cesium.js相关官网或博客

Cesium.JS Cesium: The Platform for 3D Geospatial Cesium API 中文&#xff1a;Cesium中文api文档 | Index - Cesium Documentation 英文&#xff1a;Index - Cesium Documentation Cesium中文网&#xff08;私人博客&#xff09; http://cesium.xin 天地图&#xff08;三维…

C中的“volatile”限定符

1、概述 尽管有大量C语言的文献&#xff0c;但是 “volatile” 关键字在某种程度上还是不能被很好地理解&#xff08;甚至是有经验的C程序员&#xff09;。究其原因&#xff0c;是在用高级语言编写的典型C程序中&#xff0c;没有 “volatile” 变量的真实用例。基本上&#xf…

创建一台可以安装linux系统的虚拟机的流程

1、打开vmware-->点击左上角文件-->新建虚拟机-->自定义 2、默认选择&#xff0c;直接下一步 3、选中稍后安装操作系统&#xff0c;然后下一步 4、选中Linux&#xff0c;然后下拉框选择CentOS7(64位) 5、设置虚拟机名称及存储位置 6、设置虚拟机处理器数量及核心数 7、…