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

金蝶云ERP远程办公指南:零公网IP实现安全内网穿透访问 本文详细介绍了如何利用内网穿透技术,在无需公网IP和复杂路由器设置的情况下,安全远程访问内网部署的金蝶云ERP系统。通过使用cpolar工具,逐步指导用户创建加密隧道、配置固定域名,并实现管理中心与数据中心的远程登录,有效解决了企业员工在外出或居家办公时的核心业务系统访问难题。 阅读详情

摘要 (#摘要)

引言

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 机制。

实现步骤(详细展开)

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

步骤 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。

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

核心代码实现

以下是 ProgressiveHashMap 和 ProgressiveCache 的完整实现,存放在 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(table1, capacity1, hash, key, value, ttlMillis)) {
            return;
        }
        if (updateInTable(table0, capacity0, hash, key, value, ttlMillis)) {
            return;
        }

        // Insert new entry
        Entry<K, V>[] targetTable = table1 != null && rehashIdx > capacity0 / 2 ? table1 : table0;
        int targetCapacity = table1 != null && rehashIdx > capacity0 / 2 ? capacity1 : capacity0;
        int index = hash & (targetCapacity - 1);
        Entry<K, V> newEntry = new Entry<>(key, value, hash, System.currentTimeMillis() + ttlMillis);
        newEntry.next = targetTable[index];
        targetTable[index] = newEntry;
        size++;
    }

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

    public int size() {
        return size;
    }

    private int hash(K key) {
        int h = key.hashCode();
        return (h ^ (h >>> 16)) & 0x7fffffff;
    }

    private V findInTable(Entry<K, V>[] table, int capacity, int hash, K key) {
        int index = hash & (capacity - 1);
        for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
            if (entry.hash == hash && key.equals(entry.key)) {
                if (System.currentTimeMillis() > entry.expiry) {
                    return null;
                }
                return entry.value;
            }
        }
        return null;
    }

    private boolean updateInTable(Entry<K, V>[] table, int capacity, int hash, K key, V value, long ttlMillis) {
        int index = hash & (capacity - 1);
        for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
            if (entry.hash == hash && key.equals(entry.key)) {
                entry.value = value;
                entry.expiry = System.currentTimeMillis() + ttlMillis;
                return true;
            }
        }
        return false;
    }

    private V removeFromTable(Entry<K, V>[] table, int capacity, int hash, K key) {
        int index = hash & (capacity - 1);
        Entry<K, V> prev = null;
        for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
            if (entry.hash == hash && key.equals(entry.key)) {
                if (prev == null) {
                    table[index] = entry.next;
                } else {
                    prev.next = entry.next;
                }
                size--;
                return entry.value;
            }
            prev = entry;
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    private void startRehash(int newCapacity) {
        if (rehashIdx != -1) return; // Avoid concurrent rehash
        try {
            table1 = new Entry[newCapacity];
            capacity1 = newCapacity;
            rehashIdx = 0;
        } catch (OutOfMemoryError e) {
            throw new RuntimeException("Failed to start rehash due to insufficient memory", e);
        }
    }

    private void progressiveRehash() {
        if (rehashIdx == -1 || table1 == null) return;
        for (int i = 0; i < rehashStep && rehashIdx < capacity0; i++, rehashIdx++) {
            Entry<K, V> entry = table0[rehashIdx];
            while (entry != null) {
                Entry<K, V> next = entry.next;
                if (System.currentTimeMillis() <= entry.expiry) {
                    int newIndex = entry.hash & (capacity1 - 1);
                    entry.next = table1[newIndex];
                    table1[newIndex] = entry;
                } else {
                    size--;
                }
                entry = next;
            }
            table0[rehashIdx] = null;
        }
        if (rehashIdx >= capacity0) {
            table0 = table1;
            capacity0 = capacity1;
            table1 = null;
            rehashIdx = -1;
        }
    }
}

代码 2:ProgressiveCache

import java.io.*;
import java.util.*;

public class ProgressiveCache extends HashMapCache {
    private static class Node {
        String key, value;
        long expiry;
        int accessCount;
        int referenceBit;
        long insertionOrder;
        Node prev, next;
        Node(String key, String value, long expiry) {
            this.key = key;
            this.value = value;
            this.expiry = expiry;
            this.accessCount = 1;
            this.referenceBit = 1;
            this.insertionOrder = insertionCounter++;
        }
    }

    private ProgressiveHashMap<String, Node> cache;
    private Node head, tail;
    private int capacity;
    private EvictionStrategy strategy;
    private TreeMap<Integer, List<Node>> freqMap;
    private Node clockHand;
    private static long insertionCounter;

    public ProgressiveCache(int capacity, EvictionStrategy strategy) {
        super(capacity, strategy);
        this.capacity = capacity;
        this.strategy = strategy;
        this.cache = new ProgressiveHashMap<>();
        this.freqMap = new TreeMap<>();
        this.head = new Node(null, null, 0);
        this.tail = new Node(null, null, 0);
        head.next = tail;
        tail.prev = head;
        this.clockHand = head;
        loadAof();
    }

    @Override
    public String get(String key) {
        if (key == null) throw new IllegalArgumentException("Key cannot be null");
        Node node = cache.get(key);
        if (node == null || System.currentTimeMillis() > node.expiry) {
            if (node != null) {
                removeNode(node);
                cache.remove(key);
                if (strategy == EvictionStrategy.LFU) updateFreqMap(node, -node.accessCount);
            }
            return null;
        }
        updateNodeAccess(node);
        return node.value;
    }

    @Override
    public void put(String key, String 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");

        Node node = cache.get(key);
        if (node != null) {
            node.value = value;
            node.expiry = System.currentTimeMillis() + ttlMillis;
            updateNodeAccess(node);
        } else {
            if (cache.size() >= capacity) {
                removeEvictedNode();
            }
            node = new Node(key, value, System.currentTimeMillis() + ttlMillis);
            cache.put(key, node, ttlMillis);
            addToHead(node);
            if (strategy == EvictionStrategy.LFU) {
                freqMap.computeIfAbsent(1, k -> new ArrayList<>()).add(node);
            }
        }
        appendToAof("PUT", key, value, ttlMillis);
    }

    private void updateNodeAccess(Node node) {
        if (strategy == EvictionStrategy.LFU) {
            updateFreqMap(node, -node.accessCount);
            node.accessCount++;
            updateFreqMap(node, node.accessCount);
            moveToHead(node);
        } else if (strategy == EvictionStrategy.CLOCK) {
            node.referenceBit = 1;
        }
    }

    private void removeEvictedNode() {
        if (strategy == EvictionStrategy.LFU) {
            Map.Entry<Integer, List<Node>> entry = freqMap.firstEntry();
            if (entry != null) {
                List<Node> nodes = entry.getValue();
                Node node = nodes.remove(nodes.size() - 1);
                if (nodes.isEmpty()) freqMap.remove(entry.getKey());
                removeNode(node);
                cache.remove(node.key);
            }
        } else if (strategy == EvictionStrategy.CLOCK) {
            while (true) {
                clockHand = clockHand.next == tail ? head.next : clockHand.next;
                if (clockHand == head) continue;
                if (System.currentTimeMillis() > clockHand.expiry) {
                    Node toRemove = clockHand;
                    clockHand = clockHand.prev;
                    removeNode(toRemove);
                    cache.remove(toRemove.key);
                    return;
                }
                if (clockHand.referenceBit == 0) {
                    removeNode(clockHand);
                    cache.remove(toRemove.key);
                    return;
                }
                clockHand.referenceBit = 0;
            }
        } else if (strategy == EvictionStrategy.FIFO) {
            Node toRemove = tail.prev;
            if (toRemove != head) {
                removeNode(toRemove);
                cache.remove(toRemove.key);
            }
        }
    }

    private void addToHead(Node node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(Node node) {
        removeNode(node);
        addToHead(node);
    }

    private void updateFreqMap(Node node, int count) {
        if (strategy != EvictionStrategy.LFU) return;
        freqMap.computeIfAbsent(count, k -> new ArrayList<>()).remove(node);
        if (freqMap.get(count) != null && freqMap.get(count).isEmpty()) {
            freqMap.remove(count);
        }
    }
}

测试代码

以下是 ProgressiveCacheTest.java,验证 ProgressiveHashMap 和 ProgressiveCache 功能。

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;

import java.io.File;

public class ProgressiveCacheTest {
    private ProgressiveCache cache;
    private File aofFile = new File("appendonly.aof");

    @Before
    public void setUp() {
        if (aofFile.exists()) aofFile.delete();
    }

    @After
    public void tearDown() {
        if (cache != null) cache.shutdown();
        if (aofFile.exists()) aofFile.delete();
    }

    @Test
    public void testProgressiveHashMapBasic() {
        ProgressiveHashMap<String, String> map = new ProgressiveHashMap<>();
        map.put("key1", "value1", 5000);
        map.put("key2", "value2", 5000);
        assertEquals("value1", map.get("key1"));
        assertEquals("value2", map.get("key2"));
        assertEquals(2, map.size());
        map.put("key1", "newValue", 5000);
        assertEquals("newValue", map.get("key1"));
        map.remove("key2");
        assertNull(map.get("key2"));
        assertEquals(1, map.size());
    }

    @Test
    public void testProgressiveRehash() {
        ProgressiveHashMap<String, String> map = new ProgressiveHashMap<>();
        for (int i = 0; i < 20; i++) {
            map.put("key" + i, "value" + i, 5000);
        }
        assertEquals(20, map.size());
        assertEquals("value10", map.get("key10"));
        // Insert more to trigger rehash
        for (int i = 20; i < 30; i++) {
            map.put("key" + i, "value" + i, 5000);
        }
        assertEquals("value25", map.get("key25"));
    }

    @Test
    public void testProgressiveCache() throws InterruptedException {
        cache = new ProgressiveCache(3, MultiEvictionCache.EvictionStrategy.LFU);
        cache.put("key1", "value1", 5000);
        cache.put("key2", "value2", 5000);
        cache.put("key3", "value3", 5000);
        for (int i = 0; i < 5; i++) cache.get("key1");
        cache.put("key4", "value4", 5000); // 移除低频 key2
        assertNull(cache.get("key2"));
        assertEquals("value1", cache.get("key1"));

        cache.put("key5", "value5", 1000);
        Thread.sleep(1500);
        assertNull(cache.get("key5")); // 过期

        cache = new ProgressiveCache(3, MultiEvictionCache.EvictionStrategy.LFU);
        assertEquals("value1", cache.get("key1")); // AOF 恢复
    }

    @Test
    public void testShrink() {
        cache = new ProgressiveCache(16, MultiEvictionCache.EvictionStrategy.FIFO);
        for (int i = 0; i < 10; i++) {
            cache.put("key" + i, "value" + i, 5000);
        }
        for (int i = 0; i < 8; i++) {
            cache.remove("key" + i);
        }
        cache.put("newKey", "newValue", 5000); // 触发缩容
        assertEquals("newValue", cache.get("newKey"));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testInvalidInput() {
        cache = new ProgressiveCache(3, MultiEvictionCache.EvictionStrategy.LFU);
        cache.put(null, "value", 1000);
    }
}

运行方式:
确保 Maven 项目包含 JUnit 依赖(同前六篇)。

将 SimpleCache.java, ExpiryCache.java, AofCache.java, LruCache.java, MultiEvictionCache.java, HashMapCache.java, ProgressiveHashMap.java, ProgressiveCache.java 和 ProgressiveCacheTest.java 放入项目。

运行测试:mvn test 或通过 IDE 执行。

测试输出(示例):

	Cache full, removed key: key2

面试要点

问:Redis 的渐进式 rehash 如何实现?

  • 答:使用双表结构(ht[0] 和 ht[1]),每次操作迁移少量桶,rehashidx 跟踪进度。查询检查双表,确保一致性。本实现用 table0, table1 和 rehashIdx,每次迁移 1 个桶。

问:渐进式 rehash 与一次性 rehash 的优势?

  • 答:渐进式 rehash 分摊迁移开销,避免高延迟,适合高并发。一次性 rehash 可能阻塞,影响实时性。本实现通过 rehashStep 控制迁移速度。

问:缩容的触发条件和实现方式?

  • 答:当 size < capacity * lowLoadFactor(如 0.1),触发缩容,容量减半。使用双表迁移,类似扩容。本实现支持动态缩容,优化内存。

下一步预告

恭喜你已征服 Redis 的渐进式 rehash,成为哈希表优化大师!系列最终章(第八篇)即将来袭,我们将 重构代码并扩展功能,打造生产级 Redis 缓存,加入并发支持、红黑树优化和动态配置,让你的实现媲美真实 Redis!敬请期待,准备好迎接大厂 offer 吧!

总结

本篇作为“Java 从零手写 Redis”系列第七篇,成功实现了 Redis 的 渐进式 rehash 机制,通过 ProgressiveHashMap 和 ProgressiveCache 实现了动态扩缩容,分摊 rehash 开销,完美适配高并发场景。基于前六篇(SimpleCache、ExpiryCache、AofCache、LruCache、MultiEvictionCache、HashMapCache),我们将 CustomHashMap 升级为支持双表结构和渐进迁移的 ProgressiveHashMap,兼容 TTL、AOF 和 LFU/CLOCK/FIFO 淘汰策略。以下是核心亮点:

核心实现:

  • 实现 ProgressiveHashMap,支持渐进式 rehash,通过 table0 和 table1 分批迁移键值对。

  • 支持动态扩容(容量翻倍)和缩容(容量减半),优化内存使用。

  • 每次操作迁移少量桶(rehashStep = 1),降低延迟,参考 Redis 字典设计。

功能兼容:

  • 整合到 ProgressiveCache,无缝支持 TTL 过期、AOF 持久化和多种淘汰策略。

  • 保持 O(1) 或近似 O(1) 的存取性能,查询双表确保数据一致性。

局限性与展望:

  • 并发安全:当前实现非线程安全,第八篇将引入锁或无锁机制。

  • 冲突优化:仍使用链表处理冲突,第八篇可引入红黑树优化高冲突场景。

  • 动态步长:rehashStep 固定,第八篇可动态调整以适应负载变化。

技术债迟早要还,但今晚的积压必须现在就破!

在这里插入图片描述

把 SAP Workflow 的 Task 搞懂:从 TS 到 WS 的建模、触发与排错全景 摘要:SAP Business Workflow中的Task是工作流设计的核心单元,分为single-step(独立执行)和multistep(流程编排)两种类型。Task设计直接影响流程的可复用性、扩展性和可排错性。系统将Task分为5类(TS/T/WS/WF/TG),运行时通过事件触发、Linkage匹配、Work Item执行的链路完成流程驱动。关键事务码包括PFTC_INS(创建Task)、SWE2(事件配置)等。正确设计Task边界并遵循"先业务提交后事件触发"原则,是构建稳定 阅读详情

相关推荐

MLC-LLM框架在安卓设备上的模型编译与部署全流程解析

本文详细解析了如何利用MLC-LLM框架在安卓设备上编译与部署大语言模型。从环境搭建、模型获取与量化编译,到最终的安卓应用打包与调试,提供了完整的实战流程。重点介绍了如何通过MLC-LLM将大型模型优化为适合移动端运行的格式,实现本地、低延迟、隐私安全的AI应用部署。

weixin_29251337的博客 475

Redis之渐进式rehash

渐进式rehash

weixin_42189550的博客 4220

PySWMM新手避坑指南:从安装到批量处理INP文件的完整流程(附常见错误解决方案)

本文为PySWMM新手提供从环境搭建到批量处理INP文件的完整避坑指南。详细讲解了虚拟环境配置、库安装方法,并针对“无法打开输入文件”和对象访问错误等高频问题提供解决方案。核心部分展示了如何利用Python脚本实现INP文件的批量运行、结果汇总以及参数自动化修改,显著提升SWMM模型的工作效率。

weixin_29092031的博客 392

渐进式ReHash

本文来说下redis中hash表的扩容问题。 文章目录概述 概述

wh柒八九的博客 1048

基于人脸识别的课堂考勤系统 tkinter+openpyxl+face_recognition

基于face_recognition库实现人脸考勤系统功能。利用Python的tkinter模块来设计考勤系统的图形化界面,此外结合openpyxl模块,实现将学生的信息和考勤打卡数据保存到表格中,方便查看和调用。该系统具备“刷脸打卡”、“新学生注册”等功能。............

lyx4949的博客 6965

TC275-AutoSAR实战:Davinci Cfg多核OS配置与调试技巧

本文详细介绍了在TC275芯片上使用Davinci Cfg工具进行AutoSAR多核操作系统配置的实战步骤与调试技巧。内容涵盖从导入arxml文件的常见“坑”开始,到配置Det、EcuC、创建多核OS实例、关联OS应用、创建任务与设置优先级调度,最后分享了利用Det日志、调试器多核视图及处理核间通信与缓存一致性等关键调试方法,旨在帮助开发者高效完成复杂车载ECU软件集成。

weixin_29219189的博客 488

渐进式rehash

在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。:上述过程中的第二步,可能会涉及大量的数据拷贝,如果一次性把哈希表1的数据拷贝到哈希表2,那么会造成Redis线程阻塞。

qq_53578500的博客 2313

大数据分析案例-基于决策树算法构建信用卡违约预测模型

本项目旨在使用机器学习等算法构建信用卡违约预测模型,主要应用在金融相关领域,根据用户以往的行为数据来预测是否会违约,有利于商业银行防范和化解信用卡风险,完善信用卡违约风险管理工作。本次实验通过探索性分析以及使用决策树构建信用卡违约模型,得出以下结论:1.可透支金额主要集中在0~200000之间。2.性别对于可透支金额的影响较小。3.受教育程度和婚姻状况对可透支金额的影响较大。4.决策树模型的准确率为0.88,模型效果还有待提高。

m0_64336780的博客 7574

Redis的渐进式Rehash

为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对。 一个哈希表,其实是一个数组,数组的每个元素称为一个哈希桶。 哈希桶中的 entry 元素中保存了 *key 和 *value 指针 哈希表存在哈希冲突问题和 rehash 可能带来的操作阻塞 Redis 解决哈希冲突的方式,就是链式哈希。就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接(叫做哈希冲突链)。 Redis 的 rehash 哈希冲突链上的元素只能通过指针逐一查找再操作。如果哈希表里写入的数据越来

weixin_44213038的博客 2214

Vue3 - 详细实现移动端H5网页调用摄像头拍照功能,微信公众号网页h5页面打开本地摄像头并点击按钮照相,vue3获取摄像头画面执行拍照保存,调整摄像头前置后置抓拍图片给用户展示或保存手机上传服务器

vue3,H5网页,手机移动端,vue3摄像头拍照,前端h5调用手机摄像头,移动端网页调用摄像头,H5实现手机拍照和选择上传,调用本地摄像头实现拍照,vue3调用摄像头拍照,手机浏览器开启摄像头并进行拍照,H5移动端网页中调用手机摄像头,H5实现web调用摄像头在线照相,vue3h5调用摄像头,微信公众号网页如何调用摄像头拍照,公众号H5页面实现打开摄像头照相,怎么生成临时图片,vue3项目调用摄像头实现拍照功能,摄像头打不开没反应,浏览器不兼容怎么办,摄像头权限不弹出,只能在本地运行,拒绝权限后怎么办,

🏆 前端领域优质创作者 1万+

手把手教你YOLOv8/YOLOv11分割训练自己数据集和推理,并教你使用Labelme工具标注数据(附分割网络结构图)

YOLOv8-seg 和 YOLOv11-seg 都是一种高效实例分割算法,实例分割模型的输出是一组勾勒出图像中每个物体的遮罩或轮廓,以及每个物体的类标签和置信度分数。当你不仅需要知道物体在图像中的位置,还需要知道它们的具体形状时,实例分割就非常有用了,功能非常强大,可以应对各种视觉任务。 YOLOv11-seg 则在 YOLOv8-seg 基础上进一步魔改了模型架构,相对分割速度可能更快,其他指标性能有待测试。实例分割可以用于自动驾驶、医疗图像处理、智能农业监控等领域。

在职AI算法工程师,擅长计算机视觉,YOLO目标检测、分割等,擅长web、pyqt界面可视化,好内容持续更新中,来这里跟大家一起学习,共同进步 4475

Redis---渐进式哈希

Redis支持的数据结构有很多,其中dict的使用非常频繁,其实Redis的每一个数据库结构都是一个dict。dict使用哈希表实现,这也是Redis性能十分强悍的原因之一,增删改查的时间复杂度为O(1). 上图是我根据Redis源码中定义的数据结构及网上资料参考画的参考图。 随着Redis的操作越来越多,dict中保存的数据量也会动态变化,当数据量增加或者减少到一定的程度,为了让负载因子维持在一个合理的范围内,Redis就会对dict的大小进行相应的扩容或者收缩。而这一过程正是通过渐进式哈希(rehas

xidianhuihui的专栏 1585

基于Java+SpringBoot+微信小程序实现奶茶点单系统

再不用担心逢年过节的第一杯奶茶有没有人送了,制作一个奶茶点单系统自己当店主,开怀畅饮,一劳永逸。

商务合作 / 项目定制 / 学习交流。个人vx:lovely_wml 1万+

【Redis】什么是渐进式rehash

字典 字典是一种用于保存键值对的抽象数据结构。比如set msg "hello redis"这个键值对就是保存在数据库字典中 字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,redis就会使用字典作为哈希键的底层实现。 redis中的字典使用哈希表作为底层实现,一个哈希表中可以有若干个哈希表节点 字典结构定义 typedef st...

真正的大师,永远怀着一颗学徒的❤ 4253

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

详解Redis的渐进式rehash

详解Redis的渐进式rehash

m0_66220639的博客 1417

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

Redis详解(六)渐进式rehash机制

在Redis中,键值对(Key-Value Pair)存储方式是由字典(Dict)保存的,而字典底层是通过哈希表来实现的。通过哈希表中的节点保存字典中的键值对。我们知道当HashMap中由于Hash冲突(负载因子)超过某个阈值时,出于链表性能的考虑,会进行Resize的操作。Redis也一样。 在redis的具体实现中,使用了一种叫做渐进式哈希(rehashing)的机制来提高字典的缩放效率,避...

傅红雪的专栏 2640

Redis核心结构以及渐进式扩容

Redis底层架构实现的总结以及提炼

at10090的博客 971

Redis面试题分享三十九:什么是渐进式hash?

在Redis的具体实现中,使用了一种叫做。

之乎者也·的博客 822

Redis:缩容、扩容、渐进式rehash

目录 1、缩容 扩容 2、渐进式rehash 1、缩容 扩容 随着redis的操作的不断执行,哈希表保存的键值会逐渐地增多或者减少,为了让哈希表的负载因子(ratio)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。 ratio= ht[0].used / ht[0].size 比如,hash表的size为4,如果已经插入了4个k-v的话,则ratio为1。 redis的默认负载因子为1,负载因子最大可以达到5(持久化的时候,需.

李白 4292

[ Redis14篇]字典之渐进式Hash结构

1.Redis字典Hash底层数据结构? Redis 字典 hash 的数据结构底层是一个 dict 对象。 用key值计算hashkey 元素插入到某个hash链上(拉链法解冲突) dict的结构: dictht table 哈希表内部的table结构 使用了拉链法来解决哈希冲突 其实是一个指向数组的指针,数组中的每一项都是entry链表的头结点。 dictht ht[2]:在dict的内部,维护了两张哈希表(可以理解为2个数组)。 作用等同于是一对滚动数组,ht[0]表是旧表,ht[1]

程序员小强的博客 1823

Redis——渐进式Rehash

用户每进行访问操作时,redis都逐渐将旧哈希表数据迁移到新哈希表;在rehash期间 两个hash表都可以访问。value类型:字典,双向链表,压缩列表,跳表,整数数组,动态字符串。当哈希表满了或达到阈值时就会进行扩容,将原有哈希表的数据。rehash需要耗费大量的时间和内存资源,为了。渐进式rehash 将哈希表分为两个哈希表。在Redis为了加快查找和访问速度 使用了。,采用了渐进式rehash 算法。旧哈希表就会被释放掉。

码鹿的记录 395

从零手写缓存框架(14)redis渐进式rehash详解

redis 的 rehash 设计 本文思维导图如下: HashMap 的 rehash 回顾 读过 HashMap 源码的同学,应该都知道 map 在扩容的时候,有一个 rehash 的过程。 没有读过也没有关系,可以花时间阅读下 从零开始手写 redis(13) HashMap源码详解 简单了解下整个过程即可。 HashMap 的扩容简介 这里简单介绍下: 扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组

1183

redis渐进式rehash

Redis的渐进式rehash是指扩展或收缩哈希表时,需要将ht[0]里面的所有键值对rehash到ht[1]里面,但这个rehash动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。Redis是当然如果这个数组一直不变,那么hash冲突会变很多,这个时候检索效率会大打折扣,所以Redis就需要把数组进行扩容(一般是扩大到原来的两倍),但是问题来了,扩容后每个hash桶的数据会分散到不同的位置,这里设计到元素的移动,必定会阻塞IO,所以这个ReHash过程会导致很多请求阻塞。

学亮编程手记 632

Redis渐进式rehash小疑问

在Redis中,Rehash是指在进行哈希表扩容或缩容时重新计算和重新分配哈希槽的过程。Redis使用哈希表来存储键值对,哈希表中的每个槽位对应一个哈希槽,每个槽位可以存储多个键值对。当哈希表的负载因子(load factor)超过一定阈值时,Redis会触发Rehash操作来扩容哈希表,以提供更多的槽位来存储新的键值对。在Rehash过程中,Redis会逐步将原来哈希表中的键值对迁移到新的哈希表中,直到所有键值对都迁移完成。

weixin_42189550的博客 1279

Redis的渐进式hash

redis的底层存储 redis把所有的key都存储在一个大字典中,学过python的小伙伴应该就会知道字典这个数据结构,但是这个数据结构跟python的数据结构不一样,这个字典跟java里面的HashMap像似, 如图所示: 我们可以看到是数组+链表的结构,我们知道的Java中的HashMap每次扩容方式是当大于LoadFactor阈值时,就需要重新分配一个2倍的数组,就需要把所有元素都重新计算hash值操作(rehash)挂载到新的数组中, 数组的大小是2,空间大小加倍 就变成了2 渐进式hash是什

qq_43338943的博客 854

Redis渐进式rehash

在Redis中,哈希表是实现快速键值对查找的关键数据结构,但随着数据的增加,哈希表可能会因为冲突过多或空间不足而需要扩容;相反,当数据大量删除后,哈希表也可能因为空间利用率过低而需要缩容。在扩容和缩容过程中,由于长度变化会导致key的索引变化,为了避免一次性rehash操作带来的长时间阻塞和性能影响,Redis采用了渐进式rehash策略。

qq_41354522的博客 1147

redis到底是怎么样进行渐进式Rehash的

Redis 默认使用了两个全局哈希表:dictht[0]和哈希表 dictht[1],一开始,当你刚插入数据时,默认使用dictht[0],此时的dictht[1] 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:1、给dictht[1]分配更大的空间,一般是当前dictht[0]已使用大小的2倍,但是必须满足是2的整数倍!2、把哈希表0 中的数据重新映射并拷贝到哈希表1 中(在hash表1下进行重新计算hash值);3、释放哈希表 0 的空间。

zhangshenglu1的博客 957
上一篇: 震撼揭秘Java泛型:T、E、K、V与通配符?的终极奥义大公开!
下一篇: 终极重构神技!手写生产级 Redis 缓存,面试官直接膜拜!
wáng bēn
博客等级 码龄7年 97粉丝 56原创
评论
成就一亿技术人!
拼手气红包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、付费专栏及课程。

余额充值