目录
摘要 (#摘要)
引言
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(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 固定,第八篇可动态调整以适应负载变化。