從零開始手寫 redis(三)內存數據重啓後如何不丟失?

前言

我們在 從零手寫 cache 框架(一)實現固定大小的緩存 中已經初步實現了我們的 cache。

我們在 從零手寫 cache 框架(一)實現過期特性 中實現了 key 的過期特性。

本節,讓我們來一起學習一下如何實現類似 redis 中的 rdb 的持久化模式。

持久化的目的

我們存儲的信息都是直接放在內存中的,如果斷電或者應用重啓,那麼內容就全部丟失了。

有時候我們希望這些信息重啓之後還在,就像 redis 重啓一樣。

load 加載

說明

在實現持久化之前,我們來看一下一個簡單的需求:

如何在緩存啓動的時候,指定初始化加載的信息。

實現思路

這個也不難,我們在 cache 初始化的時候,直接設置對應的信息即可。

api

爲了便於後期拓展,定義 ICacheLoad 接口。

public interface ICacheLoad<K, V> {

    /**
     * 加載緩存信息
     * @param cache 緩存
     * @since 0.0.7
     */
    void load(final ICache<K,V> cache);

}

自定義初始化策略

我們在初始化的時候,放入 2 個固定的信息。

public class MyCacheLoad implements ICacheLoad<String,String> {

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

}

測試

只需要在緩存初始化的時候,指定對應的加載實現類即可。

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

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

持久化

說明

上面先介紹初始化加載,其實已經完成了 cache 持久化的一半。

我們要做的另一件事,就是將 cache 的內容持久化到文件或者數據庫,便於初始化的時候加載。

接口定義

爲了便於靈活替換,我們定義一個持久化的接口。

public interface ICachePersist<K, V> {

    /**
     * 持久化緩存信息
     * @param cache 緩存
     * @since 0.0.7
     */
    void persist(final ICache<K, V> cache);

}

簡單實現

我們實現一個最簡單的基於 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);
        }
    }

}

定時執行

上面定義好了一種持久化的策略,但是沒有提供對應的觸發方式。

我們就採用對用戶透明的設計方式:定時執行。

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。

測試

我們只需要在創建 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"}

對應的緩存加載

我們只需要實現以下對應的加載即可,解析文件,然後初始化 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...
    }
}

然後在初始化時使用即可。

小結

到這裏,我們一個類似於 redis rdb 的持久化就簡單模擬完成了。

但是對於 rdb 這裏還有需要可優化點,比如 rdb 文件的壓縮、格式的定義、CRC 校驗等等。

redis 考慮到性能問題,還有 AOF 的持久化模式,二者相輔相成,才能達到企業級別的緩存效果。

我們後續將陸續引入這些特性。

對你有幫助的話,歡迎點贊評論收藏關注一波~

你的鼓勵,是我最大的動力~

深入學習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章