You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何持久化Guava AtomicLongMap?解决重启后值丢失问题

嘿,我刚好遇到过类似的场景,给你几个实用的解决方案,你可以根据自己的需求来选:

解决方案一:定时持久化 + 启动加载

这是最简单的实现方式,核心思路是定期把内存中的AtomicLongMap数据写入持久化存储(比如本地文件),应用启动时再从存储中加载数据。同时配合JVM关闭钩子,在应用正常退出时触发一次保存,减少数据丢失的概率。

实现步骤:

  1. 编写序列化/反序列化方法,把AtomicLongMap转换成可存储的格式(比如JSON)
  2. 应用启动时,从文件加载数据初始化AtomicLongMap
  3. 用定时任务定期(比如每5分钟)将当前AtomicLongMap的数据写入文件
  4. 添加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)后,异步触发持久化操作。这样每次更新后都会触发保存,同时用异步线程避免阻塞业务逻辑。

实现思路:

  1. 自定义一个包装类,内部持有AtomicLongMap实例
  2. 重写所有修改数据的方法,在操作完成后提交一个异步任务去持久化
  3. 同样保留启动加载和关闭钩子逻辑

核心代码示例:

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):

  1. 先引入LevelDB的Java依赖(比如org.iq80.leveldb:leveldb:0.12)
  2. 实现基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 07:07:08