Redis源码学习:高性能Hash表的设计与实现
哈希表(Hash)是Redis数据库的数据类型之一,理解哈希表的实现对于掌握Redis非常重要。这篇文章,从哈希冲突和哈希扩展这两个角度,来一步步讲解Redis哈希表的工作原理。
什么是哈希表?
哈希表是一种通过哈希函数将键映射到值的数据结构。简单来说,就是通过一个计算公式(哈希函数)把一个键(比如一个名字)转换成一个数组的索引,数组中的每一个元素就是一个哈希桶(也叫bucket),然后在这个索引位置存储对应的值(比如电话号码)。这样我们就能以O(1)的时间复杂度通过键快速找到对应的值。
哈希冲突
哈希冲突是什么?
当键的数量超过数组的大小,必然会出现两个不同的键通过哈希函数映射到数组的同一个位置时,就发生了哈希冲突。举个例子,如果我们把“Tom”和“Jerry”这两个名字通过同一个公式转换成同一个数组位置,这时候就会有冲突。
如何解决哈希冲突?
Redis使用链式哈希来解决这个问题。链式哈希的意思是,每个bucket不再只存一个值,而是变成一个链表的头指针。如果有冲突,新来的键值对就插到链表头中。这样,同一个位置上可以存多个键值对。
Redis中的链式哈希
在Redis使用链式哈希解决哈希冲突,每个bucket指向一个链表,源码(位于dict.h文件)如下:
// 哈希表的定义
typedef struct dictht {// 哈希项数组,保存指向哈希项的指针dictEntry **table;// 哈希表的大小unsigned long size;// 哈希表小的掩码,总是等于 size -1 unsigned long sizemask;// 哈希项的数量,因为有哈希冲突的存在,used可能会比size大unsigned long used;
} dictht;// 哈希项的定义
typedef struct dictEntry {void *key; // 键union {void *val;uint64_t u64;int64_t s64;double d;} v; // 值struct dictEntry *next; // 指向下一个条目的指针(用于链式哈希)
} dictEntry;
每个dictEntry结构包含一个键key,一个值v,以及一个指向下一个条目的指针next。
联合体
v是一个四选一的类型,包含了指向实际值的指针val,还包含了无符号的 64 位整数、有符号的 64 位整数,以及 double 类的值。这是一种节省内存的设计。当值为整数或双精度浮点数时,由于其本身就是 64 位,就可以不用指针指向了,而是可以直接存在键值对的结构体中,这样就避免了再用一个指针,从而节省了内存空间。
插入键值对时,如果发生冲突,新条目将插入到链表的头部。采用头插法的好处就是性能高,不需要遍历链表了。
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) {// 进行rehash检查if (dictIsRehashing(d)) _dictRehashStep(d);unsigned long idx = dictHashKey(d, key) & ht->sizemask;dictEntry *entry = ht->table[idx];while (entry) {if (dictCompareKeys(d, key, entry->key)) {if (existing) *existing = entry;return NULL;}entry = entry->next;}entry = zmalloc(sizeof(*entry));entry->next = ht->table[idx];ht->table[idx] = entry;ht->used++;dictSetKey(d, entry, key);return entry;
}
哈希扩展
为什么需要哈希扩展?
随着哈希表中存储的键值对越来越多,哈希冲突变得越来越频繁,哈希表的效率会降低。为了保持高效的性能,需要在恰当的扩展哈希表,也就是增加哈希表的大小。这个过程称为哈希扩展或rehash。
渐进式rehash
Redis采用渐进式rehash策略,以避免扩展过程中阻塞数据库的正常操作。也就是说,Redis不会一次性把所有数据搬到新的哈希表中,而是分多次慢慢进行,每次只处理一个bucket数据。
rehash的实现
Redis在dict结构中,定义了两个哈希表,用于 rehash 时交替保存数据。
typedef struct dict {dictType *type;void *privdata;dictht ht[2]; //两个Hash表,交替使用,用于rehash操作long rehashidx; //Hash表是否在进行rehash的标识,-1表示没有进行rehashint16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;
每次rehash只处理一个bucket的链表,这样就不会长时间阻塞数据库的其他操作。调用dictRehash函数,传入的参数n始终为1,这样一来,每次迁移完一个bucket,哈希表就会执行正常的增删查请求操作,这就是在代码层面实现渐进式 rehash 的方法。
以下是相关的源码片段:
void _dictRehashStep(dict *d) {if (d->iterators == 0) dictRehash(d, 1);
}int dictRehash(dict *d, int n) {if (!dictIsRehashing(d)) return 0;while (n--) {dictEntry *de, *nextde;/* 如果ht[0]迁移完 */if (d->ht[0].used == 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]);/* 值改完1,表示rehash结束 */d->rehashidx = -1;/* 返回0,表示ht[0]中所有元素都迁移完成 */return 0;}/* 当前正在迁移的桶*/while (d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;/* 哈希表中的哈希项 */de = d->ht[0].table[d->rehashidx];while (de) {unsigned long h;/* 获得下一个哈希项 */nextde = de->next;/* 当前哈希项在ht[1]的位置 */h = dictHashKey(d, de->key) & d->ht[1].sizemask;/* 添加到ht[1]中 */de->next = d->ht[1].table[h];d->ht[1].table[h] = de;/* 减少ht[0]中的哈希项 */d->ht[0].used--;/* 增加ht[1]中的哈希项*/d->ht[1].used++;/* 指向下一个哈希项 */de = nextde;}/* 如果当前bucket中已经没有哈希项了,将该bucket置为NULL */d->ht[0].table[d->rehashidx] = NULL;/* 将rehash加1,下一次将迁移下一个bucket中的元素 */d->rehashidx++;}/* 返回1,表示ht[0]中仍然有元素没有迁移 */return 1;
}
什么时候触发rehash
Redis中在每次的增删查中都会判断是否需要扩容,在函数_dictExpandIfNeeded中检查负载因子(LoadFactor=used/size),只要满足以下任意一个条件就会触发哈希表扩容:
- 哈希表的LoadFactor>=1,并且服务器没有执行SAVE或REWRITEAOF等子进程
- 哈希表的LoadFactor>5
static int _dictExpandIfNeeded(dict *d)
{/* Incremental rehashing already in progress. Return. */if (dictIsRehashing(d)) return DICT_OK;/* 如果哈希表为空,则进行初始化为4. */if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);if (!dictTypeExpandAllowed(d))return DICT_OK;if ((dict_can_resize == DICT_RESIZE_ENABLE &&d->ht[0].used >= d->ht[0].size) ||(dict_can_resize != DICT_RESIZE_FORBID &&d->ht[0].used / d->ht[0].size > dict_force_resize_ratio)){/* 扩容大小为used + 1, 底层会对扩容的大小做判断,实际上找的是第一个大于等于used+1的2倍 */return dictExpand(d, d->ht[0].used + 1);}return DICT_OK;
}
rehash扩容大小
通过函数_dictExpand对哈希表进行扩容,每次扩容大小总是2的幂,看下内部的 _dictNextPower函数源码:
static unsigned long _dictNextPower(unsigned long size)
{/* 哈希表的初始大小 */unsigned long i = DICT_HT_INITIAL_SIZE;/* 如果要扩容的大小已经超过了最大值,则返回最大值加1*/if (size >= LONG_MAX) return LONG_MAX + 1LU;/* 要扩容的大小没有超过最大值 */while(1) {/* 从DICT_HT_INITIAL_SIZE(通常是4)开始,不断将i乘以2,直到 i 大于等于size */if (i >= size)return i;i *= 2;}
}
总结
通过链式哈希,Redis有效地解决了哈希冲突的问题;通过渐进式rehash,Redis确保了哈希表扩展时的高效性和稳定性。这些机制让Redis哈希表在处理大量数据时仍然保持高效。
参考资料
- Redis Documentation
- Redis GitHub Repository
- Redis源码剖析与实战
相关文章:
Redis源码学习:高性能Hash表的设计与实现
哈希表(Hash)是Redis数据库的数据类型之一,理解哈希表的实现对于掌握Redis非常重要。这篇文章,从哈希冲突和哈希扩展这两个角度,来一步步讲解Redis哈希表的工作原理。 什么是哈希表? 哈希表是一种通过哈希…...
如何防范常见的数据库安全问题
随着数据量的增加和系统的复杂性提高,数据库可能面临多种安全威胁,包括未授权访问、数据泄露、注入攻击等。 1. 未授权访问 未授权访问是指,未经授权的用户对数据库的内容进行访问。这会导致数据泄露、数据篡改或其他安全事故。 针对未授权访问的防范措施如下。 (1)强化…...
[Day 19] 區塊鏈與人工智能的聯動應用:理論、技術與實踐
區塊鏈的數據透明性 區塊鏈技術作為一種分布式賬本技術,因其去中心化、不可篡改和高度透明的特性,已經在各行各業中得到了廣泛應用。在本文中,我們將深入探討區塊鏈的數據透明性,包括其原理、實現方法及相關代碼示例,…...
【Hadoop学习笔记】认识Hadoop
认识Hadoop 从网上找的课程做的笔记,有些图是自己理解画的,可能不正确,可以作为参考,有疑问的地方请直接指出,共同交流。 Hadoop是由Apache基金会开发的一个分布式系统基础架构,主要解决海量数据的存储和海…...
CISP-PTE综合靶机-WinServer2003
1.收集网站的地址和开放的端口,完成前期信息收集。10分 2.访问站点,找出站点的敏感文件,利用返回数据找到相关敏感信 息,完成网站结构的信息收集。10分 3.利用文件包含漏洞读取敏感文件,找出数据库连接凭证,利用此 凭证连接数据库。10分 4.网站后台提权:找出后台管理员登…...
sklearn之各类朴素贝叶斯原理
sklearn之贝叶斯原理 前言1 高斯朴素贝叶斯1.1 对连续变量的处理1.2 高斯朴素贝叶斯算法原理 2 多项式朴素贝叶斯2.1 二项分布和多项分布2.2 详细原理2.3 如何判断是否符合多项式贝叶斯 3 伯努利朴素贝叶斯4 类别贝叶斯4 补充朴素贝叶斯4.1 核心原理4.2 算法流程 前言 如果想看…...
年薪50w+的项目经理,手把手教你如何复盘
复盘是一种重要的学习和改进工具,对于项目经理来说,能帮助识别项目中的成功与失败,为未来的项目管理提供宝贵经验。 理论部分 定义目标。在开始复盘之前,明确复盘的目标是什么。是为了找出项目中的问题并提出解决方案,…...
Web3新视野:Lumoz节点的潜力与收益解读
摘要:低估值、高回报、无条件退款80%...... Lumoz正通过其 zkVerifier 节点销售活动,引领一场ZK计算革命。 长期以来,加密市场以其独特的波动性和增长潜力,持续吸引着全球投资者的目光。而历史数据表明,市场往往在一年…...
【shell脚本速成】mysql备份脚本
文章目录 案例需求脚本应用场景:解决问题脚本思路实现代码 🌈你好呀!我是 山顶风景独好 🎈欢迎踏入我的博客世界,能与您在此邂逅,真是缘分使然!😊 🌸愿您在此停留的每一刻…...
高考志愿填报,理科生如何分析选专业?
理科生选择专业的范围更大一些,相比文科说理工科的院校也更多,如何选择适合自己的专业,这是一个比较重要的课题,毕竟大学专业直接关系到职业,是一辈子的大事。 那么理科究竟如何选择专业呢?需要从什么地方…...
qt 简单实验 json格式的文件写入配置文件
1.概要 2.代码 //#include "mainwindow.h"#include <QApplication> #include <QFile> #include <QJsonDocument> #include <QJsonObject> //读取json数据的配置文件int main(int argc, char *argv[]) {QApplication a(argc, argv);QString…...
将WIN10的wifi上网分享给以太网接口
目录 打开网络设置设置属性点这里的设置将wlan主机的以太网接口IP设为自动获取 如果连接不成功,拔网线重连一次 打开网络设置 设置属性 点这里的设置 将wlan主机的以太网接口IP设为自动获取 如果连接不成功,拔网线重连一次...
在 iPhone 上恢复已删除联系人的 5 种简便方法
想象一下:您正在 iPhone 上滚动并搜索要拨打的联系人,但却找不到任何结果。然后您想起昨晚您试图删除一个名字相似的联系人,但不知何故删除了错误的联系人。或者您的孩子错误地删除了一些联系人。这些情况足以让您感到迷茫。但别担心…...
小白指南:前端使用javascript如何判断集合是不是空集合?
背景 最近在开发一个Web应用时,我遇到了一个关于集合处理的问题。具体来说,我需要判断一个集合是否为空。集合可以是数组、对象、Map或Set等不同的数据结构。就简单的整理了一下如何在JavaScript中有效地判断一个集合是否为空呢? 解决方案 …...
人力资源招聘社会校企类型招聘系统校园招聘小程序
校企社会人力资源招聘小程序:开启高效招聘新时代 🚀开篇:打破传统,开启招聘新篇章 在快速发展的现代社会,人力资源招聘已经成为企业和学校共同关注的重要议题。为了更高效、便捷地满足双方的招聘需求,一款…...
docker重要操作与直连方法
文章目录 前言一、nvidia-docker安装方法1、nvidia-docker安装2、重启动ssh 二、构建镜像1、构建镜像docker拉取构建本地镜像加载构建 2、容器转镜像3、镜像打包4、删除镜像 三、构建容器1、容器构建2、启动镜像3、删除容器 四、docker直连(ssh -p)1、docker更改密码2、物理机操…...
Windows环境利用 OpenCV 中 CascadeClassifier 分类器识别人眼 c++
Windows环境中配置OpenCV 关于在Windows环境中配置opencv的说明,具体可以参考:VS2022 配置OpenCV开发环境详细教程。 CascadeClassifier 分类器 CascadeClassifier 是 OpenCV 库中的一个类,它用于实现一种快速的物体检测算法,称…...
Golang | Leetcode Golang题解之第167题两数之和II-输入有序数组
题目: 题解: func twoSum(numbers []int, target int) []int {low, high : 0, len(numbers) - 1for low < high {sum : numbers[low] numbers[high]if sum target {return []int{low 1, high 1}} else if sum < target {low} else {high--}}r…...
【软件工程】【23.04】p2
关键字: 计算机软件定义、需求基本性质、创建系统类图所涉及的工作、RUP创建系统用况模型活动、软件生存周期模型、能力等级和成熟度等级区别联系; 模块结构图:深度宽度、扇入扇出、作用域、控制域; 程序流程图:语句…...
Java多线程编程与并发控制策略
Java多线程编程与并发控制策略 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天,我想和大家分享一下Java多线程编程与并发控制策略的相关知识&am…...
华为云AI开发平台ModelArts
华为云ModelArts:重塑AI开发流程的“智能引擎”与“创新加速器”! 在人工智能浪潮席卷全球的2025年,企业拥抱AI的意愿空前高涨,但技术门槛高、流程复杂、资源投入巨大的现实,却让许多创新构想止步于实验室。数据科学家…...
shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...
AtCoder 第409场初级竞赛 A~E题解
A Conflict 【题目链接】 原题链接:A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串,只有在同时为 o 时输出 Yes 并结束程序,否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...
什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!
简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求,并检查收到的响应。它以以下模式之一…...
LRU 缓存机制详解与实现(Java版) + 力扣解决
📌 LRU 缓存机制详解与实现(Java版) 一、📖 问题背景 在日常开发中,我们经常会使用 缓存(Cache) 来提升性能。但由于内存有限,缓存不可能无限增长,于是需要策略决定&am…...
掌握 HTTP 请求:理解 cURL GET 语法
cURL 是一个强大的命令行工具,用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中,cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...
CVPR2025重磅突破:AnomalyAny框架实现单样本生成逼真异常数据,破解视觉检测瓶颈!
本文介绍了一种名为AnomalyAny的创新框架,该方法利用Stable Diffusion的强大生成能力,仅需单个正常样本和文本描述,即可生成逼真且多样化的异常样本,有效解决了视觉异常检测中异常样本稀缺的难题,为工业质检、医疗影像…...
Elastic 获得 AWS 教育 ISV 合作伙伴资质,进一步增强教育解决方案产品组合
作者:来自 Elastic Udayasimha Theepireddy (Uday), Brian Bergholm, Marianna Jonsdottir 通过搜索 AI 和云创新推动教育领域的数字化转型。 我们非常高兴地宣布,Elastic 已获得 AWS 教育 ISV 合作伙伴资质。这一重要认证表明,Elastic 作为 …...
