How to write redis memory data from scratch without losing it after restart?

preface

We are From zero handwriting cache framework (1) to achieve a fixed size cache Our cache has been preliminarily implemented in.

We are Zero handwriting cache framework (1) implementation of expiration feature The expiration feature of key is implemented in.

In this section, let's learn how to implement the persistence mode similar to rdb in redis.

Purpose of persistence

The information we store is directly stored in memory. If we power off or restart the application, all the content will be lost.

Sometimes we want this information to remain after restart, just like redis restart.

Load load

explain

Before we implement persistence, let's look at a requirement:

How to specify the information of initialization loading when the cache is started.

Realization idea

This is not difficult. When initializing the cache, we can directly set the corresponding information.

api

To facilitate later expansion, the ICacheLoad interface is defined.

public interface ICacheLoad<K, V> {

    /**
     * Load cache information
     * @param cache cache
     * @since 0.0.7
     */
    void load(final ICache<K,V> cache);

}

Custom initialization policy

When initializing, we put in two fixed messages.

public class MyCacheLoad implements ICacheLoad<String,String> {

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

}

test

You only need to specify the corresponding loading implementation class during cache initialization.

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

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

Persistence

explain

Initialization loading is introduced above. In fact, half of cache persistence has been completed.

Another thing we need to do is to persist the contents of the cache to a file or database for easy loading during initialization.

Interface definition

In order to facilitate flexible replacement, we define a persistent interface.

public interface ICachePersist<K, V> {

    /**
     * Persistent cache information
     * @param cache cache
     * @since 0.0.7
     */
    void persist(final ICache<K, V> cache);

}

Simple implementation

We implement the simplest json based persistence. Of course, a persistence pattern similar to AOF can be added later.

public class CachePersistDbJson<K,V> implements ICachePersist<K,V> {

    /**
     * Database path
     * @since 0.0.8
     */
    private final String dbPath;

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

    /**
     * Persistence
     * key Length key+value
     * The first space, get the length of the key, and then intercept it
     * @param cache cache
     */
    @Override
    public void persist(ICache<K, V> cache) {
        Set<Map.Entry<K,V>> entrySet = cache.entrySet();

        // create a file
        FileUtil.createFile(dbPath);
        // Empty file
        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);
        }
    }

}

Timed execution

The above defines a persistent strategy, but does not provide a corresponding trigger method.

We adopt a design method that is transparent to users: regular execution.

public class InnerCachePersist<K,V> {

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

    /**
     * Cache information
     * @since 0.0.8
     */
    private final ICache<K,V> cache;

    /**
     * Cache persistence strategy
     * @since 0.0.8
     */
    private final ICachePersist<K,V> persist;

    /**
     * Thread execution class
     * @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;

        // initialization
        this.init();
    }

    /**
     * initialization
     * @since 0.0.8
     */
    private void init() {
        EXECUTOR_SERVICE.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    log.info("Start persisting cached information");
                    persist.persist(cache);
                    log.info("Complete persistent cache information");
                } catch (Exception exception) {
                    log.error("File persistence exception", exception);
                }
            }
        }, 0, 10, TimeUnit.MINUTES);
    }

}

The interval of timed execution is 10min.

test

We only need to specify our persistence policy when creating the 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);

In order to ensure the completion of file persistence, we slept for a while.

File effect

  • 1.rdb

The generated file contents are as follows:

{"key":"2","value":"2"}
{"key":"1","value":"1"}

Corresponding cache load

We only need to implement the following corresponding loading, parse the file, and then initialize the cache.

/**
 * Load policy - file path
 * @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);

    /**
     * File path
     * @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] Start processing path: {}", dbPath);
        if(CollectionUtil.isEmpty(lines)) {
            log.info("[load] path: {} File content is empty, return directly", dbPath);
            return;
        }

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

            // implement
            // Simple types are OK, and complex deserialization will fail
            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...
    }
}

Then use it during initialization.

Summary

Here, we have completed a simple simulation of persistence similar to redis rdb.

However, for rdb, there are still some optimization points, such as rdb file compression, format definition, CRC verification and so on.

redis takes into account performance issues and the persistence mode of AOF. The two complement each other in order to achieve enterprise level caching effect.

We will introduce these features later.

If it's helpful to you, you're welcome to comment and collect a wave of attention~

Your encouragement is my greatest motivation~

Posted by ewillms on Sat, 14 May 2022 13:19:44 +0300