在 Java 中,使用 Map 存储内存数据并定时持久化到磁盘,是很多系统常见的需求,如缓存、配置中心、本地任务队列等。为了保证高性能和低延迟,选择一个高效的序列化框架至关重要。本文将介绍如何使用 Kryo 高效、线程安全地实现 Map 的持久化与恢复。


🔧 业务背景

我们需要定期将内存中的 Map<K, V> 写入磁盘文件,并支持随时从文件中读取恢复。要求包括:

  • 高性能序列化,支持复杂对象
  • 线程安全处理,避免并发问题
  • 序列化过程不中断业务写操作
  • 支持任意 Java 对象作为值

🚀 为什么选择 Kryo?

Kryo 是一个高性能、紧凑的 Java 序列化框架,优势明显:

特性Kryo 描述
性能比 Java 默认序列化快 10 倍以上
数据体积序列化结果更小
支持复杂对象结构支持对象图、循环引用等
可定制支持注册类型、自定义 Serializer

🧩 Maven 依赖

pom.xml 中添加 Kryo 及其依赖:

<dependencies>
  <dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>5.5.0</version>
  </dependency>
  <dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>minlog</artifactId>
    <version>1.3.1</version>
  </dependency>
  <dependency>
    <groupId>org.objenesis</groupId>
    <artifactId>objenesis</artifactId>
    <version>3.3</version>
  </dependency>
</dependencies>

⚠️ Kryo 线程安全问题

Kryo 实例不是线程安全的。如果多个线程同时调用同一个 Kryo 实例,会导致各种异常。

✅ 正确方式:每线程一个 Kryo 实例

使用 ThreadLocal<Kryo> 是最简单有效的做法:

private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
    Kryo kryo = new Kryo();
    kryo.setRegistrationRequired(false); // 支持任意对象
    kryo.setReferences(true);            // 支持循环引用
    return kryo;
});

public static Kryo getKryo() {
    return kryoThreadLocal.get();
}

🧱 并发修改 Map 的问题

如果序列化过程中 Map 正在被其他线程修改,会出现:

  • 抛出 ConcurrentModificationException
  • 序列化结果不一致
  • 部分数据丢失

✅ 推荐做法:加锁或快照

方式一:使用 ReadWriteLock(强一致性)

private final Map<String, Object> map = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();

public void serialize() {
    lock.readLock().lock();
    try (Output output = new Output(new FileOutputStream("data.bin"))) {
        getKryo().writeObject(output, map);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        lock.readLock().unlock();
    }
}

方式二:快照复制(弱一致,性能优)

public void serializeWithSnapshot() {
    Map<String, Object> snapshot;
    synchronized (map) {
        snapshot = new HashMap<>(map); // 拷贝快照
    }
    try (Output output = new Output(new FileOutputStream("data.bin"))) {
        getKryo().writeObject(output, snapshot);
    }
}

适合读多写少场景,避免长时间读锁。


💾 持久化示例代码

public void saveToDisk(String path, Map<String, Object> map) {
    try (Output output = new Output(new FileOutputStream(path))) {
        getKryo().writeObject(output, map);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

public Map<String, Object> loadFromDisk(String path) {
    try (Input input = new Input(new FileInputStream(path))) {
        return getKryo().readObject(input, HashMap.class);
    } catch (IOException e) {
        e.printStackTrace();
        return Collections.emptyMap();
    }
}

✅ 总结

项目建议做法
Kryo 实例线程安全使用 ThreadLocal<Kryo>
Map 并发访问加锁或快照拷贝
持久化频率可配合定时任务实现
Map 类型ConcurrentHashMap + 快照 最优

🧰 可拓展建议

  • 可以封装成 PersistentMap<K, V>,支持定时保存与自动恢复
  • 可以集成进 Spring Boot,通过配置开启持久化
  • 序列化格式可压缩(GZIP)进一步减小磁盘占用

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注