当前位置: 首页 > news >正文

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表的设计与实现

哈希表&#xff08;Hash&#xff09;是Redis数据库的数据类型之一&#xff0c;理解哈希表的实现对于掌握Redis非常重要。这篇文章&#xff0c;从哈希冲突和哈希扩展这两个角度&#xff0c;来一步步讲解Redis哈希表的工作原理。 什么是哈希表&#xff1f; 哈希表是一种通过哈希…...

如何防范常见的数据库安全问题

随着数据量的增加和系统的复杂性提高,数据库可能面临多种安全威胁,包括未授权访问、数据泄露、注入攻击等。 1. 未授权访问 未授权访问是指,未经授权的用户对数据库的内容进行访问。这会导致数据泄露、数据篡改或其他安全事故。 针对未授权访问的防范措施如下。 (1)强化…...

[Day 19] 區塊鏈與人工智能的聯動應用:理論、技術與實踐

區塊鏈的數據透明性 區塊鏈技術作為一種分布式賬本技術&#xff0c;因其去中心化、不可篡改和高度透明的特性&#xff0c;已經在各行各業中得到了廣泛應用。在本文中&#xff0c;我們將深入探討區塊鏈的數據透明性&#xff0c;包括其原理、實現方法及相關代碼示例&#xff0c;…...

【Hadoop学习笔记】认识Hadoop

认识Hadoop 从网上找的课程做的笔记&#xff0c;有些图是自己理解画的&#xff0c;可能不正确&#xff0c;可以作为参考&#xff0c;有疑问的地方请直接指出&#xff0c;共同交流。 Hadoop是由Apache基金会开发的一个分布式系统基础架构&#xff0c;主要解决海量数据的存储和海…...

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+的项目经理,手把手教你如何复盘

复盘是一种重要的学习和改进工具&#xff0c;对于项目经理来说&#xff0c;能帮助识别项目中的成功与失败&#xff0c;为未来的项目管理提供宝贵经验。 理论部分 定义目标。在开始复盘之前&#xff0c;明确复盘的目标是什么。是为了找出项目中的问题并提出解决方案&#xff0c…...

Web3新视野:Lumoz节点的潜力与收益解读

摘要&#xff1a;低估值、高回报、无条件退款80%...... Lumoz正通过其 zkVerifier 节点销售活动&#xff0c;引领一场ZK计算革命。 长期以来&#xff0c;加密市场以其独特的波动性和增长潜力&#xff0c;持续吸引着全球投资者的目光。而历史数据表明&#xff0c;市场往往在一年…...

【shell脚本速成】mysql备份脚本

文章目录 案例需求脚本应用场景&#xff1a;解决问题脚本思路实现代码 &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f388;欢迎踏入我的博客世界&#xff0c;能与您在此邂逅&#xff0c;真是缘分使然&#xff01;&#x1f60a; &#x1f338;愿您在此停留的每一刻…...

高考志愿填报,理科生如何分析选专业?

理科生选择专业的范围更大一些&#xff0c;相比文科说理工科的院校也更多&#xff0c;如何选择适合自己的专业&#xff0c;这是一个比较重要的课题&#xff0c;毕竟大学专业直接关系到职业&#xff0c;是一辈子的大事。 那么理科究竟如何选择专业呢&#xff1f;需要从什么地方…...

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设为自动获取 如果连接不成功&#xff0c;拔网线重连一次 打开网络设置 设置属性 点这里的设置 将wlan主机的以太网接口IP设为自动获取 如果连接不成功&#xff0c;拔网线重连一次...

在 iPhone 上恢复已删除联系人的 5 种简便方法

想象一下&#xff1a;您正在 iPhone 上滚动并搜索要拨打的联系人&#xff0c;但却找不到任何结果。然后您想起昨晚您试图删除一个名字相似的联系人&#xff0c;但不知何故删除了错误的联系人。或者您的孩子错误地删除了一些联系人。这些情况足以让您感到迷茫。但别担心&#xf…...

小白指南:前端使用javascript如何判断集合是不是空集合?

背景 最近在开发一个Web应用时&#xff0c;我遇到了一个关于集合处理的问题。具体来说&#xff0c;我需要判断一个集合是否为空。集合可以是数组、对象、Map或Set等不同的数据结构。就简单的整理了一下如何在JavaScript中有效地判断一个集合是否为空呢&#xff1f; 解决方案 …...

人力资源招聘社会校企类型招聘系统校园招聘小程序

校企社会人力资源招聘小程序&#xff1a;开启高效招聘新时代 &#x1f680;开篇&#xff1a;打破传统&#xff0c;开启招聘新篇章 在快速发展的现代社会&#xff0c;人力资源招聘已经成为企业和学校共同关注的重要议题。为了更高效、便捷地满足双方的招聘需求&#xff0c;一款…...

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的说明&#xff0c;具体可以参考&#xff1a;VS2022 配置OpenCV开发环境详细教程。 CascadeClassifier 分类器 CascadeClassifier 是 OpenCV 库中的一个类&#xff0c;它用于实现一种快速的物体检测算法&#xff0c;称…...

Golang | Leetcode Golang题解之第167题两数之和II-输入有序数组

题目&#xff1a; 题解&#xff1a; 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

关键字&#xff1a; 计算机软件定义、需求基本性质、创建系统类图所涉及的工作、RUP创建系统用况模型活动、软件生存周期模型、能力等级和成熟度等级区别联系&#xff1b; 模块结构图&#xff1a;深度宽度、扇入扇出、作用域、控制域&#xff1b; 程序流程图&#xff1a;语句…...

Java多线程编程与并发控制策略

Java多线程编程与并发控制策略 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天&#xff0c;我想和大家分享一下Java多线程编程与并发控制策略的相关知识&am…...

JavaSec-RCE

简介 RCE(Remote Code Execution)&#xff0c;可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景&#xff1a;Groovy代码注入 Groovy是一种基于JVM的动态语言&#xff0c;语法简洁&#xff0c;支持闭包、动态类型和Java互操作性&#xff0c…...

循环冗余码校验CRC码 算法步骤+详细实例计算

通信过程&#xff1a;&#xff08;白话解释&#xff09; 我们将原始待发送的消息称为 M M M&#xff0c;依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)&#xff08;意思就是 G &#xff08; x ) G&#xff08;x) G&#xff08;x) 是已知的&#xff09;&#xff0…...

python/java环境配置

环境变量放一起 python&#xff1a; 1.首先下载Python Python下载地址&#xff1a;Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个&#xff0c;然后自定义&#xff0c;全选 可以把前4个选上 3.环境配置 1&#xff09;搜高级系统设置 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功能模块&#xff0c;用户不点击提交按钮&#xff0c;只要输入框失去焦点&#xff0c;就会提前提示验证码是否正确。 一&#xff0c;模板上增加验证码 <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&#xff1f; IPsec VPN 5.1 IPsec传输模式&#xff08;Transport Mode&#xff09; 5.2 IPsec隧道模式&#xff08;Tunne…...

python报错No module named ‘tensorflow.keras‘

是由于不同版本的tensorflow下的keras所在的路径不同&#xff0c;结合所安装的tensorflow的目录结构修改from语句即可。 原语句&#xff1a; from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后&#xff1a; from tensorflow.python.keras.lay…...

MinIO Docker 部署:仅开放一个端口

MinIO Docker 部署:仅开放一个端口 在实际的服务器部署中,出于安全和管理的考虑,我们可能只能开放一个端口。MinIO 是一个高性能的对象存储服务,支持 Docker 部署,但默认情况下它需要两个端口:一个是 API 端口(用于存储和访问数据),另一个是控制台端口(用于管理界面…...

TSN交换机正在重构工业网络,PROFINET和EtherCAT会被取代吗?

在工业自动化持续演进的今天&#xff0c;通信网络的角色正变得愈发关键。 2025年6月6日&#xff0c;为期三天的华南国际工业博览会在深圳国际会展中心&#xff08;宝安&#xff09;圆满落幕。作为国内工业通信领域的技术型企业&#xff0c;光路科技&#xff08;Fiberroad&…...