redis怎么设计一个高性能hash表
问题
- redis 怎么解决的hash冲突问题 ?
- redis 对于扩容rehash有什么优秀的设计?
hash
目标是解决hash冲突,那什么是hash冲突呢?
实际上,一个最简单的 Hash 表就是一个数组,数组里的每个元素是一个哈希桶(也叫做 Bucket),第一个数组元素被编为哈希桶 0,以此类推。当一个键值对的键经过 Hash 函数计算后,再对数组元素个数取模,就能得到该键值对对应的数组元素位置,也就是第几个哈希桶。下面画几个图来说明下:
上图所示,写入16个键,那么对应的桶只有8个(想一下如果一个桶只能保存一个元素,那么势必会存在数据覆盖),如果写入的key值过多,我们的hash表要怎么处理呢? 事先声明一个很大的hash表嘛,这种肯定是不现实的,不说大小怎么确定,资源也会存在浪费。
那么回过来,我们看下hash冲突,key1 和 key9 都被映射到了 Hash 表的桶 1 中,这样,当桶 5 只能保存一个 key 时,key1 和 key3 就会有一个 key 无法保存到哈希表中了。
看下redis怎么解决hash冲突:总体来说一个是链式hash和渐进式rehash。
链式哈希如何设计与实现?
所谓的链式哈希,就是用一个链表把映射到 Hash 表同一桶中的键给连接起来。下面我们就来看看 Redis 是如何实现链式哈希的,以及为何链式哈希能够帮助解决哈希冲突。
在 dict.h 文件中,Hash 表被定义为一个二维数组(dictEntry **table),这个数组的每个元素是一个指向哈希项(dictEntry)的指针。下面的代码展示的就是在 dict.h 文件中对 Hash 表的定义,你可以看下:
typedef struct dictht {dictEntry **table; //二维数组unsigned long size; //Hash表大小unsigned long sizemask;unsigned long used;
} dictht;
再看dictEntry,一定是会一个当前自己的指针,一个next指针
typedef struct dictEntry {void *key;union {void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next;
} dictEntry;
下面还是拿key1 和 key9 来举例,还是相同的映射流程,采用链式hash的方式画个图就都清楚了
可以看出,当我们查询key9的时候,会先hash(key9)/8 的结果确定桶的位置,再根据链表中的next指针遍历要得到的结果。
想一下,这样会有什么不足?(链表过长的时候,查询复杂度上升)
rehash
hash表的缺点是链表过长,查询效果会下降,那么就要想办法让它的链表存储变短一些。在Redis 中准备了两个哈希表,用于 rehash 时交替保存数据。如图定义:
typedef struct dict {...dictht ht[2]; //两个Hash表,交替使用,用于rehash操作long rehashidx; //Hash表是否在进行rehash的标识,-1表示没有进行rehash...
} dict;
- 其次,在正常服务请求阶段,所有的键值对写入哈希表 ht[0]。
- 接着,当进行 rehash 时,键值对被迁移到哈希表 ht[1]中。
- 最后,当迁移完成后,ht[0]的空间会被释放,并把 ht[1]的地址赋值给 ht[0],ht[1]的表大小设置为 0。这样一来,又回到了正常服务请求的阶段,ht[0]接收和服务请求,ht[1]作为下一次 rehash 时的迁移表。
到这里应该了解怎么进行rehash,保证我们使用的空间足够了,那么有两个问题: 什么时候触发 rehash? rehash 扩容扩多大? rehash 如何执行?
什么时候触发 rehash?
判断是否触发的函数:dictExpandIfNeeded,在里面找一下触发条件
变量值是在 dictEnableResize 和 dictDisableResize作用分别是启用和禁止哈希表执行 rehash 功能
//如果Hash表为空,将Hash表扩为初始大小
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);//如果Hash表承载的元素个数超过其当前大小,并且可以进行扩容,或者Hash表承载的元素个数已是当前大小的5倍
if (d->ht[0].used >= d->ht[0].size &&(dict_can_resize ||d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{return dictExpand(d, d->ht[0].used*2);
}
实际上,_dictExpandIfNeeded 函数中定义了三个扩容条件。
- 条件一:ht[0]的大小为 0。
- 条件二:ht[0]承载的元素个数已经超过了 ht[0]的大小,同时 Hash 表可以进行扩容。
- 条件三:ht[0]承载的元素个数,是 ht[0]的大小的 dict_force_resize_ratio 倍,其中,dict_force_resize_ratio 的默认值是 5。
剩下的就是看下这个dictExpandIfNeeded方法是谁在使用了 ,dictAdd:用来往 Hash 表中添加一个键值对。 dictRelace:用来往 Hash 表中添加一个键值对,或者键值对存在时,修改键值对。 dictAddorFind:直接调用 dictAddRaw。
rehash 扩容扩多大?
int dictExpand(dict *d, unsigned long size);// 当前表的已用空间大小为 size,那么就将表扩容到 size2 的大小。
dictExpand(d, d->ht[0].used*2);
在 Redis 中,rehash 对 Hash 表空间的扩容是通过调用 dictExpand 函数 来完成的。dictExpand 函数的参数有两个,一个是要扩容的 Hash 表,另一个是要扩到的容量
在 dictExpand 函数中,具体执行是由 _dictNextPower 函数完成的,以下代码显示的 Hash 表扩容的操作,就是从 Hash 表的初始大小(DICT_HT_INITIAL_SIZE),不停地乘以 2,直到达到目标大小。
static unsigned long _dictNextPower(unsigned long size)
{//哈希表的初始大小unsigned long i = DICT_HT_INITIAL_SIZE;//如果要扩容的大小已经超过最大值,则返回最大值加1if (size >= LONG_MAX) return LONG_MAX + 1LU;//扩容大小没有超过最大值while(1) {//如果扩容大小大于等于最大值,就返回截至当前扩到的大小if (i >= size)return i;//每一步扩容都在现有大小基础上乘以2i *= 2;}
}
为什么要实现渐进式 rehash?
Hash 表在执行 rehash 时,由于 Hash 表空间扩大,原本映射到某一位置的键可能会被映射到一个新的位置上,因此,很多键就需要从原来的位置拷贝到新的位置。而在键拷贝时,由于 Redis 主线程无法执行其他请求,所以键拷贝会阻塞主线程,这样就会产生 rehash 开销。Redis为了降低这方面的开销,采用了渐进式 rehash 的方法。
简单的说,就是分批来迁移桶内数据,并不会一次性把当前 Hash 表中的所有键,都拷贝到新位置,而是会分批拷贝,每次的键拷贝只拷贝 Hash 表中一个 bucket 中的哈希项。
dictRehash 的主要执行流程:
整理了dictRehash函数的逻辑的核心执行流程:
int dictRehash(dict *d, int n) {int empty_visits = n*10;...//主循环,根据要拷贝的bucket数量n,循环n次后停止或ht[0]中的数据迁移完停止while(n-- && d->ht[0].used != 0) {...}//判断ht[0]的数据是否迁移完成if (d->ht[0].used == 0) {//ht[0]迁移完后,释放ht[0]内存空间zfree(d->ht[0].table);//让ht[0]指向ht[1],以便接受正常的请求d->ht[0] = d->ht[1];//重置ht[1]的大小为0_dictReset(&d->ht[1]);//设置全局哈希表的rehashidx标识为-1,表示rehash结束d->rehashidx = -1;//返回0,表示ht[0]中所有元素都迁移完return 0;}//返回1,表示ht[0]中仍然有元素没有迁移完return 1;
}
需要关注个核心参数:全局哈希表 dict 结构中的 rehashidx 变量相关了。 rehashidx 变量表示的是当前 rehash 在对哪个 bucket 做数据迁移。比如,当 rehashidx 等于 0 时,表示对 ht[0]中的第一个 bucket 进行数据迁移;当 rehashidx 等于 1 时,表示对 ht[0]中的第二个 bucket 进行数据迁移,以此类推。
相关文章:

redis怎么设计一个高性能hash表
问题 redis 怎么解决的hash冲突问题 ?redis 对于扩容rehash有什么优秀的设计? hash 目标是解决hash冲突,那什么是hash冲突呢? 实际上,一个最简单的 Hash 表就是一个数组,数组里的每个元素是一个哈希桶&…...

《软件方法》强化自测题-总纲(6)
DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 按照业务建模、需求、分析、设计工作流考察,答案不直接给出,可访问自测链接或扫二维码自测,做到全对才能知道答案。 知识点见《软件方法》&#x…...

vue2中,下拉框多选和全选的实现
vue2中,下拉框多选和全选的实现 代码布局在methods: 中添加功能函数较为完整的一个整体代码: 如图所示点击全选即可完成下拉框中全部子项的全部的选中,同时取消全选即可全部取消选择。 代码布局 <div class"chos-box2"><…...
Android-Framework 默认音乐音量最大
代码位置:frameworks/base/services/core/java/com/android/server/audio/AudioService.java -712,6 712,9 public class AudioService extends IAudioService.Stub}} // force music max volume AudioSystem.DEFAULT_STREAM_VOLUME[AudioSystem.STREAM_MUSIC] MA…...

formData对象打印不出来
用el-upload上传图片 以流的形式传给后台 所以用formData对象带数据 let formData new FormData() formData.append(name,monkey7) console.log(formData) 明明已经把数据append进去了 console.log在控制台却打印不出 后来发现他得用formData.get("xxx"…...
【Web安全】SQL注入攻击几种常见防御手法总结
文章目录 前言一、使用参数化查询二、输入验证和过滤三、使用存储过程四、最小权限原则五、使用ORM框架六、使用准备语句七、使用安全的数据库连接八、避免动态拼接SQL语句九、使用防火墙和入侵检测系统(一)防火墙(二)入侵检测系统(Intrusion Detection System,简称IDS)十、定期…...
Linux网络编程杂谈(聊聊网络编程背后的故事)
数据是如何传输到物理网络上的? 以TCP为例,当 TCP 决定发送数据时,这些数据需要经过多个处理阶段才能真正被传输到物理网络。其中一个关键步骤是将数据移动到网络接口卡 (NIC)。以下是这个过程的详细描述: 数据序列化: TCP 会为要…...
执行Maven项目时,无法解析项目的依赖关系
报错[ERROR] Failed to execute goal on project pdms-services: Could not resolve dependencies for project ..... 在IDEA ----> setting ---->Remote Jar Repositories ----> Maven jar repositories中添加远程仓库的http地址。 再次进行maven的clean和install就好…...
索引有哪些缺点以及具体有哪些索引类型
索引的优缺点 优点: 合理的增加索引,可以提高数据查询的效率,减少查询时间 有一些特殊的索引,可以保证数据的完整性,比如唯一索引 缺点: 创建索引和维护索引需要消耗时间 索引需要额外占用物理空间 对创建…...

前端学成在线项目详细解析二
12-banner区域-课程表布局 HTML布局 <div class"right"><h3>我的课程表</h3><div class"content">1</div> </div> CSS样式 /* 课程表 */ .banner .right {margin-top: 60px;width: 218px;height: 305px;background-…...
Linux 网卡性能优化设置
在高速网络传输中,每秒传输的数据量非常大。网络设备设置有一种缓存机制,即“缓存区”,在 Linux 系统中,网卡缓冲分为两种类型:软件缓冲区和硬件缓冲区。 要提高网络吞吐率,首先当然是升级linux kernel。其…...
华为OD 最大嵌套括号深度(100分)【java】B卷
华为OD统一考试A卷+B卷 新题库说明 你收到的链接上面会标注A卷还是B卷。目前大部分收到的都是B卷。 B卷对应20022部分考题以及新出的题目,A卷对应的是新出的题目。 我将持续更新最新题目 获取更多免费题目可前往夸克网盘下载,请点击以下链接进入: 我用夸克网盘分享了「华为O…...

“微信小程序登录与用户信息获取详解“
目录 引言微信小程序微信登录介绍1. 微信登录的基本概念2. 微信小程序中的微信登录 微信小程序登录的wxLogin与getUserProfile的区别1. wx.login()2. wx.getUserProfile()3.两者区别 微信小程序登录的理论概念1. 微信登录流程2. 用户授权与登录态维护 微信小程序登录的代码演示…...
软考-防火墙技术与原理
本文为作者学习文章,按作者习惯写成,如有错误或需要追加内容请留言(不喜勿喷) 本文为追加文章,后期慢慢追加 by 2023年10月 防火墙概念 根据网络的安全信任程度和需要保护的对象,人为地划分若干安全区域…...
MOS管型号
MOS 管型号 N型 型号类型电压电流Rds封装资料AP60N03DFN30V60A45mΩPDFN3x3-8L手册 P型 型号类型电压电流Rds封装资料AO4447AP30V-18.5A8.2mΩSOIC-8手册 NP型 型号类型电压电流Rds封装资料NP4606PN30V7A、-6A45mΩSOP8手册KS3640MBPN30V20A、-22A45mΩPDFN3333手册NCE30…...
龙测票选,5本最受欢迎的软件测试书籍
随着技术的发展,软件测试所涉猎的领域越来越广泛,包括测试理论、方法、管理、工具等,一直在随之变化。对新手来说,这时候需要有一个引路明灯,避免走弯路,提高学习效率。而书籍就扮演着这样的角色。一本好的…...

C#中各种循环遍历的功能与应用
在C#编程中,循环遍历是一种重要的技巧,它使我们能够有效地处理集合、数组和其他数据结构。本文将深入探讨C#中常见的循环遍历方式,包括for循环、foreach循环、while循环和do while循环,并给出它们在实际应用中的使用场景、示例和最…...

【必看技巧】Access开发者必备:如何用代码隐藏功能区、导航区、状态栏?
hi,大家好呀! 今天想着给大家分享点啥呢?最近几个月断更的有些“勤快”了,那就给大家分享个几行代码。 当我们在access中开发完成后,为了让我们的系统更加的像一个系统,我们会把access的功能区࿰…...

领先一步,效率翻倍:PieCloudDB Database 预聚集特性让查询速度飞起来!
在大数据时代,如何有效地管理和处理海量数据成为了企业面临的核心挑战。为此,拓数派推出了首款数据计算引擎 PieCloudDB Database,作为一款全新的云原生虚拟数仓,旨在提供更高效、更灵活的数据处理解决方案。 PieCloudDB 的设计理…...

系统性认知网络安全
前言:本文旨在介绍网络安全相关基础知识体系和框架 目录 一.信息安全概述 信息安全研究内容及关系 信息安全的基本要求 保密性Confidentiality: 完整性Integrity: 可用性Availability: 二.信息安全的发展 20世纪60年代&…...

测试微信模版消息推送
进入“开发接口管理”--“公众平台测试账号”,无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息: 关注测试号:扫二维码关注测试号。 发送模版消息: import requests da…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...
【解密LSTM、GRU如何解决传统RNN梯度消失问题】
解密LSTM与GRU:如何让RNN变得更聪明? 在深度学习的世界里,循环神经网络(RNN)以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而,传统RNN存在的一个严重问题——梯度消失&#…...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...