Redis底层数据结构之Dict

目录

    • 一、概述
    • 二、Dict结构
    • 三、Dictht结构
    • 四、DictEntry结构
    • 五、核心特性

上一篇文章 reids底层数据结构之quicklist

一、概述

Redis 的 Dict 是一个高效的键值对映射数据结构,采用双哈希表实现以支持无锁的渐进式 Rehash,确保扩容或缩容时的高效性能。它通过哈希表节点以链表形式解决哈希冲突,允许快速的查找、插入和删除操作,是实现 Redis 各种数据类型和高级功能的基础架构之一。

二、Dict结构

在这里插入图片描述

type:这是一个指向 dictType 结构的指针,为该字典提供一套特定的操作函数。dictType 结构包含了一系列函数指针,用于定义键值对的复制、释放、比较等操作。这使得 dict 结构能够以通用的方式操作不同类型的键和值。

privdata:这是一个指向任意类型数据的指针,该数据会被传递给 dictType 结构中的各种函数。它允许这些函数拥有一个通用的接口,同时又能进行针对特定情境的操作。

ht[0]ht[1]: 这是两个 dictht(哈希表)数组,用于存储字典中的元素。Redis 使用一个渐进式的 rehashing 过程来扩展或缩小这个数据结构的容量。通常,所有的操作都在 ht[0] 上进行,当字典需要扩展或缩小时,元素会逐渐从 ht[0] 移动到 ht[1],这一过程是逐渐进行的,以避免长时间的阻塞

rehashidx: 这是一个标记,用来表示rehashing 进程的进度。当不在 rehashing 时,该值为 -1。当开始 rehashing 时,该值会被设置为 0,并随着 rehashing 过程的进行逐渐增加,直到 rehashing 完成,此时再次将该值设置为 -1。

iterators:这是正在对字典进行迭代的迭代器的数量。这个计数有助于管理对字典的迭代,确保即使在 rehashing 过程中也能安全地进行迭代操作。有活跃的迭代器时,可能会暂停 rehashing 过程,以保证迭代器的一致性和准确性。

三、Dictht结构

在这里插入图片描述

/* This is our hash table structure. Every dictionary has two of this as we* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {dictEntry **table;      // HashTable数组unsigned long size;     // HashTable的大小unsigned long sizemask; // HashTable大小掩码,总是等于size - 1, 通常用来计算索引unsigned long used;     // 已经使用的节点数,实际上就是HashTable中已经存在的dictEntry数量
} dictht;

Redis的字典使用HashTable作为底层实现,一个HashTable中可以保存多个哈希表结点(dictEntry), 而每个dictEntry中就保存着字典中的一个键值对, table属性就是一个数组, 数组中每个元素的类型都是指向dictEntry的指针,而size属性便是table数组的长度, sizemask总是等于size - 1, 用于计算索引信息, used属性记录当前HashTable中dictEntry的总数量

table:这是一个数组,具体类型是指向字典条目(dictEntry)指针的数组。每一个 dictEntry 包含了键值对信息。这个 table 是哈希表的本质存储结构,实际的数据(键值对)都存储在这里。

size:这个成员表示哈希表中 table 数组的大小,也就是可以容纳的 dictEntry 数目。它总是2的幂次,这是为了使用位掩码(sizemask)与运算来替代取模运算,提升计算效率。

sizemask:它是与 size 成员配合使用的位掩码。它总是等于 size - 1。当计算一个键的哈希值来确定其在 table 数组中的位置时,用哈希值对 size 取模,等价于与 sizemask 进行按位与运算。后者由于只涉及二进制位运算,因此效率更高。

used:代表哈希表中已有的元素数量,即实际已经使用的 dictEntry 数目。这个数值在添加或删除键值对时会相应地增加或减少。

四、DictEntry结构

在这里插入图片描述

struct dictEntry {void *key;union {void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next;     /* Next entry in the same hash bucket. */
};
typedef struct {void *key;dictEntry *next;
} dictEntryNoValue;

dictEntry是Dictht中结点的表现形式, 每个dictEntry都保存着一个键值对, key属性指向键值对的键对象, 而v属性则保存着键值对的值, Redis采用了联合体来定义v, 使键值对的值既可以存储一个指针, 也可以存储有符号/无符号整形数据,甚至可以存储浮点形数据, Redis使用联合体的形式来存储键值对的值可以让内存使用更加精细灵活, 另外, 既然是HashTable, 不可避免会发生两个键不同但是计算出来存放索引相同的情况, 为了解决Hash冲突的问题, dictEntry还有一个next属性, 用来指向与当前dictEntry在同一个索引的下一个dictEntry.

五、核心特性

哈希算法

Redis计算哈希值和索引值方法如下:

 #1、使用字典设置的哈希函数,计算键 key 的哈希值hash = dict->type->hashFunction(key);#2、使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值index = hash & dict->ht[x].sizemask;

解决哈希冲突

当两个或更多的键被哈希到同一位置时(即发生哈希冲突),Redis 通过链地址法来解决冲突,即在这个位置维护一个链表,所有哈希到这个位置的键值对都会被加入到这个链表中。

自动扩容和缩容

可以预见的是,随着我们不断的对HashTable进行操作,可能会发生以下两种情况:

  • 不断的向HashTable中添加数据,HashTable中每个索引上的dictEntry数量会越来越多,也就是单链表会越来越长,这会十分影响字典的查询效率(最坏的场景可能要把整个单链表遍历完毕才能确定一个Key对应的dictEntry是否存在), 而Redis通常被当做缓存,这种低性能的场景是不被容许的.

  • 向一个本身已经十分巨大的HashTable执行删除节点的操作,由于原先这个HashTable的size很大(也就是说table数组十分巨大,我们假设size为M), 但是执行了大量的删除操作之后,table数组中很多元素指向了NULL(由于对应的索引上已经没有任何dictEntry, 相当于一个空的单链表), HashTable中剩余结点我们假设为N,这时M远大于N,也就是说之上table数组中至少有M - N个元素指向了NULL,这是对内存空间的巨大浪费,而Redis是内存型数据库,这种浪费内存的场景也是不被容许的.

针对以上两种场景,为了让HashTable的负载因子(HashTable中所有dictEntry的数量/HashTable的size值)维持在一个合理的范围内,Redis在HashTable保存的dictEntry数量太多或者太少的时候,会对HashTable的大小进行扩展或者收缩,在没有执行Rehash操作时,字典的所有数据都存储在ht[0]所指向的HashTable中,而在Rehash操作过程中,Redis会创建一个新的HashTable, 并且令ht[1]指向它,然后逐步的将ht[0]指向的HashTable的数据迁移到ht[1]上来

判断扩展HashTable的逻辑

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{/* Incremental rehashing already in progress. Return. */if (dictIsRehashing(d)) return DICT_OK;/* If the hash table is empty expand it to the initial size. */if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);/* If we reached the 1:1 ratio, and we are allowed to resize the hash* table (global setting) or we should avoid it but the ratio between* elements/buckets is over the "safe" threshold, we resize doubling* the number of buckets. */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);}return DICT_OK;
}

每次获取一个Key的索引信息时,都会调用上述的_dictExpandIfNeeded(dict *d)方法判断是否需要对当前HashTable执行扩展操作,满足下列任意条件之一,便会执行扩展操作:

  • 当前ht[0]所指向的HashTable大小为0
  • 服务器目前没有执行BGSAVE或者BGREWRITEAOF操作,并且HashTable的负载因子大于等于1(d->ht[0].used >= d->ht[0].size)
  • 服务器目前正在执行BGSAVE或者BGREWRITEAOF操作,并且HashTable的负载因子大于5(dict_force_resize_ratio = 5)

判断收缩HashTable的逻辑

int htNeedsResize(dict *dict) {long long size, used;size = dictSlots(dict);used = dictSize(dict);return (size > DICT_HT_INITIAL_SIZE &&(used*100/size < HASHTABLE_MIN_FILL));
}/* Resize the table to the minimal size that contains all the elements,
* but with the invariant of a USED/BUCKETS ratio near to <= 1 */
int dictResize(dict *d)
{int minimal;if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;minimal = d->ht[0].used;if (minimal < DICT_HT_INITIAL_SIZE)minimal = DICT_HT_INITIAL_SIZE;return dictExpand(d, minimal);
}

Redis会定期的检查数据库字典HashTable的状态,当HashTable的负载因子小于0.1时,会自动的对HashTable执行收缩操作

rehash过程

  • 分配空间:为ht[1]指向的HashTable分配空间, 分配空间的大小取决于要执行的操作,以及ht[0]所指向HashTable中dictEntry结点的数量, 也就是ht[0].used中记录的值:
    • 如果当前执行的是扩展操作,那么新HashTable的大小为第一个大于等于2 * ht[0].used的2的n次方幂
    • 如果当前执行的是收缩操作,那么新HashTable的大小为第一个大于等于ht[0].used的2的n次方幂
  • 渐进式rehash:将ht[0]所指向HashTable中的所有dictEntry节点都迁移到ht[1]所指向的新HashTable上(由于两个HashTable的size不同,所以在迁移过程中要重新计算dictEntry的索引,这也就是rehash的关键所在)
  • 迁移完成:当迁移完成之后,ht[0]所指向的HashTable中已经没有任何节点,释放该HashTable, 并且令ht[0]指向迁入节点的新HashTable, 最后为ht[1]创建一个空白的HashTable,为下一次rehash做准备

在这里插入图片描述
说明:可以看到当前rehashidx指向ht[0]的索引2,这说明ht[0][0, rehashidx - 1]对应buckets上的dictEntry都已经迁移完毕,另外我们发现正在执行渐进式rehash字典中的数据一部分在ht[0]中,而另一部分在ht[1]中,所以在渐进式rehash执行期间,字典的删除,查找以及更新操作,都会在两个HashTable上执行(先尝试在ht[0]上执行操作,如果没有成功,则再尝试在ht[1]上进行操作),而在渐进式rehash执行期间,如果我们需要往字典中添加新的结点,则会一律添加到ht[1]上,这样可以保证ht[0]上的结点只减不增(也就是已经迁移过的buckets不会再出现新的结点)

渐进式rehash

为了避免rehash过程阻塞服务器,Redis采用渐进式rehash策略。在进行rehash期间,所有的读写操作都会同时在ht[0]ht[1]上进行,并逐步将ht[0]上的键值对迁移到ht[1]

rehash触发条件

  • 负载因子过高(扩容):
    • 当前ht[0]所指向的HashTable大小为0
    • 服务器目前没有执行BGSAVE或者BGREWRITEAOF操作,并且HashTable的负载因子大于等于1(d->ht[0].used >= d->ht[0].size)
    • 服务器目前正在执行BGSAVE或者BGREWRITEAOF操作,并且HashTable的负载因子大于5(dict_force_resize_ratio = 5)
  • 负载因子过低(缩容):
    • Redis会定期的检查数据库字典HashTable的状态,当HashTable的负载因子小于0.1时,会自动的对HashTable执行收缩操作

Dict优缺点

Dict 提供了快速的键值对查找和插入性能,能高效地支持数据的增删改查操作;然而,在内存使用上它可能不如一些更紧凑的数据结构高效,尤其是在存储大量小对象时,还有当哈希表发生扩容或收缩时可能会有短暂的性能下降。

参考 redis底层数据结构-Dict

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

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

相关文章

想冲宇宙厂,直接挂了。。。

宇宙厂实际是字节&#xff0c;这个称呼是因为字节跳动主宰了宇宙内一切App&#xff0c;有点家大业大的意思。 今天分享一位字节春招凉经&#xff0c;问了一些数据库和Java八股&#xff0c;没出算法题&#xff0c;直接挂了&#xff0c;竟然最喜欢出算法题的字节&#xff0c;这次…

iptables实现docker容器动态端口映射实操

背景 之前在《Docker 动态修改容器端口映射的方法》一文中&#xff0c;说明了如何使用修改配置和加防火墙规则实现动态端口映射。但是没有具体分享加防火墙实现动态端口映射的实际案例。今天就分享一下实际操作案例&#xff0c;供大家参考。 分析 动态端口映射的用途 容器端口…

(2024)Visual Studio的介绍、安装与使用

Visual Studio介绍 1.Visual Studio是什么&#xff1f; Visual Studio是微软公司推出的一款开发工具包系列产品&#xff0c;它是一个基本完整的开发工具集&#xff0c;为软件开发者提供了整个软件生命周期中所需的大部分工具。 2.Visual Studio的定义 Visual Studio是美国微软公…

网盘_游戏_博客自动化部署(Nginx多项目部署)

目录 一.前提介绍 二.环境介绍 三.自述&#xff08;脚本&#xff09; 四.关于Nginx多项目部署 一.前提介绍 在我之前的博客里详细介绍了上述项目的部署&#xff0c;那么如何使用简单脚本自动部署和使用Nginx多项目部署是本文来介绍的基础篇章。 二.环境介绍 CentOS Linux…

fawawf

c语言中的小小白-CSDN博客c语言中的小小白关注算法,c,c语言,贪心算法,链表,mysql,动态规划,后端,线性回归,数据结构,排序算法领域.https://blog.csdn.net/bhbcdxb123?spm1001.2014.3001.5343 给大家分享一句我很喜欢我话&#xff1a; 知不足而奋进&#xff0c;望远山而前行&am…

【Linux】文件目录及路径表示

1. Linux目录结构 在 Linux 系统中&#xff0c;有几个目录是比较重要的&#xff0c;平时需要注意不要误删除或者随意更改内部文件。 /etc&#xff1a; 这个是系统中的配置文件&#xff0c;如果更改了该目录下的某个文件可能会导致系统不能启动。 /bin, /sbin, /usr/bin, /usr…

java泛型介绍

Java 泛型是 JDK 5 引入的一个特性&#xff0c;它允许我们在定义类、接口和方法时使用类型参数&#xff0c;从而使代码更加灵活和类型安全。泛型的主要目的是在编译期提供类型参数&#xff0c;让程序员能够在编译期间就捕获类型错误&#xff0c;而不是在运行时才发现。这样做提…

小程序AI智能名片S2B2C商城系统:解锁内容深耕新境界,助力品牌企业高效定制内容策略

在数字化时代&#xff0c;内容营销已成为品牌企业获取市场份额、增强用户黏性的关键武器。然而&#xff0c;面对海量的互联网信息和复杂多样的社交媒体平台&#xff0c;如何有效地深耕内容&#xff0c;成为众多品牌企业面临的难题。 传统的内容分类与识别方式&#xff0c;往往依…

【数据分析面试】28. 20个Python问答题 (入门级考察:基础操作、数据处理与分析统计)

今天的20个问题考察了 Python 的基础能力&#xff0c;包括数据结构、基本操作、数据处理、数据分析和统计等方面。无论是从事数据分析、机器学习还是其他数据相关工作&#xff0c;这些都是必不可少的基础技能。 数据结构与基础操作&#xff1a; 什么是 Pandas 库&#xff1f;它…

中兴5G随身wifi怎么样?中兴5G随身wifiVS格行5G随身wifi对比测评!公认最好的随身WiFi的格行随身WiFi真实测评!随身WiFi哪个品牌好?

随着各大品牌5G随身wifi的横空出世&#xff0c;其中中兴和格行5G随身wifi的呼声越来越高&#xff0c;那么性能上谁更胜一筹&#xff1f;套餐费用谁更亲民&#xff1f;售后保障谁更到位&#xff1f;今天就来一个全方位测评对比&#xff01; 一&#xff0c;首先是设备的整体外观&…

uniapp:小白1分钟学会使用webSocket(可无脑复制)

uni.connectSocket() uni.$emit页面通信 项目中使用uni.connectSocket()创建webSocket的总结&#xff0c;代码可无脑复制&#xff0c;直接使用。 1、main.js 引入vuex import store from ./store; Vue.prototype.$store store;vuex中封装webSocket 2、vuex的&#xff1a;index…

linux autogroup

一&#xff1a;概述 对于linux autogroup的作用&#xff0c;很多同学可能是听说过&#xff0c;但&#xff0c;并未验证过。 考虑下面场景&#xff0c;开两个terminal&#xff0c;T1和T2&#xff0c;在T1中运行进程P1&#xff0c;P1开启9个线程编译代码&#xff0c;在T2中运行…

yield函数怎么理解?

目录 白话系列&#xff1a; 例子&#x1f330;&#xff1a; 什么叫暂停 yield和next搭配使用 例子&#x1f330;&#xff1a; 白话系列&#xff1a; 可以暂停&#xff0c;可以生成&#xff0c;next一个&#xff0c;yield一个 例子&#x1f330;&#xff1a; def generat…

CUDA线程管理

核函数在主机端启动时&#xff0c;执行会转移到设备上&#xff0c;并且将控制权转移回主机。当核函数在GPU上运行时&#xff0c;主机可以运行其他函数。因此&#xff0c;主机与核函数是异步的。 此时&#xff0c;设备端也就是GPU上会产生大量的线程&#xff0c;并且每个线程都…

(七)小案例银行家应用程序-申请贷款-some方法和every方法

some方法 ● 我们先回顾一下includes方法 console.log(movements.includes(-130));只要数组中存在-130这个值&#xff0c;就会返回true&#xff0c;否则就会返回flase ● 而some方法只要达成某一个条件就会返回true&#xff0c;否则就返回flase const someMethod movement…

stm32开发之threadx之modulex模块文件的生成脚本项目

前言 为了保证在window上运行&#xff0c;且体积小的问题&#xff0c;所以采用c语言编写生成脚本,将相关路径由json文件进行配置,使用了一个cjson库进行解析项目构建使用的是cmake 项目代码 CMakeLists文件 cmake_minimum_required(VERSION 3.27) project(txm_bat_script C…

Day13-Java进阶-IO字节流及其练习题

1. IO流介绍 2. IO 流体系结构 字节流读取纯文本文件会出现乱码问题 2.1 FileOutputStream 字节输出流 package com.itheima.stream.output;import java.io.FileOutputStream; import java.io.IOException;public class FileOutputStreamDemo3 {/*IO流的异常处理方式: jdk7版本…

socket编程——tcp

在我这篇博客&#xff1a;网络——socket编程中介绍了关于socket编程的一些必要的知识&#xff0c;以及介绍了使用套接字在udp协议下如何通信&#xff0c;这篇博客中&#xff0c;我将会介绍如何使用套接字以及tcp协议进行网络通信。 1. 前置准备 在进行编写代码之前&#xff…

C语言学习/复习30--结构体的声明/初始化/typedef改名/内存对齐大小计算

一、自定义数据类型 二、结构体 1.结构体的定义&#xff08;与数组相对比&#xff09; 2.结构体全局/局部变量的定义 3.typedef对结构体改名 4.匿名结构体类型的声明 注意事项1&#xff1a; 匿名后必须立即创建结构体变量 、 5.结构体与链表节点定义 注意事项1&…

Datawhale ChatGPT基础科普

根据课程GitHub - datawhalechina/hugging-llm: HuggingLLM, Hugging Future. 摘写自己不懂得一些地方&#xff0c;具体可以再到以上项目地址 LM&#xff1a;这是ChatGPT的基石的基石。 Transformer&#xff1a;这是ChatGPT的基石&#xff0c;准确来说它的一部分是基石。 G…