MemcachedからRedisへ移行する為の検証をやってみた。

MemcachedからRedisへ移行検証なのか・・

Memcachedとしては、 オンメモリストレージ 、シンプルなプロトコル等といった特徴があるので性能重視の処理等で重宝して使用していたが、デメリットとして、データの揮発性があげられる。
データの揮発性を考慮し、MemcachedからRedis移行が容易にできるのかといったことを実施してみる。

各説明は本家サイトをご参照ください。
https://redis.io/
https://memcached.org/

環境、バージョンについて

・CentOS Linux release 7.5
・Redis:4.10.0
・Redis client:3.0.1
・Java:1.8.0_191

使用ライブラリーについて

・Mavenプロジェクトで作成したプロジェクトのpom.xmlに以下を追加。

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.0.1</version>
        </dependency>
        <dependency>
            <groupId>net.spy</groupId>
            <artifactId>spymemcached</artifactId>
            <version>2.11.4</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.4</version>
        </dependency>

簡易的なJavaクライアントを作成し、記述内容を比較

簡易的なMemcached Javaクライアント

expireの設定でミリ秒なのか、秒なのかを考慮した実装を行わないといけないので、すべてCalendarクラスを使用した変換処理が必要となっているのでひと手間が必要となっている。

import java.util.Calendar;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import net.spy.memcached.AddrUtil;
import net.spy.memcached.ConnectionFactoryBuilder;
import net.spy.memcached.FailureMode;
import net.spy.memcached.MemcachedClient;

/**
 * SpyMemcacheClient
 */
public class SpyMemcacheClient {

    private MemcachedClient client = null;

    private static final int CONNECTION_TIMEOUT = 500;

    private static final int SOCKET_TIMEOUT = 500;

    /**
     * SpyMemcacheClientの生成
     * @param ipAndPort IP:PORT形式で指定
     */
    public SpyMemcacheClient(final String ipAndPort) {
        try {

            ConnectionFactoryBuilder builder =
                    new ConnectionFactoryBuilder().setProtocol(ConnectionFactoryBuilder.Protocol.TEXT)
                            .setFailureMode(FailureMode.Redistribute).setDaemon(true).setOpTimeout(CONNECTION_TIMEOUT)
                            .setMaxReconnectDelay(CONNECTION_TIMEOUT).setFailureMode(FailureMode.Retry);

            this.client = new MemcachedClient(builder.build(), AddrUtil.getAddresses(ipAndPort));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 指定キーでValue値(String型)を取得する。
     *
     * @param key 指定キー
     * @return value値(String型)
     * @throws Exception
     */
    public String getValue(final String key) throws Exception {

        return (String) getObjectValue(key);
    }

    /**
     * 指定キーでValue値(Object型)を取得する。
     *
     * @param key 指定キー
     * @return value値(Object型)
     * @throws Exception
     */
    public Object getObjectValue(final String key) throws Exception {
        Future<Object> f = null;
        try {
            f = client.asyncGet(key);
            Object value = f.get(SOCKET_TIMEOUT, TimeUnit.MILLISECONDS);
            return value;
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            if (f != null) {
                f.cancel(true);
            }
            throw e;
        }
    }

    /**
     * 指定キー、指定value値で登録する。
     *
     * @param key 指定キー
     * @param value value値
     * @return boolean値(登録成功:true, 登録失敗:false)
     */
    public boolean setValue(final String key, final String value) {
        boolean res = true;
        Future<Boolean> f = null;
        try {
            f = client.set(key, 0, value);
            res = f.get(SOCKET_TIMEOUT, TimeUnit.MILLISECONDS);

        } catch (Exception e) {
            if (f != null) {
                f.cancel(true);
            }
            res = false;
            e.printStackTrace();
        }

        return res;
    }

    /**
     * 指定キー、指定value値、指定expire値で登録する。
     *
     * @param key 指定キー
     * @param value 指定value値(String型)
     * @param expires 指定expire値(ミリ秒)
     * @return boolean値(登録成功:true, 登録失敗:false)
     */
    public boolean setValue(final String key, final String value, final int milliSec) {

        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.MILLISECOND, milliSec);

        return setObjectValue(key, value, cal);
    }

    /**
     * 指定キー、指定value値、指定expire値で登録する。
     *
     * @param key 指定キー
     * @param value 指定value値(String型)
     * @param cal 指定expire値(Calendar)
     * @return boolean値(登録成功:true, 登録失敗:false)
     */
    public boolean setValue(final String key, final String value, final Calendar cal) {

        return setObjectValue(key, value, cal);
    }

    /**
     * 指定キー、指定value値、指定expire値で登録する。
     *
     * @param key 指定キー
     * @param value 指定value値(Object型)
     * @param cal 指定expire値(Calendar)
     * @return boolean値(登録成功:true, 登録失敗:false)
     */
    public boolean setObjectValue(final String key, final Object value, final Calendar cal) {
        boolean res = true;

        Future<Boolean> f = null;
        try {
            f = client.set(key, (int) (cal.getTime().getTime() / 1000), value);
            res = f.get(SOCKET_TIMEOUT, TimeUnit.MILLISECONDS);

        } catch (Exception e) {
            if (f != null) {
                f.cancel(true);
            }
            e.printStackTrace();
        }

        return res;
    }

}

簡易的なRedis Javaクライアント

expireの設定として秒、ミリ秒の設定をライブラリーとして準備されているので必要に応じて呼び出すだけとなっている。

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.params.SetParams;

/**
 * RedisClient
 *
 */
public class RedisClient implements ClientInterface {

    private JedisPool pool;

    /**
     * Redisクライアントの生成
     * @param poolName
     */
    public RedisClient(final String ipAndPort) {

        if (pool == null || pool.isClosed()) {
            synchronized (this) {
                String[] arrays = ipAndPort.split(":");

                JedisPoolConfig conf = new JedisPoolConfig();
                conf.setMaxIdle(200);
                conf.setMinIdle(100);
                conf.setMaxTotal(200);
                this.pool = new JedisPool(conf, arrays[0], Integer.parseInt(arrays[1]));
            }
        }
    }

    /**
     * 指定key, value, expires(seconds)で登録する。
     * @param key
     * @param value
     * @param seconds
     * @return
     */
    public String setex(final String key, final String value, final int seconds) {
        return this.pool.getResource().setex(key, seconds, value);
    }

    /*
     * (非 Javadoc)
     *
     * @see jp.tech.blog.sample.ClientInterface#setpx(java.lang.String, java.lang.String, long)
     */
    @Override
    public String setpx(final String key, final String value, final long milliseconds) {

        SetParams params = new SetParams();
        params.px(milliseconds);
        return this.pool.getResource().set(key, value, params);

    }

    /* (非 Javadoc)
     * @see jp.tech.blog.sample.ClientInterface#get(java.lang.String)
     */
    @Override
    public String get(final String key) {
        return this.pool.getResource().get(key);
    }

    /**
     * Connection Resource information.
     */
    public void info() {
        System.out.println(this.pool.getResource().info());
    }

    /**
     * Connection Pool Closed.
     */
    public void close() {
        if (!this.pool.isClosed()) {
            this.pool.close();
        }
    }

}

まとめ

Memcached、Redisの簡易的なJavaクライアントを今回作成してみて、Redisを使用したクライアントの方がより、メソッドが用意されているように思われ、導入するにあたっての難易度は高くないように思いました。
上記、クライアントを200スレッド、登録、取得を 10000回 繰り返しを行った際でも、性能的にも問題になりそうな面はなさそう。
※あくまでもオンメモリー上にデータがある場合

今回の検証内容として、MemcachedからRedisに移行にするにあたってクライアント周りについて記載してみました。
次回はさらに、ファイルから取得したケースでどのくらい性能が変わってくるのか測定をやってみたいと思います。