preface

We have initially implemented our cache in implementing a fixed size cache from the Zero handwriting Cache framework (1).

We implemented the expiration feature of key in implementing expiration feature from Zero Handwriting Cache framework (1).

In this section, let’s learn how to implement a persistence pattern similar to RDB in Redis.

The purpose of persistence

The information we store is stored directly in memory, and if the power goes out or the application restarts, it’s all lost.

Sometimes we want this information to be there after a restart, just like redis is.

The load is loaded

instructions

Before implementing persistence, let’s look at a simple requirement:

How to specify initial loading information when the cache is started.

Implementation approach

It is not difficult to set the corresponding information directly during cache initialization.

api

ICacheLoad interface is defined to facilitate later expansion.

public interface ICacheLoad<K.V> {

    /** * Load cache information *@paramCache cache *@since0.0.7 * /
    void load(final ICache<K,V> cache);

}
Copy the code

Customize the initialization policy

When we initialize, we put in two fixed pieces of information.

public class MyCacheLoad implements ICacheLoad<String.String> {

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

test

You only need to specify the corresponding load implementation class when the cache is initialized.

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

Assert.assertEquals(2, cache.size());
Copy the code

persistence

instructions

Initial loading was introduced above, but cache persistence is already half done.

The other thing we need to do is to persist the contents of the cache to a file or database so that they can be loaded at initialization time.

The interface definition

To facilitate flexible substitution, we define a persistent interface.

public interface ICachePersist<K.V> {

    /** * Persistent cache information *@paramCache cache *@since0.0.7 * /
    void persist(final ICache<K, V> cache);

}
Copy the code

Simple implementation

We implement a very simple JSON-based persistence, although we can add aOF-like persistence schemes later.

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

    /** * Database path *@since0.0.8 * /
    private final String dbPath;

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

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

        // Create a file
        FileUtil.createFile(dbPath);
        // Clear the 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 = newPersistEntry<>(); persistEntry.setKey(key); persistEntry.setValue(entry.getValue()); persistEntry.setExpire(expireTime); String line = JSON.toJSONString(persistEntry); FileUtil.write(dbPath, line, StandardOpenOption.APPEND); }}}Copy the code

Regularly perform

A persistent policy is defined, but no trigger is provided.

We design in a way that is transparent to the user: timed execution.

public class InnerCachePersist<K.V> {

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

    /** * Cache information *@since0.0.8 * /
    private final ICache<K,V> cache;

    /** * Cache persistence policy *@since0.0.8 * /
    private final ICachePersist<K,V> persist;

    /** * thread execution class *@since0.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();
    }

    /** * initialize *@since0.0.8 * /
    private void init(a) {
        EXECUTOR_SERVICE.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run(a) {
                try {
                    log.info("Start persisting cache information");
                    persist.persist(cache);
                    log.info("Complete persistent caching of information");
                } catch (Exception exception) {
                    log.error("File persistence exception", exception); }}},0.10, TimeUnit.MINUTES); }}Copy the code

The scheduled execution interval is 10 minutes.

test

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

We slept for a while to make sure the file persisted.

Effect of file

  • 1.rdb

The content of the generated file is as follows:

{"key":"2","value":"2"}
{"key":"1","value":"1"}
Copy the code

Corresponding cache loading

We simply perform the following load, parse the file, and initialize the cache.

/** * Load policy - file path *@author binbin.hou
 * @since0.0.8 * /
public class CacheLoadDbJson<K.V> implements ICacheLoad<K.V> {

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

    /** * File path *@since0.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;
            }

            / / execution
            // Simple types are ok, complex deserializations 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...}}Copy the code

Then use it during initialization.

summary

At this point, we have a simple simulation of redis RDB-like persistence.

But for RDB, there are optimization points, such as RDB file compression, format definition, CRC check, and so on.

Redis takes into account performance issues and AOF’s persistence mode, both of which complement each other to achieve enterprise-level caching.

We’ll introduce these features as we go along.

If it helps you, please feel free to like it

Your encouragement, is my biggest motivation ~