目录
引言
Redis 的超高性能离不开其底层哈希表(HashMap),它是键值存储的核心,提供 O(1) 平均时间复杂度的存取操作。在面试中,“手写一个 HashMap” 是大厂必考题,考察你对哈希函数、冲突处理、动态扩容和源码设计的理解。继前五篇实现基础缓存、键过期、AOF 持久化、LRU 和多种淘汰策略后,本篇带你手写 CustomHashMap
,替换 Java 的 HashMap,并深入解析 JDK HashMap
源码,揭开 Redis 哈希表的性能秘密,让你在面试中直接让面试官目瞪口呆!
目标
:
-
实现 CustomHashMap,支持 O(1) 存取、冲突处理和动态扩容。
-
解析 JDK HashMap 源码,揭示 Redis 哈希表的优化思路。
-
兼容 TTL、AOF 和淘汰策略,提供可运行代码和测试用例。
-
提供详细步骤、图表和面试要点,助你深入学习。
适合人群:
-
想深入理解 Redis 哈希表和 JDK HashMap 源码的开发者。
-
准备大厂面试、需手写 HashMap 的程序员。
-
对数据结构、源码分析和性能优化感兴趣的初学者和进阶者。
前置工作:
-
环境:Java 8+,Maven/Gradle。
-
依赖:前五篇的 SimpleCache、ExpiryCache、AofCache、LruCache、MultiEvictionCache 类(本篇继承)。
-
代码仓库:https://github.com/codeInbpm/redis-handwritten。
-
建议:运行前五篇代码后,跟着本篇敲代码,体验哈希表的性能魔法!
Redis 哈希表应用场景
为什么需要自定义 HashMap?
Redis 的键值存储依赖高效的哈希表实现,类似 Java 的 HashMap
,提供以下核心特性:
-
O(1) 存取:哈希函数将键映射到数组索引,平均 O(1) 复杂度。
-
冲突处理:通过链表(或红黑树)解决哈希冲突。
-
动态扩容:当负载因子超限时,自动扩容保持性能。
-
内存优化:Redis 定制哈希表,结合渐进式 rehash(第7篇)提升并发性能。
面试常见问题:
-
“如何实现一个高效的 HashMap?哈希函数如何设计?”
-
“HashMap 的冲突处理和扩容机制是什么?”
-
“Redis 的哈希表与 Java HashMap 有何不同?”
-
“JDK HashMap 的源码优化了哪些细节?”
本篇将实现 CustomHashMap
,替换 Java 的 HashMap 用于 HashMapCache
,并深入解析 JDK HashMap 源码(Java 8),揭示 Redis 哈希表的底层原理,为第7篇的渐进式 rehash铺路。
CustomHashMap的实现步骤(详细展开)
步骤 1:设计哈希表数据结构
目标:
为 CustomHashMap 设计高效的数据结构,支持键值存取和冲突处理。
详细说明:
- 使用数组存储桶(buckets),每个桶是一个单向链表,存储键值对。
- Entry 类:定义节点,包含:
- key, value:键值对。
- hash:预计算的哈希值,优化查找。
- next:指向链表下一节点,处理冲突。
容量和负载因子:
- 初始容量为 16,负载因子为 0.75。
- 当元素数量超过 capacity * loadFactor,触发扩容。
兼容性:
- 设计与 Java HashMap 一致的接口,支持前篇的 TTL、AOF 和淘汰策略。
初始设置:
- 数组 Entry[] table 初始化为 16。
- 容量为 2 的幂,优化哈希计算。
要求:
- 定义 Entry 类,包含 key, value, hash, next。
- 使用 Entry[] table 存储桶。
- 支持 put, get, remove, size 方法。
- 确保容量为 2 的幞,哈希计算使用位运算。
为何如此设计:
- 数组 + 链表是 HashMap 的经典结构,简单高效,平均 O(1)。
- 预计算哈希值减少重复计算,提升性能。
- 负载因子和 2 的幂容量是 JDK HashMap 的优化实践,Redis 也采用类似设计。
CustomHashMap 数据结构
步骤 2:实现哈希函数和冲突处理
目标:
- 设计高效哈希函数,处理键冲突,确保 O(1) 平均存取。
详细说明:
哈希函数:
-
使用 key.hashCode() 生成初始哈希值。
-
优化分散性:hash = (h ^ (h >>> 16)) & (capacity - 1),高位参与计算,减少冲突。
-
容量为 2 的幞,& (capacity - 1) 等价于取模,性能更高。
冲突处理:
-
同一桶内冲突时,追加节点到链表头部(O(1) 插入)。
-
查找时遍历链表,比较 hash 和 key.equals()。
操作流程:
-
get(key):计算哈希,定位桶,遍历链表,匹配键后返回 value。
-
put(key, value):计算哈希,定位桶,若键存在则更新 value,否则插入新节点。
-
remove(key):计算哈希,定位桶,移除匹配节点。
-
TTL 兼容:节点存储 expiry,get 时检查过期时间。
要求:
-
哈希函数确保均匀分布,减少冲突概率。
-
链表操作保持 O(1) 插入和删除。
-
兼容 TTL 检查,过期键返回 null。
-
异常处理空键或值。
为何如此设计:
-
高位异或优化哈希分布,参考 JDK HashMap 的 hash 方法。
-
链表头部插入减少操作复杂度。
-
TTL 集成保持与前篇一致性。
哈希表操作流程
步骤 3:实现动态扩容
目标:
实现哈希表动态扩容,保持性能稳定。
详细说明:
扩容触发:
-
当 size > capacity * loadFactor(如 0.75),触发扩容。
-
新容量为原容量两倍(如 16 -> 32)。
扩容流程:
-
创建新数组 newTable,容量翻倍。
-
遍历旧数组的每个节点,重新计算哈希,分配到新桶。
-
更新 table 引用,释放旧数组。
优化:
-
容量为 2 的幞,哈希计算使用 & (capacity - 1)。
-
链表节点批量转移,减少拷贝开销。
-
TTL 和 AOF:扩容不影响 TTL 检查和 AOF 记录。
-
异常处理:捕获内存不足异常,确保健壮性。
要求:
-
扩容后键值对完整性无损。
-
尽量减少 rehash 时间开销。
-
保持容量为 2 的幞。
为何如此设计:
-
动态扩容防止链表过长,维持 O(1) 平均复杂度。
-
2 的幞容量简化哈希计算,参考 JDK 和 Redis 设计。
-
异常处理提升生产级可靠性。
步骤 4:整合到缓存系统并解析 JDK 源码
目标:
将 CustomHashMap 整合到 HashMapCache,替换 Java HashMap,并解析 JDK HashMap 源码。
详细说明:
整合到 HashMapCache:
-
HashMapCache 继承 MultiEvictionCache。
-
替换 Map<String, Node> 为 CustomHashMap<String, Node>。
-
调整 get, put, removeEvictedNode 方法,使用 CustomHashMap 接口。
-
兼容 LFU/CLOCK/FIFO、TTL 和 AOF 逻辑。
JDK HashMap 源码解析:
- 哈希函数:hash = (h ^ (h >>> 16)) 优化分散性。
- 冲突处理:Java 8 中,链表长度超过 8 时转为红黑树,减少 O(n) 退化。
- 扩容:容量翻倍,节点重新分配,树化/解树化处理。
- Redis 对比:Redis 使用自定义哈希表,支持渐进式 rehash(第7篇),内存更紧凑。
操作调整:
- get:检查 TTL,更新淘汰策略状态(如 LFU 的 accessCount)。
- put:插入或更新节点,触发扩容时同步更新链表和频率映射。
- AOF:调用父类的 appendToAof 和 loadAof。
要求:
-
CustomHashMap 接口与 Java HashMap 一致。
-
保留 LFU/CLOCK/FIFO、TTL 和 AOF 功能。
-
解析 JDK 源码,提取 Redis 相关优化点。
-
保持 O(1) 或近似 O(1) 性能。
为何如此设计:
- 替换 Java HashMap 验证自定义实现的正确性。
- 源码解析帮助理解 Redis 哈希表的优化思路。
- 兼容前篇功能,为渐进式 rehash(第7篇)铺路。
HashMapCache 整合与源码解析
步骤 5:添加测试用例
目标:
- 通过 JUnit 测试验证 CustomHashMap 和 HashMapCache 的功能,覆盖存取、冲突、扩容、淘汰、TTL 和 AOF。
详细说明:
测试场景:
- CustomHashMap:验证 put, get, remove,冲突处理,动态扩容。
- HashMapCache:验证 LFU/CLOCK/FIFO 淘汰、TTL 过期、AOF 恢复。
- 异常处理:测试空键/值、无效 TTL。
- 性能:插入大量键,验证扩容正确性。
实现细节:
- 使用 @Before 和 @After 清理 AOF 文件和资源。
- 测试扩容(插入超过容量触发 resize)。
- 通过 Thread.sleep 测试 TTL 过期。
- 模拟重启验证 AOF 恢复。
用户学习点:
- 提供详细测试代码,读者可直接运行。
- 附带输出解释,展示哈希表和缓存行为。
要求:
- 测试覆盖所有功能和边界条件。
- 验证扩容后键值对完整性。
- 提供清晰断言和错误信息。
为何如此设计:
- 全面测试确保代码可靠性,增强用户信心。
- 模拟真实场景(扩容、过期、重启)帮助理解哈希表应用。
核心代码实现
以下是 CustomHashMap
和 HashMapCache
的完整实现,存放在 CustomHashMap.java 和 HashMapCache.java。
代码 1:CustomHashMap
public class CustomHashMap<K, V> {
private static class Entry<K, V> {
K key;
V value;
int hash;
Entry<K, V> next;
Entry(K key, V value, int hash) {
this.key = key;
this.value = value;
this.hash = hash;
}
}
private Entry<K, V>[] table;
private int size;
private int capacity;
private final float loadFactor;
private static final int INITIAL_CAPACITY = 16;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
@SuppressWarnings("unchecked")
public CustomHashMap() {
this.capacity = INITIAL_CAPACITY;
this.loadFactor = DEFAULT_LOAD_FACTOR;
this.table = new Entry[INITIAL_CAPACITY];
}
public V get(K key) {
if (key == null) throw new IllegalArgumentException("Key cannot be null");
int hash = hash(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))) {
return entry.value;
}
}
return null;
}
public void put(K key, V value) {
if (key == null || value == null) throw new IllegalArgumentException("Key or value cannot be null");
int hash = hash(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)) {
entry.value = value;
return;
}
}
if (size >= capacity * loadFactor) {
resize();
index = hash & (capacity - 1);
}
Entry<K, V> newEntry = new Entry<>(key, value, hash);
newEntry.next = table[index];
table[index] = newEntry;
size++;
}
public V remove(K key) {
if (key == null) throw new IllegalArgumentException("Key cannot be null");
int hash = hash(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;
}
public int size() {
return size;
}
private int hash(K key) {
int h = key.hashCode();
return (h ^ (h >>> 16)) & 0x7fffffff; // 优化分散性,确保正数
}
@SuppressWarnings("unchecked")
private void resize() {
try {
Entry<K, V>[] oldTable = table;
capacity *= 2;
table = new Entry[capacity];
size = 0;
for (Entry<K, V> entry : oldTable) {
while (entry != null) {
put(entry.key, entry.value);
entry = entry.next;
}
}
} catch (OutOfMemoryError e) {
throw new RuntimeException("Failed to resize hashmap due to insufficient memory", e);
}
}
}
代码 2:HashMapCache
package com.redis.cache;
import java.io.*;
import java.util.*;
public class HashMapCache extends MultiEvictionCache {
private static class Node {
String key, value;
long expiry;
int accessCount; // LFU
int referenceBit; // CLOCK
long insertionOrder; // FIFO
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 CustomHashMap<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 HashMapCache(int capacity, EvictionStrategy strategy) {
super(capacity, strategy);
this.capacity = capacity;
this.strategy = strategy;
this.cache = new CustomHashMap<>();
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);
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(clockHand.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);
}
}
}
测试代码
- 以下是 HashMapCacheTest.java,
验证
CustomHashMap 和 HashMapCache 功能。
package com;
import com.redis.cache.CustomHashMap;
import com.redis.cache.HashMapCache;
import com.redis.cache.MultiEvictionCache;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import java.io.File;
public class HashMapCacheTest {
private HashMapCache 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 testCustomHashMapBasic() {
CustomHashMap<String, String> map = new CustomHashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
assertEquals("value1", map.get("key1"));
assertEquals("value2", map.get("key2"));
assertEquals(2, map.size());
map.put("key1", "newValue");
assertEquals("newValue", map.get("key1"));
map.remove("key2");
assertNull(map.get("key2"));
assertEquals(1, map.size());
}
@Test
public void testCustomHashMapResize() {
CustomHashMap<String, String> map = new CustomHashMap<>();
for (int i = 0; i < 20; i++) {
map.put("key" + i, "value" + i);
}
assertEquals(20, map.size());
assertEquals("value10", map.get("key10"));
}
@Test
public void testHashMapCache() throws InterruptedException {
cache = new HashMapCache(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 HashMapCache(3, MultiEvictionCache.EvictionStrategy.LFU);
assertEquals("value1", cache.get("key1")); // AOF 恢复
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidInput() {
cache = new HashMapCache(3, MultiEvictionCache.EvictionStrategy.LFU);
cache.put(null, "value", 1000);
}
}
运行方式:
-
确保 Maven 项目包含 JUnit 依赖(同前五篇)。
-
将 SimpleCache.java, ExpiryCache.java, AofCache.java, LruCache.java, MultiEvictionCache.java, CustomHashMap.java, HashMapCache.java 和 HashMapCacheTest.java 放入项目。
-
运行测试:mvn test 或通过 IDE 执行。
测试输出(示例):
Cache full, removed key: key2
JDK HashMap 源码解析
以下是 JDK 8 HashMap 的关键优化,揭示与 Redis 哈希表的关联:
哈希函数 (java.util.HashMap.hash):
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 高位异或低位,减少冲突,Redis 类似优化哈希分布。
冲突处理 (putVal, getNode):
- 链表存储冲突节点,长度超过 8 时转为红黑树(TREEIFY_THRESHOLD)。
- Redis 哈希表使用链表,未采用红黑树,优先内存紧凑。
·动态扩容 (resize):·
- 容量翻倍,节点重新分配。
- Redis 引入渐进式 rehash(第7篇),分摊扩容开销。
性能优化:
- 2 的幂容量,使用 & (n - 1) 替代模运算。
- 延迟初始化,首次 put 时分配数组。
Redis 对比:
- Redis 哈希表更轻量,支持渐进式 rehash和高并发。
- JDK HashMap 通用性强,功能丰富(如红黑树)。
JDK HashMap 源码解析
面试要点
问:如何实现一个高效的 HashMap?
- 答:使用数组 + 链表,哈希函数映射键到桶,链表处理冲突。容量为 2 的幂,位运算优化哈希。动态扩容保持负载因子(如 0.75)。本实现参考 JDK 哈希函数和扩容机制。
问:HashMap 的冲突处理如何优化?
- 答:链表适合低冲突场景,高冲突可转为红黑树(如 JDK 8,链表长度 > 8)。本实现使用链表,简单高效,Redis 类似优先内存紧凑。
问:动态扩容的性能影响?
- 答:一次性 rehash 可能导致延迟,Redis 使用渐进式 rehash(第7篇)分摊开销。本实现一次性扩容,适合学习。
问:Redis 哈希表与 JDK HashMap 的区别?
- 答:Redis 哈希表轻量,支持渐进式 rehash,优化内存和并发。JDK HashMap 通用性强,引入红黑树。本实现是简化版,接近 JDK 设计。
下一步预告
恭喜你掌握 Redis 的哈希表核心!下一篇文章,我们将实现 渐进式 rehash
,让哈希表扩容如丝般顺滑!” 敬请期待!
- 完整代码:https://github.com/codeInbpm/redis-handwritten
互动:
运行代码了吗?有问题?欢迎在评论区留言!
觉得本篇炸裂吗?一键三连,关注系列,下一期更精彩!