目录
摘要 (#摘要)
引言
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?
传统哈希表(如 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 字典设计。

步骤 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 控制迁移速度。

步骤 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篇,重构与扩展)铺路。

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

最低0.47元/天 解锁文章
2301

被折叠的 条评论
为什么被折叠?



