引言
Redis 的持久化机制是其企业级应用的基石,AOF(Append-Only File)
通过记录写操作日志,确保数据在重启后不丢失,广泛用于高可靠性场景。在面试中,“Redis 如何实现持久化?” 是热门考题,考察你对文件操作、数据一致性和系统设计的掌握。继前两篇实现固定大小缓存和键过期后,本篇带你手写 AOF 持久化
,打造一个支持数据持久化的 AofCache!
目标:
-
实现 AOF 持久化,记录所有写操作到日志文件。
-
支持重启后数据恢复,保持键值对和 TTL。
-
提供可运行代码、测试用例和图解,助你掌握 Redis 持久化原理。
适合人群:
-
想深入理解 Redis 持久化机制的开发者。
-
准备大厂面试、需手写持久化功能的程序员。
-
对数据持久化感兴趣的初学者和进阶者。
前置工作:
-
环境:Java 8+,Maven/Gradle。
-
依赖:前两篇的
SimpleCache
和ExpiryCache
类(本篇继承)。 -
代码仓库:redis-handwritten
建议:运行前两篇代码后,跟着本篇敲代码,体验数据永不丢失的魔法!
为什么需要 AOF 持久化?
Redis 支持两种持久化方式:
RDB:
定期生成数据快照,体积小但可能丢数据。
AOF:
记录每条写操作日志,数据完整性更高,适合高可靠性场景。
AOF 的核心优势:
-
数据安全:每次写操作都追加到日志,减少数据丢失风险。
-
可读性:AOF 文件是文本格式,便于调试和恢复。
-
灵活性:支持定时重写(rewrite),优化文件大小。
面试常见问题:
-
“Redis 的 AOF 和 RDB 有什么区别?”
-
“如何实现一个持久化的键值存储?”
-
“AOF 文件过大如何优化?”
本篇将实现 AOF 持久化,通过追加日志记录 put 操作,并支持重启后数据恢复,为后续功能(如缓存淘汰策略)铺路。
实现步骤
我们将基于第二篇的 ExpiryCache(支持键过期),实现 AofCache 类,新增以下功能:
-
AOF 日志记录
:每次 put 操作追加到 appendonly.aof 文件。 -
数据恢复
:启动时从 AOF 文件加载键值对和 TTL。 -
日志格式
:设计简单的文本格式,记录操作类型、键、值和 TTL。 -
健壮性
:处理文件操作异常,确保数据一致性。
步骤 1:扩展缓存类结构
-
继承
ExpiryCache
,添加文件操作逻辑。 -
定义 AOF 文件路径(
appendonly.aof)
)。
步骤 2:实现日志追加
-
重写
put
方法,每次写入时追加日志(格式:PUT key value ttl
)。 -
使用
FileWriter
实现高效追加。
步骤 3:实现数据恢复
-
构造函数中读取
AOF
文件,解析日志并恢复键值对和 TTL。 -
使用
BufferedReader
逐行解析。
步骤 4:添加测试用例
-
使用 JUnit 测试日志记录、数据恢复和异常处理。
-
模拟重启场景,验证数据一致性。
核心代码实现
以下是 AofCache
的完整实现,存放在 AofCache.java
中,继承自 ExpiryCache
。
前置代码:SimpleCache 和 ExpiryCache(复用前两篇)
为节省篇幅,假设 SimpleCache 和 ExpiryCache 已存在(见第一、二篇)。以下是简要结构:
- SimpleCache:固定大小缓存,支持 get、put。
- ExpiryCache:支持 TTL、惰性删除、定期删除、随机淘汰。
主代码:AofCache
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class AofCache extends ExpiryCache {
private File aofFile = new File("appendonly.aof");
public AofCache(int capacity) {
super(capacity);
loadAof(); // 启动时加载 AOF 文件
}
// 重写 put 方法,追加日志
@Override
public void put(String key, String value, long ttlMillis) {
super.put(key, value, ttlMillis);
appendToAof("PUT", key, value, ttlMillis);
}
// 追加日志到 AOF 文件
private void appendToAof(String op, String key, String value, long ttl) {
try (FileWriter writer = new FileWriter(aofFile, true);
BufferedWriter bufferedWriter = new BufferedWriter(writer)) {
bufferedWriter.write(op + " " + key + " " + value + " " + ttl + "\n");
bufferedWriter.flush();
} catch (IOException e) {
System.err.println("Failed to append to AOF: " + e.getMessage());
}
}
// 从 AOF 文件恢复数据
private void loadAof() {
if (!aofFile.exists()) {
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(aofFile))) {
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(" ", 4);
if (parts.length == 4 && parts[0].equals("PUT")) {
String key = parts[1];
String value = parts[2];
long ttl = Long.parseLong(parts[3]);
// 只恢复未过期的键
if (System.currentTimeMillis() < ttl) {
super.put(key, value, ttl - System.currentTimeMillis());
}
}
}
} catch (IOException | NumberFormatException e) {
System.err.println("Failed to load AOF: " + e.getMessage());
}
}
// 获取 AOF 文件大小(用于测试)
public long getAofFileSize() {
return aofFile.exists() ? aofFile.length() : 0;
}
}
测试代码
以下是 AofCacheTest.java
,验证 AOF 日志记录、数据恢复和异常处理。
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import java.io.File;
public class AofCacheTest {
private AofCache cache;
private File aofFile = new File("appendonly.aof");
@Before
public void setUp() {
// 清理旧的 AOF 文件
if (aofFile.exists()) {
aofFile.delete();
}
cache = new AofCache(3);
}
@After
public void tearDown() {
cache.shutdown(); // 关闭定时任务
if (aofFile.exists()) {
aofFile.delete();
}
}
@Test
public void testAofPersistence() throws InterruptedException {
// 测试日志记录
cache.put("key1", "value1", 5000);
cache.put("key2", "value2", 10000);
assertTrue(cache.getAofFileSize() > 0); // 确认 AOF 文件已写入
// 模拟重启
cache = new AofCache(3);
assertEquals("value1", cache.get("key1"));
assertEquals("value2", cache.get("key2"));
// 测试过期键不恢复
cache.put("key3", "value3", 1000);
Thread.sleep(1500); // 等待 key3 过期
cache = new AofCache(3); // 重新加载
assertNull(cache.get("key3")); // key3 不应恢复
assertEquals("value2", cache.get("key2")); // key2 仍有效
// 测试容量限制
cache.put("key4", "value4", 5000);
assertTrue(cache.size() <= 3);
}
}
运行方式:
-
确保 Maven 项目包含 JUnit 依赖(同前两篇)。
-
将 SimpleCache.java、ExpiryCache.java、AofCache.java 和 AofCacheTest.java 放入项目。
-
运行测试:mvn test 或通过 IDE 执行。
测试输出(示例):
Cache full, removed key: key1
AOF 日志追加与恢复流程
面试要点
以下是基于本篇的常见面试问题及答案:
问:Redis 的 AOF 和 RDB 有什么区别?
- 答:AOF 记录写操作日志,数据完整性高但文件较大;RDB 是定时快照,体积小但可能丢失数据。AOF 适合高可靠性场景,RDB 适合快速恢复。本实现采用 AOF,追加日志确保数据安全。
问:AOF 文件过大如何优化?
- 答:通过重写(rewrite)合并冗余操作,如多次 put 同一键只保留最新记录。另可定期清理过期键,减少无效日志。
问:AOF 持久化的性能瓶颈?
- 答:频繁写磁盘可能影响性能,可通过缓冲写(BufferedWriter)、异步写或调整 fsync 策略(如 everysec)优化。本实现使用 flush 确保数据写入。
问:如何保证 AOF 数据一致性?
- 答:通过追加写(避免覆盖)、异常处理和校验日志格式确保一致性。本实现捕获 IO 异常并只恢复有效键。
下一步预告
恭喜你掌握 Redis 的 AOF 持久化
!下一篇文章,我们将实现 LRU 缓存淘汰策略,让缓存更智能,解锁 Redis 的高效内存管理!标题:“缓存淘汰神技!手写 LRU 算法,秒杀 90% 程序员!” 敬请期待!
完整代码:请访问 GitHub: redis-handwritten 获取完整实现和测试用例。
互动:
运行代码了吗?有问题?欢迎在评论区留言!
觉得本篇炸裂吗?点个赞,关注系列,下一期更精彩!