在 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)进一步减小磁盘占用
