如何持久化Guava AtomicLongMap?解决重启后值丢失问题
嘿,我刚好遇到过类似的场景,给你几个实用的解决方案,你可以根据自己的需求来选:
解决方案一:定时持久化 + 启动加载
这是最简单的实现方式,核心思路是定期把内存中的AtomicLongMap数据写入持久化存储(比如本地文件),应用启动时再从存储中加载数据。同时配合JVM关闭钩子,在应用正常退出时触发一次保存,减少数据丢失的概率。
实现步骤:
- 编写序列化/反序列化方法,把
AtomicLongMap转换成可存储的格式(比如JSON) - 应用启动时,从文件加载数据初始化
AtomicLongMap - 用定时任务定期(比如每5分钟)将当前
AtomicLongMap的数据写入文件 - 添加JVM关闭钩子,在应用正常退出时执行一次保存
代码示例:
import com.google.common.util.concurrent.AtomicLongMap; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class PersistentSequenceManager { private static final String DATA_FILE = "sequence_data.json"; private final AtomicLongMap<String> sequenceMap; private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); public PersistentSequenceManager() throws IOException { // 启动时加载数据 this.sequenceMap = loadAtomicLongMap(); // 定时保存(每5分钟执行一次) scheduler.scheduleAtFixedRate(this::saveAtomicLongMap, 5, 5, TimeUnit.MINUTES); // 添加关闭钩子,正常退出时保存 Runtime.getRuntime().addShutdownHook(new Thread(() -> { saveAtomicLongMap(); scheduler.shutdown(); })); } // 从文件加载AtomicLongMap private AtomicLongMap<String> loadAtomicLongMap() throws IOException { AtomicLongMap<String> map = AtomicLongMap.create(); if (Files.exists(Paths.get(DATA_FILE))) { String jsonContent = Files.readString(Paths.get(DATA_FILE), StandardCharsets.UTF_8); List<Map.Entry<String, Long>> entries = new ObjectMapper() .readValue(jsonContent, new TypeReference<List<Map.Entry<String, Long>>>() {}); for (Map.Entry<String, Long> entry : entries) { map.put(entry.getKey(), entry.getValue()); } } return map; } // 将AtomicLongMap保存到文件 private void saveAtomicLongMap() { try { List<Map.Entry<String, Long>> entries = new ArrayList<>(sequenceMap.asMap().entrySet()); String jsonContent = new ObjectMapper().writeValueAsString(entries); Files.write(Paths.get(DATA_FILE), jsonContent.getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { // 建议替换为日志框架记录异常 e.printStackTrace(); } } // 对外提供的序列获取方法 public long getNextSequence(String key) { return sequenceMap.incrementAndGet(key); } }
优缺点:
- ✅ 实现简单,不需要额外依赖
- ✅ 性能影响小,只有定时IO操作
- ❌ 若应用意外崩溃,会丢失最后一次保存后的数据
解决方案二:操作后异步持久化
如果你希望数据尽可能少丢失,可以封装AtomicLongMap,每次修改操作(比如incrementAndGet、put)后,异步触发持久化操作。这样每次更新后都会触发保存,同时用异步线程避免阻塞业务逻辑。
实现思路:
- 自定义一个包装类,内部持有
AtomicLongMap实例 - 重写所有修改数据的方法,在操作完成后提交一个异步任务去持久化
- 同样保留启动加载和关闭钩子逻辑
核心代码示例:
public class AsyncPersistentAtomicLongMap { private final AtomicLongMap<String> delegate; private final String dataFile; private final ExecutorService persistExecutor = Executors.newSingleThreadExecutor(); public AsyncPersistentAtomicLongMap(String dataFile) throws IOException { this.dataFile = dataFile; this.delegate = loadAtomicLongMap(dataFile); // 关闭钩子 Runtime.getRuntime().addShutdownHook(new Thread(() -> { saveAtomicLongMap(delegate, dataFile); persistExecutor.shutdown(); })); } public long incrementAndGet(String key) { long value = delegate.incrementAndGet(key); triggerAsyncPersist(); return value; } public void put(String key, long value) { delegate.put(key, value); triggerAsyncPersist(); } // 其他方法如get, decrementAndGet等按需封装 private void triggerAsyncPersist() { persistExecutor.submit(() -> saveAtomicLongMap(delegate, dataFile)); } // 复用方案一中的load和save方法 private AtomicLongMap<String> loadAtomicLongMap(String filePath) throws IOException { /* ... */ } private void saveAtomicLongMap(AtomicLongMap<String> map, String filePath) { /* ... */ } }
优缺点:
- ✅ 数据丢失风险极低(仅会丢失崩溃前正在异步执行的那部分更新)
- ✅ 业务逻辑无阻塞(异步持久化)
- ❌ 频繁操作会触发较多IO,可能带来一定性能开销(可通过批量持久化优化,比如累计N次操作后再保存)
解决方案三:替换为本地持久化KV存储
如果对数据持久化的可靠性要求极高(不允许任何数据丢失),可以直接放弃纯内存的AtomicLongMap,改用本地持久化的KV存储(比如LevelDB、BerkeleyDB)。这类存储本身就支持原子操作和持久化,性能也不错。
示例(用LevelDB):
- 先引入LevelDB的Java依赖(比如
org.iq80.leveldb:leveldb:0.12) - 实现基于LevelDB的序列生成逻辑:
import org.iq80.leveldb.DB; import org.iq80.leveldb.DBIterator; import org.iq80.leveldb.Options; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import static org.iq80.leveldb.impl.Iq80DBFactory.factory; public class LevelDBSequenceManager { private final DB db; public LevelDBSequenceManager() throws IOException { Options options = new Options(); options.createIfMissing(true); // 数据存储目录 this.db = factory.open(new File("sequence_leveldb"), options); // 关闭钩子 Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { db.close(); } catch (IOException e) { e.printStackTrace(); } })); } public long getNextSequence(String key) { byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); // 原子性更新:读取当前值 → 计算下一个值 → 写入 while (true) { byte[] currentBytes = db.get(keyBytes); long current = currentBytes == null ? 0 : Long.parseLong(new String(currentBytes)); long next = current + 1; byte[] nextBytes = String.valueOf(next).getBytes(StandardCharsets.UTF_8); // 用compareAndSet保证原子性(LevelDB的put不支持CAS,所以自己实现循环重试) if (db.compareAndSet(keyBytes, currentBytes, nextBytes)) { return next; } } } // 可选:初始化某个键的起始值 public void setInitialValue(String key, long value) { db.put(key.getBytes(StandardCharsets.UTF_8), String.valueOf(value).getBytes(StandardCharsets.UTF_8)); } }
优缺点:
- ✅ 数据持久化可靠,崩溃后不会丢失任何数据
- ✅ 支持原子操作,多线程安全
- ❌ 需要引入额外的KV存储依赖,增加了项目复杂度
- ❌ 单进程使用没问题,多进程需要额外处理锁竞争
内容的提问来源于stack exchange,提问作者grassbl8d
相关产品推荐
相关产品推荐

