内存数据如何重启不丢失? - zxlrise/cache GitHub Wiki

1、前言

1.1、持久化的目的

我们存储的信息都是直接放在内存中的,如果断电或者应用重启,那么内容就全部丢失了。有时候我们希望这些信息重启之后还在,就像 redis 重启一样。

2、load 加载

2.1、说明

在实现持久化之前,我们来看一下一个简单的需求: 如何在缓存启动的时候,指定初始化加载的信息。

2.2、实现思路

这个也不难,我们在 cache 初始化的时候,直接设置对应的信息即可。

2.3、api

为了便于后期拓展,定义 ICacheLoad 接口。

public interface ICacheLoad<K, V> {
    /**
     * 加载缓存信息
     * @param cache 缓存
     * @since 0.0.7
     */
    void load(final ICache<K,V> cache);
}

2.4、自定义初始化策略

我们在初始化的时候,放入 2 个固定的信息。
public class MyCacheLoad implements ICacheLoad<String,String> {

@Override
public void load(ICache<String, String> cache) {
    cache.put("1", "1");
    cache.put("2", "2");
}

}

2.5、测试

只需要在缓存初始化的时候,指定对应的加载实现类即可。

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .load(new MyCacheLoad())
        .build();

Assert.assertEquals(2, cache.size());

3、持久化

3.1、说明

上面先介绍初始化加载,其实已经完成了 cache 持久化的一半。 我们要做的另一件事,就是将 cache 的内容持久化到文件或者数据库,便于初始化的时候加载。

3.2、接口定义

为了便于灵活替换,我们定义一个持久化的接口。

public interface ICachePersist<K, V> {
    /**
     * 持久化缓存信息
     * @param cache 缓存
     * @since 0.0.7
     */
    void persist(final ICache<K, V> cache);
}

3.3、简单实现

我们实现一个最简单的基于 json 的持久化,当然后期可以添加类似于 AOF 的持久化模式。

public class CachePersistDbJson<K,V> implements ICachePersist<K,V> {
    /**
     * 数据库路径
     * @since 0.0.8
     */
    private final String dbPath;

    public CachePersistDbJson(String dbPath) {
        this.dbPath = dbPath;
    }

    /**
     * 持久化
     * key长度 key+value
     * 第一个空格,获取 key 的长度,然后截取
     * @param cache 缓存
     */
    @Override
    public void persist(ICache<K, V> cache) {
        Set<Map.Entry<K,V>> entrySet = cache.entrySet();

        // 创建文件
        FileUtil.createFile(dbPath);
        // 清空文件
        FileUtil.truncate(dbPath);

        for(Map.Entry<K,V> entry : entrySet) {
            K key = entry.getKey();
            Long expireTime = cache.expire().expireTime(key);
            PersistEntry<K,V> persistEntry = new PersistEntry<>();
            persistEntry.setKey(key);
            persistEntry.setValue(entry.getValue());
            persistEntry.setExpire(expireTime);

            String line = JSON.toJSONString(persistEntry);
            FileUtil.write(dbPath, line, StandardOpenOption.APPEND);
        }
    }
}

3.4、定时执行

上面定义好了一种持久化的策略,但是没有提供对应的触发方式。 我们就采用对用户透明的设计方式:定时执行。

public class InnerCachePersist<K,V> {

    private static final Log log = LogFactory.getLog(InnerCachePersist.class);

    /**
     * 缓存信息
     * @since 0.0.8
     */
    private final ICache<K,V> cache;

    /**
     * 缓存持久化策略
     * @since 0.0.8
     */
    private final ICachePersist<K,V> persist;

    /**
     * 线程执行类
     * @since 0.0.3
     */
    private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();

    public InnerCachePersist(ICache<K, V> cache, ICachePersist<K, V> persist) {
        this.cache = cache;
        this.persist = persist;

        // 初始化
        this.init();
    }

    /**
     * 初始化
     * @since 0.0.8
     */
    private void init() {
        EXECUTOR_SERVICE.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    log.info("开始持久化缓存信息");
                    persist.persist(cache);
                    log.info("完成持久化缓存信息");
                } catch (Exception exception) {
                    log.error("文件持久化异常", exception);
                }
            }
        }, 0, 10, TimeUnit.MINUTES);
    }

}

定时执行的时间间隔为 10min。

3.5、测试

我们只需要在创建 cache 时,指定我们的持久化策略即可。

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .load(new MyCacheLoad())
        .persist(CachePersists.<String, String>dbJson("1.rdb"))
        .build();
Assert.assertEquals(2, cache.size());
TimeUnit.SECONDS.sleep(5);

为了确保文件持久化完成,我们沉睡了一会儿。
文件效果

  • 1.rdb 生成的文件内容如下:
{"key":"2","value":"2"}
{"key":"1","value":"1"}

3.6、对应的缓存加载

我们只需要实现以下对应的加载即可,解析文件,然后初始化 cache。

/**
 * 加载策略-文件路径
 * @author binbin.hou
 * @since 0.0.8
 */
public class CacheLoadDbJson<K,V> implements ICacheLoad<K,V> {

    private static final Log log = LogFactory.getLog(CacheLoadDbJson.class);

    /**
     * 文件路径
     * @since 0.0.8
     */
    private final String dbPath;

    public CacheLoadDbJson(String dbPath) {
        this.dbPath = dbPath;
    }

    @Override
    public void load(ICache<K, V> cache) {
        List<String> lines = FileUtil.readAllLines(dbPath);
        log.info("[load] 开始处理 path: {}", dbPath);
        if(CollectionUtil.isEmpty(lines)) {
            log.info("[load] path: {} 文件内容为空,直接返回", dbPath);
            return;
        }

        for(String line : lines) {
            if(StringUtil.isEmpty(line)) {
                continue;
            }

            // 执行
            // 简单的类型还行,复杂的这种反序列化会失败
            PersistEntry<K,V> entry = JSON.parseObject(line, PersistEntry.class);

            K key = entry.getKey();
            V value = entry.getValue();
            Long expire = entry.getExpire();

            cache.put(key, value);
            if(ObjectUtil.isNotNull(expire)) {
                cache.expireAt(key, expire);
            }
        }
        //nothing...
    }
}

然后在初始化时使用即可。

⚠️ **GitHub.com Fallback** ⚠️