动态扩容神技!手写 Redis 渐进式 rehash,面试官直接跪拜!

摘要 (#摘要)

引言

Redis 的哈希表性能关键在于动态扩缩容,而 渐进式 rehash 是其核心优化,避免一次性 rehash 导致的性能抖动。在高并发场景下,渐进式 rehash 分批迁移数据,保持低延迟,广泛应用于 Redis 的字典实现。面试中,“如何实现 Redis 的渐进式 rehash?” 是大厂高频考题,考察你对哈希表优化和并发设计的理解。继前六篇实现基础缓存、键过期、AOF、LRU、多种淘汰策略和自定义 HashMap 后,本篇带你手写 ProgressiveHashMap,实现渐进式 rehash,整合到 ProgressiveCache,让你在面试中直接让面试官跪拜!
目标:

  • 实现 ProgressiveHashMap,支持渐进式 rehash 和动态扩缩容。

  • 整合到 ProgressiveCache,兼容 TTL、AOF 和淘汰策略。

  • 提供可运行代码、测试用例、图表和面试要点,助你深入学习。

适合人群:

  • 想深入理解 Redis 哈希表优化的开发者。

  • 准备大厂面试、需手写渐进式 rehash 的程序员。

  • 对高并发数据结构和性能优化感兴趣的初学者和进阶者。
    前置工作:

  • 环境:Java 8+,Maven/Gradle。

  • 依赖:前六篇的 SimpleCache、ExpiryCache、AofCache、LruCache、MultiEvictionCache、HashMapCache 类(本篇继承)。

  • 代码仓库:https://github.com/codeInbpm/redis-handwritten.git。

  • 建议:运行前六篇代码后,跟着本篇敲代码,体验渐进式 rehash 的性能魔法!
    渐进式rehash

为什么需要渐进式 rehash?

传统哈希表(如 CustomHashMap)在扩容时一次性 rehash 所有键,导致高延迟,尤其在高并发或大数据量场景下。Redis 的渐进式 rehash 通过以下方式优化:

  • 分批迁移:每次 get 或 put 只迁移部分键,分摊开销。

  • 双表结构:维护旧表和新表,查询时检查两表,逐步迁移。

  • 动态缩容:支持容量缩减,优化内存使用。

  • 高并发:避免长时间阻塞,适合实时系统。

面试常见问题:

  • “Redis 的渐进式 rehash 如何实现?与一次性 rehash 相比优势是什么?”

  • “如何在高并发场景下保证 rehash 一致性?”

  • “缩容的触发条件和实现方式是什么?”
    本篇将实现 ProgressiveHashMap,支持渐进式扩缩容,整合到 ProgressiveCache,并对比 Redis 和 JDK HashMap 的 rehash 机制。

实现步骤(详细展开)

以下是实现 ProgressiveHashMapProgressiveCache 的五个详细步骤,确保用户清晰理解,易于学习和实践。

步骤 1:设计渐进式 rehash 数据结构

目标:

  • 为 ProgressiveHashMap 设计支持渐进式 rehash 的数据结构。

详细说明:
双表结构:

  • 维护两个数组:table0(旧表)和 table1(新表)。

  • rehashIdx:记录当前迁移的桶索引,初始为 -1(无 rehash)。

Entry 类:

  • 继承 CustomHashMap.Entry,新增 expiry 字段支持 TTL:
    key, value, hash, next(同前篇)。

  • expiry:过期时间,兼容 TTL。

状态管理:

  • size:总键值对数量(两表之和)。

  • capacity0, capacity1:旧表和新表容量。

  • loadFactor:负载因子(如 0.75)。

  • rehashStep:每次操作迁移的桶数(如 1)。

扩缩容触发:

  • 扩容:size > capacity0 * loadFactor 且无 rehash 进行。

  • 缩容:size < capacity0 * lowLoadFactor(如 0.1)且无 rehash 进行。

要求:

  • 定义 Entry 类,兼容 TTL。
  • 使用 table0 和 table1 支持渐进式迁移。
  • 维护 rehashIdx 和 rehashStep 控制迁移进度。

要求:

  • 异常处理(如内存不足)。
  • 为何如此设计:
  • 双表结构是 Redis 渐进式 rehash 的核心,允许分批迁移。
  • rehashIdx 跟踪进度,确保一致性。
  • 缩容支持内存优化,参考 Redis 字典设计。
    渐进式HashMap

步骤 2:实现渐进式 rehash 机制

目标:

  • 实现渐进式 rehash 逻辑,分摊扩缩容开销。

详细说明:
触发 rehash:

  • 扩容:size > capacity0 * loadFactor,创建 table1(容量翻倍)。

  • 缩容:size < capacity0 * lowLoadFactor,创建 table1(容量减半,保持 2 的幂)。

  • 设置 rehashIdx = 0,标记 rehash 开始。

渐进迁移:

  • 每次 get, put, remove 调用 progressiveRehash。
  • progressiveRehash 迁移 rehashStep 个桶(从 table0 到 table1)。
  • 迁移后更新 rehashIdx,当 rehashIdx >= capacity0 时,完成 rehash,table0 = table1,table1 = null。

查询逻辑:

  • get 和 remove 检查 table0 和 table1(若 rehash 进行中)。
  • 计算哈希索引:hash & (capacity0 - 1)(旧表)或 hash & (capacity1 - 1)(新表)。

TTL 兼容:

  • 检查 Entry.expiry,过期键直接移除。

要求:

  • 每次操作迁移少量桶(如 1-5 个),避免阻塞。
  • 查询检查双表,确保数据一致性。
  • 完成 rehash 后清理 table1 和 rehashIdx。
  • 异常处理空输入、无效 TTL。

为何如此设计:

  • 分批迁移降低单次操作延迟,适合高并发。

  • 双表查询保证数据不丢失。

  • Redis 字典使用类似机制,rehashStep 控制迁移速度。
    渐进式rehash流程

步骤 3:整合到缓存系统

目标:

  • 将 ProgressiveHashMap 整合到 ProgressiveCache,替换 CustomHashMap,兼容前篇功能。

详细说明:
继承结构:

  • ProgressiveCache 继承 HashMapCache。

  • 替换 CustomHashMap<String, Node> 为 ProgressiveHashMap<String, Node>。

功能兼容:

  • LFU:更新 freqMap 和 accessCount。
  • CLOCK:维护 referenceBit 和 clockHand。
  • FIFO:维护 insertionOrder。
  • TTL:检查 Node.expiry。
  • AOF:调用父类的 appendToAof 和 loadAof。

操作调整:

  • get:检查双表,触发 progressiveRehash,更新淘汰策略状态。
  • put:插入或更新节点,触发 rehash,同步更新链表和频率映射。
  • removeEvictedNode:从双表移除节点。

要求:

  • ProgressiveHashMap 接口与 CustomHashMap 一致。
  • 保留 LFU/CLOCK/FIFO、TTL 和 AOF 功能。
  • rehash 不影响淘汰策略和持久化。
  • 保持 O(1) 或近似 O(1) 性能。

为何如此设计:

  • 替换 CustomHashMap 验证渐进式 rehash 的正确性。
  • 兼容前篇功能,确保系统完整性。
  • 为最终章(第8篇,重构与扩展)铺路。
    ProgressiveCache整合

步骤 4:优化扩缩容性能

目标:

  • 优化渐进式 rehash 的性能,降低延迟和内存开销。
    详细说明:
    迁移步长:
  • 设置 rehashStep = 1(默认),每次操作迁移 1 个桶。
  • 可动态调整(如高负载时增加步长)。
    内存管理:
  • 迁移完成即释放 table1,避免内存浪费。
  • 缩容时确保新容量不低于最小值(如 16)。
    查询优化:
  • 若 rehashIdx = -1,仅查询 table0,减少开销。
  • 优先检查 table1(若迁移进度较高),降低双表查询成本。

并发考虑:

  • 当前实现非线程安全,第8篇引入锁或无锁优化。
  • rehash 过程中保持数据一致性。’

要求:

  • 控制 rehashStep,平衡性能和延迟。

  • 优化双表查询,优先检查可能存在的表。

  • 异常处理内存分配失败。

为何如此设计:

  • 小步长迁移适合高并发,参考 Redis 的 ht[0] 和 ht[1] 设计。
  • 内存释放和查询优化提升效率。
  • 为并发优化(第8篇)预留空间。
    渐进式与一次性再哈希延迟

步骤 5:添加测试用例

目标:

  • 通过 JUnit 测试验证 ProgressiveHashMap 和 ProgressiveCache 的功能,覆盖 rehash、扩缩容、淘汰、TTL 和 AOF。

详细说明:
测试场景:

  • ProgressiveHashMap:验证 put, get, remove,渐进式 rehash,扩缩容。

  • ProgressiveCache:验证 LFU/CLOCK/FIFO 淘汰、TTL 过期、AOF 恢复。

  • 异常处理:测试空键/值、无效 TTL、内存不足。

  • 性能:插入大量键,验证 rehash 进度和数据一致性。

实现细节:

  • 使用 @Before 和 @After 清理 AOF 文件。

  • 测试扩容(插入超过容量)和缩容(移除大量键)。

  • 通过 Thread.sleep 测试 TTL 过期。

  • 模拟重启验证 AOF 恢复。

用户学习点:

  • 提供详细测试代码,读者可直接运行。

  • 附带输出解释,展示 rehash 行为。

要求:

  • 测试覆盖所有功能和边界条件。

  • 验证 rehash 过程中数据一致性。

  • 提供清晰断言和错误信息。

为何如此设计:

  • 全面测试确保代码可靠性。

  • 模拟真实场景(扩容、缩容、重启)帮助理解 rehash。

  • 详细注释便于学习和调试。
    在这里插入图片描述

核心代码实现

以下是 ProgressiveHashMapProgressiveCache 的完整实现,存放在 ProgressiveHashMap.java 和 ProgressiveCache.java。

代码 1:ProgressiveHashMap

public class ProgressiveHashMap<K, V> {
   
   
    private static class Entry<K, V> {
   
   
        K key;
        V value;
        int hash;
        long expiry;
        Entry<K, V> next;
        Entry(K key, V value, int hash, long expiry) {
   
   
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.expiry = expiry;
        }
    }

    private Entry<K, V>[] table0;
    private Entry<K, V>[] table1;
    private int size;
    private int capacity0;
    private int capacity1;
    private final float loadFactor;
    private final float lowLoadFactor;
    private int rehashIdx;
    private final int rehashStep;
    private static final int INITIAL_CAPACITY = 16;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    private static final float LOW_LOAD_FACTOR = 0.1f;

    @SuppressWarnings("unchecked")
    public ProgressiveHashMap() {
   
   
        this.capacity0 = INITIAL_CAPACITY;
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        this.lowLoadFactor = LOW_LOAD_FACTOR;
        this.table0 = new Entry[INITIAL_CAPACITY];
        this.table1 = null;
        this.rehashIdx = -1;
        this.rehashStep = 1;
    }

    public V get(K key) {
   
   
        if (key == null) throw new IllegalArgumentException("Key cannot be null");
        progressiveRehash();
        int hash = hash(key);
        V value = findInTable(table0, capacity0, hash, key);
        if (value == null && table1 != null) {
   
   
            value = findInTable(table1, capacity1, hash, key);
        }
        return value;
    }

    public void put(K key, V value, long ttlMillis) {
   
   
        if (key == null || value == null) throw new IllegalArgumentException("Key or value cannot be null");
        if (ttlMillis <= 0) throw new IllegalArgumentException("TTL must be positive");

        progressiveRehash();
        int hash = hash(key);

        // Check if rehash is needed
        if (rehashIdx == -1) {
   
   
            if (size >= capacity0 * loadFactor) {
   
   
                startRehash(capacity0 * 2);
            } else if (size < capacity0 * lowLoadFactor && capacity0 > INITIAL_CAPACITY) {
   
   
                startRehash(capacity0 / 2);
            }
        }

        // Try update in table1 first if rehashing
        if (table1 != null && updateInTable
Redis渐进式rehash是一种在进行哈希表扩容或收缩时的一种渐进式处理方式。它避免了Redis阻塞的问题,但也带来了一些其他的问题。在进行渐进式rehash时,Redis需要分配一个新的哈希表,并为该哈希表分配新的大小的内存。这导致了Redis内存使用量瞬间增加,并且在Redis满容状态下,rehash操作会导致大量的Key被驱逐。 为了解决这个问题,Redis实现了辅助服务器来在读写操作时进行渐进式rehash操作。但是如果服务器比较空闲,Redis数据库将会长时间同时使用两个哈希表。为了处理这种情况,Redis周期函数在发现字典正在进行渐进式rehash操作时,会花费1毫秒的时间来帮助进行渐进式rehash操作。 总之,Redis渐进式rehash是一种有效避免阻塞的哈希表扩容或收缩方式,但在操作过程中可能会导致内存使用量增加和大量Key被驱逐的问题。为了处理这些问题,Redis使用辅助服务器和周期函数来优化渐进式rehash操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Redis详解(六)渐进式rehash机制](https://blog.youkuaiyun.com/fedorafrog/article/details/104633237)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wáng bēn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值