引言
Redis 作为高性能的内存键值数据库,席卷了各大互联网公司,成为缓存和数据存储的首选神器!想知道 Redis 的底层奥秘?想在面试中吊打对手?从今天起,我们将用 Java 从零开始手写 Redis,8 篇文章带你从入门到精通!本篇是第一步:实现一个固定大小的键值缓存,支持 get
和 put
操作,简单却硬核,直接让你在面试中脱颖而出!
目标:
-
理解 Redis 作为键值存储的核心原理。
-
用 Java 实现一个固定大小的缓存,支持基本的键值操作。
-
为后续功能(如过期、淘汰策略、持久化)打下基础。
适合人群:
-
想深入理解 Redis 底层实现的开发者。
-
准备大厂面试、需要手写数据结构的程序员。
-
对缓存系统感兴趣的初学者和进阶者。
准备工作:
- 环境:Java 8+,Maven/Gradle(可选)。
- 建议:跟着文章敲代码,运行测试用例,感受手写 Redis 的快感!
为什么需要手写 Redis 缓存?
Redis 是一个基于内存的键值数据库,以高性能和灵活性著称。它的核心功能包括:
- 超高性能:内存操作,读写速度极快,适合高并发场景。
- 固定大小:内存有限,缓存需限制容量,超出时通过淘汰策略移除数据。
- 简单接口:提供
get
、put
等操作,易于扩展到复杂功能(如过期、持久化)。
在面试中,面试官常问:- “如何设计一个高效的缓存系统?”
- “Redis 的键值存储底层如何实现?”
- “HashMap 作为缓存有什么局限性?”
本篇通过手写一个固定大小的缓存,带你搞懂 Redis 的基础原理,同时为后续功能(如键过期、LRU 淘汰、AOF 持久化)打下坚实基础。
实现步骤
我们将实现一个简单的键值缓存类 SimpleCache
,具备以下功能:
存储键值对:通过 put(key, value)
存储字符串键值对。
获取值:通过get(key)
获取键对应的值,若键不存在返回 null。
固定大小:限制缓存容量,超出时移除最早的数据(当前用简单策略,后续升级为 LRU)。
步骤 1:定义缓存类结构
-
使用 Java 的
HashMap
作为底层存储,实现 O(1) 的键值存取。 -
设置
capacity
参数限制缓存大小。 -
当缓存满时,移除最早的键(当前用随机移除,简单易懂)。
步骤 2:实现 put
和 get
方法
-
put:存入键值对,若缓存满,移除一个键后再插入。
-
get:根据键获取值,处理键不存在的情况。
步骤 3:添加健壮性检查
-
验证
key
和value
不为null
。 -
确保
capacity
为正数,防止无效输入。
步骤 4:编写测试用例
-
使用 JUnit 测试
put
、get
和容量限制功能。 -
验证边界条件(如空输入、容量超限)。
图片:SimpleCache 类结构
核心代码实现
以下是SimpleCache
的完整实现,代码简洁、可运行,存放在 SimpleCache.java
中。
import java.util.HashMap;
import java.util.Map;
public class SimpleCache {
private Map<String, String> cache; // 存储键值对
private int capacity; // 缓存最大容量
// 构造函数,初始化缓存
public SimpleCache(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("Capacity must be positive");
}
this.cache = new HashMap<>();
this.capacity = capacity;
}
// 获取键对应的值
public String get(String key) {
if (key == null) {
throw new IllegalArgumentException("Key cannot be null");
}
return cache.get(key);
}
// 存入键值对,超出容量时移除最早的键
public void put(String key, String value) {
if (key == null || value == null) {
throw new IllegalArgumentException("Key or value cannot be null");
}
if (cache.size() >= capacity) {
// 移除最早的键(简单策略:随机移除一个键)
String firstKey = cache.keySet().iterator().next();
cache.remove(firstKey);
System.out.println("Cache full, removed key: " + firstKey);
}
cache.put(key, value);
}
// 获取当前缓存大小
public int size() {
return cache.size();
}
}
测试代码
为验证功能,我们提供 JUnit 测试用例,存放在 SimpleCacheTest.java
中,覆盖基本功能和边界条件。
import org.junit.Test;
import static org.junit.Assert.*;
public class SimpleCacheTest {
@Test
public void testSimpleCache() {
SimpleCache cache = new SimpleCache(3); // 容量为 3
// 测试 put 和 get
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
assertEquals("value1", cache.get("key1"));
assertEquals("value2", cache.get("key2"));
assertEquals("value3", cache.get("key3"));
assertEquals(3, cache.size());
// 测试容量限制
cache.put("key4", "value4"); // 超出容量,移除 key1
assertNull(cache.get("key1")); // key1 已被移除
assertEquals("value4", cache.get("key4"));
assertEquals(3, cache.size());
// 测试空键值
try {
cache.put(null, "value");
fail("Should throw IllegalArgumentException");
} catch (IllegalArgumentException e) {
// 预期异常
}
try {
cache.get(null);
fail("Should throw IllegalArgumentException");
} catch (IllegalArgumentException e) {
// 预期异常
}
// 测试无效容量
try {
new SimpleCache(0);
fail("Should throw IllegalArgumentException");
} catch (IllegalArgumentException e) {
// 预期异常
}
}
}
运行方式:
- 创建
Maven
项目,添加JUnit
依赖:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
-
将
SimpleCache.java
和SimpleCacheTest.java
放入项目(src/main/java 和 src/test/java)。 -
运行测试:mvn test 或通过 IDE(如 IntelliJ)执行。
测试输出:
Cache full, removed key: key1
面试要点
以下是基于本篇内容的常见面试问题及答案,帮助你轻松应对大厂考题:
-
问:为什么要限制缓存大小?
- 答:内存资源有限,缓存需控制大小以避免内存溢出。通过淘汰策略(如 LRU、LFU)移除不常用数据,保证性能和资源利用率。
-
问:HashMap 作为缓存的局限性是什么?
- 答:HashMap 不支持容量限制和淘汰策略,且非线程安全。在高并发场景下需使用 ConcurrentHashMap 或加锁机制。
-
问:如何改进当前移除策略?
- 答:随机移除效率低,可用 LRU(最近最少使用)算法,通过双向链表 + HashMap 实现 O(1) 的存取和淘汰(将在第4篇详解)。
-
问:缓存系统设计需要考虑哪些因素?
- 答:包括容量限制、淘汰策略、并发安全性、数据持久化、键过期机制等。本篇实现了容量限制,后续将逐步完善其他功能。
下一步预告
恭喜你完成 Redis 手写第一步!下一篇文章,我们将实现 键过期机制,让键值对自动消失,解锁 Redis 的核心功能!标题:“Redis 过期机制大揭秘!一招让键值自动消失,面试稳了!” 敬请期待!
你运行代码了吗?遇到问题?欢迎在评论区留言!
觉得这篇教程炸裂吗?点个赞,关注系列,下一期更精彩!