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

【面试题】如果 Redis 遇到 Hash 冲突了该怎么处理?

👉博主介绍: 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO 专家博主

⛪️ 个人社区:个人社区
💞 个人主页:个人主页
🙉 专栏地址: ✅ Java 中级
🙉八股文专题:剑指大厂,手撕 Java 八股文

在这里插入图片描述

文章目录

      • 1. Redis中的哈希表实现
      • 2. 哈希冲突解决策略
      • 3. 渐进式 rehashing
      • 4. 性能分析
      • 5. 使用 java 实现 Redis 的哈希表代码案例

1. Redis中的哈希表实现

Redis 是一个开源的内存数据结构存储系统,通常用作数据库、缓存和消息中间件。它支持多种数据结构,包括字符串(strings)、哈希表(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等。

在 Redis 中,哈希表是一种将字段(field)映射到值(value)的数据结构。哈希表非常适合存储对象,你可以把对象的每个字段看作哈希表中的一个键值对。

Redis 的哈希表内部实现是使用了动态调整大小的哈希表。它不是简单的单个哈希表,而是由两个哈希表组成的结构,这种设计是为了优化重新哈希(rehashing)的过程,使得在哈希表增长时可以平滑地进行,避免性能突然下降。

以下是 Redis 哈希表的一些关键特点:

  1. 哈希函数:Redis 使用 MurmurHash2 作为默认的哈希函数,这是一个非加密的哈希函数,用于快速计算哈希码。

  2. 冲突解决:当多个键被哈希到同一个位置时,Redis 使用链地址法来解决冲突,即每个哈希桶实际上是一个链表,存放所有哈希到该位置的键值对。

  3. 自动扩展:随着数据的增长,当哈希表的负载因子达到一定阈值时,Redis 会自动创建一个新的更大的哈希表,并逐步将旧哈希表中的数据迁移到新哈希表中,这个过程称为渐进式重新哈希(incremental rehashing)。

  4. 内存效率:为了节省内存,Redis 对小整数使用整数编码,对于短字符串采用特定的编码方式以减少内存占用。

  5. 操作命令:Redis 提供了一系列针对哈希表的操作命令,如 HSET (设置哈希表字段)、HGET (获取哈希表字段)、HDEL (删除哈希表字段) 等。

当你在 Redis 中执行哈希表相关的命令时,Redis 会根据这些命令对底层的哈希表进行相应的操作。由于 Redis 的数据都是存储在内存中的,所以哈希表的操作非常迅速。不过,需要注意的是,如果哈希表变得非常大,那么它的内存消耗也会相应增加,可能会影响到其他数据结构的可用内存空间。因此,在实际应用中需要考虑好内存使用的平衡。

2. 哈希冲突解决策略

哈希冲突是指在哈希表中,两个或多个不同的键经过哈希函数计算后得到相同的哈希值的情况。为了解决哈希冲突,通常会采用以下几种策略:

  1. 链地址法(Separate Chaining)

    • 每个哈希桶(bucket)实际上是一个链表或其他动态数据结构。
    • 当发生冲突时,将具有相同哈希值的元素添加到同一个桶中的链表里。
    • 优点是实现简单,且可以很好地处理大量冲突的情况。
    • 缺点是如果链表过长,查找效率可能会下降。
  2. 开放地址法(Open Addressing)

    • 当遇到冲突时,在哈希表内寻找下一个空闲的位置来存储元素。
    • 常见的探测方法有:
      • 线性探测(Linear Probing):按照顺序向后查找下一个空位。
      • 二次探测(Quadratic Probing):按照一个平方序列来查找下一个位置。
      • 双重哈希(Double Hashing):使用第二个哈希函数来决定下一步探测的距离。
    • 优点是可以减少内存开销,因为不需要额外的数据结构来存储冲突项。
    • 缺点是随着负载因子增加,性能可能显著降低,尤其是当哈希表接近满载时。
  3. 再哈希法(Rehashing)

    • 当发生冲突时,尝试使用另一个哈希函数来找到一个新的哈希值。
    • 这种方法需要事先准备好多个哈希函数。
    • 优点是可以进一步分散元素,减少冲突的可能性。
    • 缺点是实现起来较为复杂,并且需要额外的空间和时间来存储和计算多个哈希函数。
  4. 建立公共溢出区(Bucket Overflow)

    • 在哈希表之外设立一个公共区域,用来存放所有产生冲突的元素。
    • 优点是简化了哈希表内部的结构。
    • 缺点是增加了访问冲突元素时的开销,因为每次都需要检查这个溢出区。
  5. 伪随机数函数法(Pseudo-random Number Generator)

    • 通过某种算法或者随机数生成器,在产生哈希值时加上一个随机数,从而使得哈希冲突几乎不可能发生。
    • 实现难度较大,但在某些特定情况下能够有效减少冲突。

Redis 主要使用的是链地址法来解决哈希冲突。每个哈希槽包含一个指向链表头的指针,当出现冲突时,新的元素会被链接到已存在的链表上。如果某个哈希槽的链表变得过长,影响到了性能,Redis 会执行渐进式 rehash 来重新分配哈希槽,以保持高效的操作性能。

3. 渐进式 rehashing

渐进式 rehashing 是 Redis 用来处理哈希表扩容的一种策略,它允许在不阻塞服务器的情况下逐步完成哈希表的重新散列。这种技术对于大型数据集尤其重要,因为它可以避免因一次性重哈希导致的服务中断或性能下降。

以下是渐进式 rehash 的工作原理:

  1. 触发条件:当哈希表中的条目数量达到一定阈值(负载因子)时,Redis 会决定对哈希表进行扩容。这个阈值通常是当前哈希表大小的一个比例,例如当负载因子超过某个预设值(如 5 或 0.7)时。

  2. 创建新哈希表:Redis 创建一个新的更大的哈希表,并且保留旧的哈希表。新的哈希表通常比旧的大两倍左右。

  3. 双哈希表结构:此时,Redis 将使用两个哈希表,一个旧的和一个新的。所有的读写操作都会同时检查这两个哈希表。

  4. 逐步迁移:Redis 在每次执行写命令(如 HSET、HDEL 等)时,除了正常处理这些命令外,还会将一些键值对从旧哈希表迁移到新哈希表中。这个过程是逐渐进行的,每次只迁移一小部分数据。

  5. 迁移过程:在迁移过程中,Redis 会维护一个索引 rehashidx,该索引指向正在被迁移的桶。每当有写操作发生时,Redis 会从 rehashidx 开始,向后迁移直到遇到空桶或者完成了所有桶的迁移。然后更新 rehashidx,以便下次继续从下一个桶开始迁移。

  6. 结束条件:当所有的键值对都被成功迁移到新哈希表后,旧哈希表会被释放,rehashidx 被设置为 -1,表示 rehash 完成。此时,只有新的哈希表在使用。

通过这种方式,渐进式 rehash 可以让 Redis 在不影响服务的情况下扩展其内部哈希表。这种方法保证了即使是在非常大的数据集上,Redis 也能够平稳地运行,不会因为哈希表的重新构建而出现长时间的停顿。

4. 性能分析

渐进式 rehashing 在 Redis 中被设计为一种优化策略,以减少哈希表扩容时的性能影响。下面是关于这种机制的一些性能分析:

优点

  1. 低延迟

    • 渐进式 rehashing 可以避免一次性重哈希带来的长时间阻塞。在大型数据集上进行一次性重哈希可能会导致显著的服务中断。
    • 通过将重哈希过程分散到多个写操作中,每次只迁移一小部分数据,这样可以确保每个单独的操作仍然保持较低的延迟。
  2. 平滑性能

    • 由于重哈希是在后台逐步完成的,因此它可以平滑地分配 CPU 和内存资源,不会突然增加服务器的负载。
    • 这种方式使得即使在进行重哈希的过程中,Redis 也能维持相对稳定的性能表现。
  3. 高效利用资源

    • 渐进式 rehashing 利用了原本可能闲置的 CPU 时间片,在执行常规命令的同时完成重哈希任务。
    • 它有效地利用了系统资源,提高了整体效率。

潜在缺点

  1. 读取开销

    • 在重哈希过程中,读取操作需要检查两个哈希表(旧的和新的),这会稍微增加读取操作的时间。
    • 不过,这个额外的开销通常是很小的,并且随着重哈希过程的推进,最终只会检查一个新的哈希表。
  2. 内存使用

    • 在重哈希期间,Redis 需要同时维护两个哈希表,这意味着内存使用量会暂时增加。
    • 对于内存受限的环境,这可能是一个需要考虑的因素。
  3. 写入开销

    • 写入操作会承担一部分重哈希的工作,这意味着它们会比正常情况下花费更多时间。
    • 尽管每次写的额外开销不大,但如果写操作非常频繁,累积起来的开销可能会变得明显。
  4. 复杂性

    • 渐进式 rehashing 的实现相对较为复杂,需要小心处理并发访问、错误恢复等情况。
    • 这种复杂性增加了代码维护的成本。

总体来说,渐进式 rehashing 是一个有效的技术,它允许 Redis 在不显著影响性能的情况下管理大型数据集。对于大多数应用场景而言,它的优势远大于潜在的缺点。不过,在特定情况下,比如对延迟要求极高的应用或内存极其有限的环境中,开发者可能需要权衡这些因素并采取适当的措施。

5. 使用 java 实现 Redis 的哈希表代码案例

实现一个类似于 Redis 的哈希表结构,可以使用 HashMap 作为基础,并模拟一些 Redis 哈希表的特性。下面是一个简单的例子,展示了如何创建一个支持基本操作(如添加、获取和删除键值对)的哈希表。

首先,我们需要定义一个类来表示哈希表,并提供相应的操作方法。这里我们将使用 Java 的 HashMap 来存储数据,同时实现渐进式 rehashing 的概念。

import java.util.HashMap;
import java.util.Map;public class SimpleRedisHashTable<K, V> {private Map<K, V> table;private int threshold; // 触发rehash的阈值private float loadFactor; // 负载因子private boolean isRehashing = false; // 是否正在进行rehashprivate int rehashIdx = -1; // 当前rehash的位置public SimpleRedisHashTable(int initialCapacity, float loadFactor) {if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " + loadFactor);this.loadFactor = loadFactor;this.threshold = (int)(initialCapacity * loadFactor);this.table = new HashMap<>(initialCapacity);}public V put(K key, V value) {if (isRehashing) {// 在进行rehash时,逐步迁移旧表中的元素for (int i = 0; i < 10 && rehashIdx != -1; i++) { // 迁移10个元素rehashStep();}} else {// 如果达到阈值,则开始rehashif (table.size() >= threshold) {startRehash();}}return table.put(key, value);}private void startRehash() {int newCapacity = table.size() * 2;threshold = (int)(newCapacity * loadFactor);Map<K, V> oldTable = table;table = new HashMap<>(newCapacity);rehashIdx = 0;isRehashing = true;}private void rehashStep() {if (rehashIdx >= oldTable.size()) {isRehashing = false;rehashIdx = -1;return;}K key = (K) oldTable.keySet().toArray()[rehashIdx];V value = oldTable.get(key);table.put(key, value);rehashIdx++;}public V get(K key) {return table.get(key);}public V remove(K key) {return table.remove(key);}public static void main(String[] args) {SimpleRedisHashTable<String, String> hashTable = new SimpleRedisHashTable<>(16, 0.75f);hashTable.put("name", "Alice");hashTable.put("age", "30");System.out.println("Name: " + hashTable.get("name"));System.out.println("Age: " + hashTable.get("age"));hashTable.remove("age");System.out.println("Age after removal: " + hashTable.get("age"));}
}

代码实现了以下功能:

  • 使用 HashMap 存储键值对。
  • 设置了一个负载因子,当哈希表的大小超过该因子乘以容量时触发 rehash。
  • 提供了 put 方法来插入键值对,并在需要时启动 rehash。
  • startRehash 方法初始化一个新的更大容量的 HashMap 并开始渐进式 rehash。
  • rehashStep 方法负责将旧表中的元素逐步迁移到新表中。
  • 提供了 getremove 方法来获取和删除键值对。

精彩专栏推荐订阅:在下方专栏👇🏻
✅ 2023年华为OD机试真题(A卷&B卷)+ 面试指导
✅ 精选100套 Java 项目案例
✅ 面试需要避开的坑(活动)
✅ 你找不到的核心代码
✅ 带你手撕 Spring
✅ Java 初阶

在这里插入图片描述


http://www.mrgr.cn/news/57616.html

相关文章:

  • PyTorch 中各类损失函数介绍
  • 报表工具怎么选?山海鲸VS帆软,哪个更适合你?
  • PeptidesFunctionalDataset(helpers.dataset_classes文件中的lrgb.py)
  • 算法专题七: 分治归并
  • 视频云存储/音视频流媒体视频平台EasyCVR视频汇聚平台在欧拉系统中启动失败是什么原因?
  • 前端流式输出3种实现
  • 代码随想录-哈希表-快乐数
  • 安装报错解决:No module named ‘quaternion‘
  • 无线路由器设置成交换机(补充)
  • kotlin等待异步任务完成
  • 植入感体感游戏
  • 嵌入式编程守则
  • 计算机xapofx1_5.dll丢失怎么办,分享5种有效的解决方法
  • 基于SpringBoot的在线拍卖系统【源码+论文】
  • 4466 最长连续重复字符(longest)
  • 数据库中DDL、DML、DCL的区别是什么
  • 免费ppt模板从哪找?盘点精美ppt模板下载方法
  • 迅策科技累亏3.63亿:应收账款周转天数飙升,净收入留存率大幅下滑
  • PE(市盈率)、PB(市净率)、PS(市销率)和PCF(市现率)评估股票是否具有投资价值的重要指标
  • Error in cpuinfo: prctl(PR_SVE_GET_VL) failed 错误记录
  • 速腾聚创与广汽埃安签订战略合作,新增多款车型定点
  • 在Java中,需要每120分钟刷新一次的`assetoken`,并且你想使用Redis作为缓存来存储和管理这个令牌
  • LeetCode每日一题3185---构成整天的下标对数目 II
  • Python基础学习(四)程序控制结构
  • 199116-50-2,Mito-Tracker Orange CMTMRos是一种高亲和力的线粒体染色剂
  • 02 P1223 排队接水