Redis数据库笔记—— Hash(哈希)的扩容机制(rehash)

大家好,这里是Good Note,关注 公主号:Goodnote,专栏文章私信限时Free。详细介绍Hash(哈希)的扩容机制(rehash)、源码、以及扩容和缩容过程。

在这里插入图片描述

文章目录

      • Redis 字典(dict)结构
      • 源码
        • 哈希表结构定义
        • 渐进式哈希扩容(rehash)
        • 渐进式哈希的优点
      • 扩容机制:rehash
        • 扩容条件
        • 扩容过程
      • 缩容机制:rehash
        • 缩容条件
        • 缩容过程
      • 图示 rehash 操作
        • 1. 开始 Rehash
        • 2. Rehash 进行中
        • 3. 节点迁移完毕
        • 4. Rehash 完毕
      • 字典的扩容和渐进式 rehash 实现
      • Rehash 的影响与优化
      • rehash执行的操作
        • 查询操作
        • 添加操作
      • 总结
    • 历史文章
      • MySQL数据库
      • Redis

Redis 的 Hash 类型是一个非常重要的数据结构,用于存储键值对的映射关系。它背后使用的是一种哈希表(hash table)结构,提供高效的 O(1) 时间复杂度进行插入、查找和删除操作。然而,当哈希表中的元素逐渐增多时,可能会触发哈希表的 扩容(rehash) 操作,以保证数据结构的高效性和性能。

Redis 字典(dict)结构

Redis 中的字典(dict)是一个使用哈希表实现的高效数据结构,它包含以下基本元素:

  • 键值对:每个元素是一个 keyvalue 的配对,Redis 通过哈希表将 key 映射到 value
  • 哈希表:采用链地址法的哈希表,桶(bucket)用于存储哈希值冲突的元素。

源码

哈希表结构定义

Redis 的字典(dict)使用了哈希表(dictht)作为其基本存储结构。每个字典(dict)由两个哈希表组成,这两个哈希表用于实现 渐进式 rehash(渐进式扩容)。下面是哈希表相关的结构定义:

1. 单个节点:

  • dictEntry:这是哈希表中的一个元素。每个元素包含一个键(key)和值(v),并且每个节点指向下一个哈希表节点(形成链地址法)。
typedef struct dictEntry {void *key;            // 键union {void *val;        // 值(可以是任意类型)uint64_t u64;     // 无符号64位整型值int64_t s64;      // 有符号64位整型值} v;dictEntry *next;      // 指向下一个哈希表节点(链表形式)
} dictEntry;

2. 哈希表:

  • dictht:这是哈希表本身的结构,包含哈希表的大小、掩码、已使用的节点数以及哈希表数组(table),数组中的每个项是 dictEntry 链表的头节点。
typedef struct dictht {dictEntry **table;        // 哈希表数组,表的每个元素是一个链表的头节点unsigned long size;       // 哈希表大小unsigned long sizemask;   // 哈希表大小掩码,size - 1unsigned long used;       // 哈希表当前已使用的节点数
} dictht;

3. 字典结构体:

  • dict:字典结构体,包含两个 dictht(旧表和新表),用于实现渐进式哈希扩容,还包含指向 dictType(类型特定函数)的指针和当前正在执行的安全迭代器的数量。
typedef struct dict {dictType *type;         // 类型特定函数void *privdata;         // 私有数据dictht ht[2];           // 两张哈希表:ht[0] 是旧表,ht[1] 是新表int rehashidx;          // rehash 索引(标识当前扩容进度)int iterators;          // 当前正在运行的迭代器数量
} dict;
渐进式哈希扩容(rehash)

在 Redis 中,哈希表的扩容(rehash)采用渐进式的方法。这意味着哈希表的扩容不是一次性完成的,而是通过多次操作逐步迁移数据,避免阻塞。具体扩容的过程是基于桶(bucket)为单位进行的,每次迁移一个桶的数据,直到完成迁移。

核心函数:dictRehash

该函数实现了渐进式的哈希扩容。它接受一个参数 n,表示希望执行的步骤数,每次执行一步操作。以下是该函数的主要步骤和逻辑:

int dictRehash(dict *d, int n) {int empty_visits = n * 10;  // 最多访问的空桶数if (!dictIsRehashing(d)) return 0;  // 如果没有进行 rehash,直接返回while (n-- && d->ht[0].used != 0) {  // 如果有数据需要迁移dictEntry *de, *nextde;assert(d->ht[0].size > (unsigned long)d->rehashidx);  // 确保 rehashidx 没有越界while (d->ht[0].table[d->rehashidx] == NULL) {  // 跳过空桶d->rehashidx++;if (--empty_visits == 0) return 1;  // 如果访问空桶次数超过限制,返回1,继续执行下一次}de = d->ht[0].table[d->rehashidx];  // 当前桶// 将当前桶中的所有元素迁移到新哈希表while (de) {uint64_t h;nextde = de->next;  // 记录下一个节点h = dictHashKey(d, de->key) & d->ht[1].sizemask;  // 计算新哈希表的桶索引de->next = d->ht[1].table[h];  // 将元素插入新哈希表d->ht[1].table[h] = de;d->ht[0].used--;  // 旧哈希表的元素减少d->ht[1].used++;  // 新哈希表的元素增加de = nextde;  // 继续处理下一个元素}d->ht[0].table[d->rehashidx] = NULL;  // 清空旧表的已迁移桶d->rehashidx++;  // 更新迁移的索引}// 检查是否已完成迁移if (d->ht[0].used == 0) {zfree(d->ht[0].table);  // 回收旧哈希表空间d->ht[0] = d->ht[1];  // 旧表变为新表_dictReset(&d->ht[1]);  // 重置新表d->rehashidx = -1;  // 结束 rehashreturn 0;  // 返回0,表示迁移完成}// 还有更多的元素需要迁移return 1;
}

函数逻辑分析

  1. 判断是否正在进行 rehash:如果 rehashidx == -1,则表示没有进行 rehash,因此直接返回 0。否则,进入扩容操作。

  2. 迁移数据:每次迁移一个桶的数据,直到迁移完当前 n 步的操作。每个桶可能包含多个哈希冲突的元素,因此会使用链地址法逐一迁移。

  3. 跳过空桶:在迁移过程中,rehashidx 会跳过空桶。通过 empty_visits 限制最多访问空桶的数量,防止迁移操作造成长时间阻塞。

  4. 迁移完成:当所有元素从旧哈希表迁移到新哈希表后,回收旧表的内存并重置新表。若迁移未完成,则返回 1,表示还有数据需要迁移。

渐进式哈希的优点
  • 性能优化:Redis 是单线程模型,如果在哈希表扩容时一次性迁移所有数据,可能会导致阻塞,影响性能。渐进式哈希通过逐步迁移数据,在后台处理扩容,避免了阻塞。

  • 实时性:在大量数据迁移的过程中,Redis 仍然能够继续处理其他操作,不会因扩容而导致延迟或服务中断。

  • 控制扩容步长:通过 dctRehash 函数中的 n 参数,用户可以控制扩容的步长(桶的数量),避免扩容操作占用过多时间。

扩容机制:rehash

哈希表的负载因子是哈希表容量与存储的元素数量之间的比例。负载因子过大时,会导致哈希冲突增加,从而影响性能,因此需要进行扩容(rehash)操作。Redis 采用渐进式 rehash 机制,以平滑过渡的方式增加哈希表的容量,避免在 rehash 时导致性能的剧烈波动。

扩容条件

Redis 字典的扩容发生在以下条件下:

  • 负载因子超过设定阈值时,触发扩容(rehash)。具体来说,当字典中的元素数量超过了字典容量的负载因子时,Redis 会自动扩容哈希表。
  • 默认负载因子:Redis 默认的负载因子为 1,Redis 会将哈希表容量扩大为原来的 2 倍。
  • 扩容是为了解决哈希冲突,提高哈希表的查找效率。
扩容过程

扩容是指当哈希表的负载因子过大时,Redis 会创建一个更大的哈希表,并将所有的元素重新映射到新的哈希表中。具体过程如下:

  1. 计算新大小:当哈希表的负载因子超过设定的阈值时,Redis 会将哈希表的容量扩大为原来大小的两倍。比如,如果当前字典大小为 N,则新的字典大小会是 2 * N

  2. 渐进式 rehash:为了避免在扩容时出现一次性大规模的性能抖动,Redis 采用了渐进式 rehash(Incremental Rehash)。在这种机制下,rehash 的过程并不是一次性完成的,而是分批进行的。具体来说,Redis 会在后续的操作中逐步完成哈希表从旧哈希表到新哈希表的元素迁移。

    • 每次执行哈希表操作(如 SETGETDEL 等)时,Redis 会检查当前是否有需要迁移的元素。如果有迁移任务,它会将一部分元素(一个元素或者说是一个桶的元素)从旧表迁移到新表,并在之后的操作中继续迁移剩余的元素,直到迁移完毕。
    • 这样可以避免一次性迁移所有元素导致的性能瓶颈,使得扩容过程对 Redis 的性能影响最小。
  3. 完成 rehash 后:当所有元素迁移到新的哈希表后,Redis 将释放旧哈希表所占用的内存。

缩容机制:rehash

与扩容操作类似,当 Redis 中的哈希表负载因子过低时,Redis 会触发缩容(rehash) 操作,目的是为了节省内存并提高效率。缩容操作主要用于在键值对数量减少时,调整哈希表的大小,避免浪费内存。

缩容条件

Redis 字典的扩容发生在以下条件下:

  • 当负载因子低于 0.1 时,Redis 会启动缩容操作。
  • Redis 会将哈希表容量缩小为适应当前键值对数量的 2 的幂次方。
  • 缩容的目的是节省内存空间,避免内存浪费。

如果现在哈希表中存储了 30 个键值对,2^5 = 32 (足够容纳 30 个键值对)。新的哈希表的大小会是 32。

缩容过程

缩容的过程与扩容类似,也是通过渐进式 rehash 来完成的,区别在于目标是减小哈希表的大小。

  1. 计算新大小

    • 当负载因子小于设定阈值时,Redis 会创建一个新的哈希表(ht[1]),其大小为当前哈希表的键值对数量所能容纳的最小 2 的幂次方。
    • 如果 ht[0].used(当前元素数量)为 N,则新的字典大小为不小于 N 的 2 的幂次方。
  2. 渐进式 rehash

    • 为了避免缩容时的性能瓶颈,Redis 会采用渐进式 rehash 机制,逐步将元素从旧的哈希表迁移到新的哈希表。
    • 每当执行哈希表的操作时(如 SETGETDEL 等),Redis 会检查是否有待迁移的元素。如果有迁移任务,Redis 会将部分元素从旧哈希表(ht[0])迁移到新哈希表(ht[1])。
    • 迁移操作会在每次操作中分批进行,直到所有元素都迁移完成,避免一次性迁移带来的性能压力。
  3. 完成 rehash 后

    • 当所有的元素迁移到新哈希表后,Redis 会释放旧哈希表所占用的内存。
    • 此时,ht[0] 被置为空表,ht[1] 成为新的哈希表。

图示 rehash 操作

来源

Redis 字典的 rehash 是一个用于扩展或收缩哈希表大小的过程。它涉及到重新分配空间,并逐步迁移键值对。以下是整个 rehash 过程的详细步骤,借助于例子,我们可以更清晰地理解 Redis 是如何管理字典的扩容和迁移的。

1. 开始 Rehash

在这一阶段,Redis 执行两个主要任务:

  • 初始化 rehashidxrehashidx 被设置为 0,标识 rehash 过程的开始。
  • ht[1] 分配空间:新的哈希表 ht[1] 会被分配至少是 ht[0] 大小的两倍空间。

此时字典的状态如下:

  • 旧哈希表 (ht[0]):存储原有键值对(例如 4 个键值对),并具有初步的哈希表大小(如大小 4)。
  • 新哈希表 (ht[1]):开始创建,并分配空间(例如大小 8),但此时没有存储任何键值对。

在这里插入图片描述

2. Rehash 进行中

此时,rehashidx 的值被逐步增加,并表示 ht[0] 当前迁移到了哪个位置。逐步迁移的原因是 渐进式 rehash,即迁移过程不会一次性完成,而是分多次进行。

  • ht[0] 中的部分桶可能已经迁移,而 ht[1] 中的部分桶已经开始填充新数据。
  • 以下是 rehashidx 值为 2 时,字典的样子:

在这里插入图片描述

3. 节点迁移完毕

当所有元素从 ht[0] 完全迁移到 ht[1] 后,ht[0] 变为空,且 ht[1] 中存储了所有键值对。此时,rehashidx 会被设置为 ht[1] 的最后一个桶位置,标志着迁移过程的完成。

4. Rehash 完毕

在 rehash 完成后,Redis 会:

  • 释放 ht[0] 的空间:由于所有的键值对已经迁移到 ht[1]ht[0] 中的空间可以被释放。
  • 替换 ht[0]ht[1]ht[1] 被替换为新的 ht[0],而 ht[0] 的数据则会被丢弃(因为它已经变为空)。
  • 更新字典的 rehashidxrehashidx 被设置为 -1,表示 rehash 完成,所有迁移工作已经结束。

此时,字典的哈希表已扩展,且容量已增加为 8,可以容纳更多的键值对,且原有的键值对未发生任何变化。

字典的扩容和渐进式 rehash 实现

在 Redis 中,字典的扩容和渐进式 rehash 由以下几个函数实现:

  • dictExpand():用于扩容哈希表,当元素数量超过哈希表容量的阈值时,会调用该函数。
  • dictResize():用于调整哈希表的大小,可以增加或减少容量。
  • dictRehash():这是核心函数之一,用于将旧哈希表的元素迁移到新哈希表。迁移的过程是逐步进行的,不会阻塞客户端的请求。

Rehash 的影响与优化

  • 性能优化:Redis 通过渐进式 rehash 技术避免了扩容时的性能瓶颈。渐进式 rehash 的引入使得 Redis 可以在高并发环境下继续处理客户端请求,极大地提升了扩容操作的效率。
  • 内存优化:通过扩容和缩容,Redis 在保证性能的同时有效地使用内存。当负载因子较高时,扩容可以避免哈希冲突过多,保证查询性能;而当元素较少时,缩容可以释放内存。
  • 内存浪费:尽管 Redis 尝试通过自动扩容和缩容来优化内存使用,但在一些极端情况下(比如字典大小变化较大时),可能会造成一定的内存浪费。

rehash执行的操作

查询操作
  • 在进行哈希表查询(例如 GET 操作)时,Redis 会先检查 旧哈希表ht[0])。
  • 如果在 旧哈希表 中找到了匹配的元素,Redis 会返回该元素。
  • 如果元素不在旧哈希表中,Redis 会继续查找 新哈希表ht[1]),即使在进行 rehash 的过程中,新哈希表中的元素还在逐步迁移。
添加操作

在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作,这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。

总结

Redis 字典(dict)的扩容机制是通过 渐进式 rehash 实现的,这使得在扩容时 Redis 可以保持高效的性能。每当字典达到一定负载因子时,Redis 会通过将哈希表的大小扩大一倍,并逐步将元素迁移到新表中,以保证系统稳定运行。同时,Redis 还支持缩容操作,以节省内存。渐进式 rehash 是 Redis 的一项关键优化,保证了 Redis 在高并发和大数据量情况下的高效性。

历史文章

MySQL数据库

  1. MySQL数据库笔记——数据库三范式
  2. MySQL数据库笔记——存储引擎(InnoDB、MyISAM、MEMORY、ARCHIVE)
  3. MySQL数据库笔记——常见的几种锁分类
  4. MySQL数据库笔记——索引介绍
  5. MySQL数据库笔记——事务介绍
  6. MySQL数据库笔记——索引结构之B+树
  7. MySQL数据库笔记——索引潜规则(回表查询、索引覆盖、索引下推)
  8. MySQL数据库笔记——索引潜规则(最左前缀原则)
  9. MySQL数据库笔记——常见慢查询优化方式
  10. MySQL数据库笔记——日志介绍
  11. MySQL数据库笔记——多版本并发控制MVCC
  12. MySQL数据库笔记——主从复制

Redis

  1. Redis数据库笔记——数据结构类型
  2. Redis数据库——Redis雪崩、穿透、击穿
  3. Redis数据库——内存淘汰机制
  4. Redis数据库笔记——内存分配器
  5. Redis数据库笔记——内存预分配

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

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

相关文章

Python自学 - 封装与私有化

1 Python自学 - 封装与私有化 1.1 封装的概念 封装是指将数据和方法封装在一起,并且需要通过类的实例来访问,这样就可以确保合适的方法来处理合适的数据,并可以做到只给外界看到想给外界看到的接口,减少数据或方法被滥用的风险。…

网络安全常见的问题

1. 什么是 DDoS 攻击?如何防范? 答:DDoS 攻击是指利用大量的计算机或者其他网络设备,同时向目标网络或者服务器 发送 大量的数据流量,以致其无法正常工作,从而导致网络瘫痪或者服务器宕机的攻击行 为。 …

<rust>在rust中,实现32位浮点数与16进制之间的转换

前言 本文是基于rust,对16进制、32位浮点数之间的互相转换的一个简单示例。 环境配置 系统:windows 平台:visual studio code 语言:rust 库:hex 关键依赖 hex"0.4.3"对于字符串与16进制之间的互相转换&am…

pytest日志显示

在 pytest 中,可以通过 钩子函数 和 配置文件 pytest.ini 配置日志的显示方式,实现对日志的灵活控制。以下是常用实现方式及配置说明。 方式一:使用 conftest.py 钩子函数自定义日志显示 通过 conftest.py 文件中的钩子函数,实现…

支持向量机算法(一):像讲故事一样讲明白它的原理及实现奥秘

1、支持向量机算法介绍 支持向量机(Support Vector Machine,SVM)是一种基于统计学习理论的模式识别方法, 属于有监督学习模型,主要用于解决数据分类问题。SVM将每个样本数据表示为空间中的点,使不同类别的…

ES中的仓库和快照

文章目录 简介仓库(Repository)快照(Snapshot)常用命令记录 简介 在Elasticsearch(简称ES)中,仓库(repository)和快照(snapshot)是两种重要的备份…

数据结构与算法之二叉树: LeetCode 108. 将有序数组转换为二叉搜索树 (Ts版)

将有序数组转换为二叉搜索树 https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/description/ 描述 给你一个整数数组 nums ,其中元素已经按 升序 排列请你将其转换为一棵 平衡 二叉搜索树 示例 1 输入:nums [-10,-3,0,5,9…

Neo4j的部署和操作

注:本博文展示部署哥操作步骤和命令,具体报告及运行截图可通过上方免费资源绑定下载 一.数据库的部署与配置 在单个节点上对进行数据库的单机部署 (1)上传neo4j-community-3.5.30-unix.tar.gz到hadoop1的/export/so…

人工智能 前馈神经网络练习题

为了构建一个有两个输入( X 1 X_1 X1​、 X 2 X_2 X2​)和一个输出的单层感知器,并进行分类,我们需要计算权值 w 1 w_1 w1​和 w 2 w_2 w2​的更新过程。以下是详细的步骤和计算过程: 初始化参数 初始权值&#xff1a…

【博主推荐】 Microi吾码开源低代码平台,快速建站,提高开发效率

🍬引言 🍬什么是低代码平台? 低代码平台(Low-Code Platform)是一种使开发人员和业务用户可以通过图形化界面和少量的编程来创建应用程序的开发工具。与传统的编程方式相比,低代码平台大大简化了开发过程&a…

opencv CV_TM_SQDIFF未定义标识符

opencv CV_TM_SQDIFF未定义标识符 opencv4部分命名发生变换,将CV_WINDOW_AUTOSIZE改为WINDOW_AUTOSIZE;CV_TM_SQDIFF_NORMED改为TM_SQDIFF_NORMED。

理解 Tomcat 架构与自定义实现

前言 Tomcat 是一个轻量级的 Web 容器,被广泛应用于 Java Web 开发中。通过它,我们可以轻松地部署和运行 Web 应用。在本文中,我们将深入分析 Tomcat 的核心架构,同时结合一段代码,手动实现一个简化的 Tomcat 服务&am…

Harbor 仓库部署(docker-compose 部署方式)

一、 安装的前提条件 硬件 资源 最低 推荐 cpu2C4C内存4G8G硬盘40G160G 软件 软件 版本 描述 dockerv17.0.6-ce 安装参考官方文档 Install Docker Engine | Docker Documentation docker-composev1.18.0 安装参考官方文档 Overview | Docker Documentation Openssllatest…

使用Llama 3.1创建合成数据集以调优你的大型语言模型

使用Llama 3.1创建合成数据集以调优你的大型语言模型 在数据驱动的人工智能领域,数据是核心资产。开发高质量数据集既复杂又昂贵,因此很多实验室和开发者选择使用合成数据集。本文将介绍如何利用大型语言模型Llama 3.1 405B创建合成数据集,并…

用户界面软件02

基于表单的用户界面 在“基于表单的用户界面”里面,用户开始时选中某个业务处理(模块),然后应用程序就使用一系列的表单来引导用户完成整个处理过程。大型机系统上的大部分用户界面都是这样子的。[Cok97]中有更为详细的讨论。 面…

go如何从入门进阶到高级

针对Go语言的学习,不同阶段应采取不同的学习方式,以达到最佳效果.本文将Go的学习分为入门、实战、进阶三个阶段,下面分别详细介绍 一、社区 Go语言中文网 作为专注于Go语言学习与推广的平台,Go语言中文网为开发者提供了丰富的中…

采用标准化的方式开展设计-研发中运用设计模式

概述 实现规范化、标准化的引导式设计,以业务需求为输入,识别业务特点,并通过引导式设计,找到最适合的设计模式、具体方案,汇总成为应用的设计,拉齐各应用的设计一的致性。 采用标准化的方式开展设计…

Qt之屏幕录制设计(十六)

Qt开发 系列文章 - screencap(十六) 目录 前言 一、实现原理 二、实现方式 1.创建录屏窗口 2.录屏窗口类定义 3.自建容器对象定义 4.用户使用 5.效果演示 总结 前言 利用Qt实现屏幕录制设计,可以通过使用Qt自带的类QScreen、QPixma…

【C语言程序设计——选择结构程序设计】求阶跃函数的值(头歌实践教学平台习题)【合集】

目录😋 任务描述 相关知识 1. 选择结构基本概念 2. 主要语句类型​(if、if-else、switch) 3. 跃迁函数中变量的取值范围 4. 计算阶跃函数的值 编程要求 测试说明 通关代码 测试结果 任务描述 本关任务:输入x的值&#x…

自定义字典转换器用于easyExcel 导入导出

文章目录 引言I 字典转换器、注解、序列化器注解定义自定义字典转换器用于easyExcel 导入导出自定义字典序列化器II 字典存储设计数据库表结构redis缓存引言 需求导入Excel时,根据字典内容或者字段编码转换 导出Excel时,根据字典内容或者字段编码转换 接口响应数据序列化时,…