永不丢数据的黑科技!手写 Redis AOF 持久化,面试官直接跪了!

引言

Redis 的持久化机制是其企业级应用的基石,AOF(Append-Only File)通过记录写操作日志,确保数据在重启后不丢失,广泛用于高可靠性场景。在面试中,“Redis 如何实现持久化?” 是热门考题,考察你对文件操作、数据一致性和系统设计的掌握。继前两篇实现固定大小缓存和键过期后,本篇带你手写 AOF 持久化,打造一个支持数据持久化的 AofCache!
目标:

  • 实现 AOF 持久化,记录所有写操作到日志文件。

  • 支持重启后数据恢复,保持键值对和 TTL。

  • 提供可运行代码、测试用例和图解,助你掌握 Redis 持久化原理。

适合人群:

  • 想深入理解 Redis 持久化机制的开发者。

  • 准备大厂面试、需手写持久化功能的程序员。

  • 对数据持久化感兴趣的初学者和进阶者。

前置工作:

  • 环境:Java 8+,Maven/Gradle。

  • 依赖:前两篇的 SimpleCacheExpiryCache 类(本篇继承)。

  • 代码仓库: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 获取完整实现和测试用例。
互动:
运行代码了吗?有问题?欢迎在评论区留言!

觉得本篇炸裂吗?点个赞,关注系列,下一期更精彩!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wáng bēn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值