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…...
JavaSec-RCE
简介 RCE(Remote Code Execution),可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景:Groovy代码注入 Groovy是一种基于JVM的动态语言,语法简洁,支持闭包、动态类型和Java互操作性,…...
循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...
python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...
STM32+rt-thread判断是否联网
一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...
dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
python报错No module named ‘tensorflow.keras‘
是由于不同版本的tensorflow下的keras所在的路径不同,结合所安装的tensorflow的目录结构修改from语句即可。 原语句: from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后: from tensorflow.python.keras.lay…...
MinIO Docker 部署:仅开放一个端口
MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...
TSN交换机正在重构工业网络,PROFINET和EtherCAT会被取代吗?
在工业自动化持续演进的今天,通信网络的角色正变得愈发关键。 2025年6月6日,为期三天的华南国际工业博览会在深圳国际会展中心(宝安)圆满落幕。作为国内工业通信领域的技术型企业,光路科技(Fiberroad&…...
