Redis 処理時間計測

Redisは永続化可能なインメモリデータベース。
連想配列、リスト、セットなどのデータ構造を扱えるキーバリューストア(KVS)。
ターンアラウンドタイム重視(オペレーション用途)、スケールアウト可能という位置付け。
Read/Writeが高速。キャッシュとして利用されることが多い。memcachedよりも機能は多い。
どのくらい高速なのか。大量データ登録の処理時間を計測してみよう!

図.データベースを分類する2つの軸、4つのエリア
f:id:masawan-guitar:20160812225135p:plain

f:id:masawan-guitar:20190501141631p:plain

<サーバー情報>
CPU = Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz
  →2008年頃に発売されていたCPU?けっこう古いやつです。
メモリ = 8 GB
OS = Ubuntu 18.04.2 LTS

◆処理時間計測結果&簡単に考察を記載
f:id:masawan-guitar:20190501181506p:plain
f:id:masawan-guitar:20190502201147p:plain

<処理時間計測結果(1回目)>
  10,000件  : 123ミリ秒
 100,000件 : 908ミリ秒
 200,000件 : 1秒 791ミリ秒
 500,000件 : 4秒 176ミリ秒
 1,000,000件 : 9秒 409ミリ秒

<処理時間計測結果(2回目)>
  10,000件  : 111ミリ秒
 100,000件 : 1秒 77ミリ秒
 200,000件 : 2秒 213ミリ秒
 500,000件 : 4秒 586ミリ秒
 1,000,000件 : 8秒 948ミリ秒

Redis Desktop Manager でデータ確認。
 → 100万件データ登録されている。
f:id:masawan-guitar:20190501182007p:plain

1万件データ登録 111ミリ秒、123ミリ秒。
100万件データ登録 9秒 409ミリ秒、8秒 948ミリ秒。
パフォーマンス良いと思う。
今回は、Key=string Value=stringで大量データ登録を試しただけ。
次回は、他の処理も試したい。
あと、大量データを「参照する処理」の時間計測をしてみようとは思う。
<追記>
後になって、本当にそんなに高速なのかなと思ってしまった部分があったので。
データ登録処理が終了した直後に「登録済データ件数」を取得して、
その登録済データ件数を画面に出力するように、クライアント側アプリのプログラムを改造してみたが。
ちゃんと全件がデータ登録できていたし、本当に高速だった。
f:id:masawan-guitar:20190503201800p:plain
<処理時間計測結果(3回目)>
  10,000件  : 133ミリ秒
 100,000件 : 1秒 133ミリ秒
 200,000件 : 1秒 950ミリ秒
 500,000件 : 4秒 698ミリ秒
 1,000,000件 : 9秒 23ミリ秒

<クライアント側で使用したアプリについて>
Redis処理時間計測のためにクライアント側のアプリを作成してみた。
Windows フォーム アプリケーション(.NET Framework 4.7.2)、言語は C#
StackExchange.Redis というライブラリを使用。
プログラムは参考までに。

◆処理時間計測に使用したC#プログラム
https://github.com/masawan/RedisLibrarygithub.com

FrmMain.cs

using StackExchange.Redis;
using Redis.DataAccess.Client;
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Threading.Tasks;

namespace RedisClient
{
    public partial class FrmMain : Form
    {
        public FrmMain()
        {
            InitializeComponent();
        }

        private static bool processingFlg = false;

        private async void btnExecSet_Click(object sender, EventArgs e)
        {
            processingFlg = true;

            RedisConnection conn = null;
            this.ChangeControlEnabled(false);
            txtResult.Text = string.Empty;

            try
            {
                conn = new RedisConnection();
                conn.Open();
                RedisCommand cmd = new RedisCommand(conn);
        
                //  (´・ω・`)ノ「令和(reiwa)」
                //フォームが無反応にならないようにするため
                //OutputProcessTime_Setの処理は、
                //別スレッドで実行して処理終了まで待つ(await)。
                await OutputProcessTime_Set(cmd,  10000);
                await OutputProcessTime_Set(cmd, 100000);
                await OutputProcessTime_Set(cmd, 200000);
                await OutputProcessTime_Set(cmd, 500000);
                await OutputProcessTime_Set(cmd,1000000);

            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "システムエラー", 
                                MessageBoxButtons.OK, 
                                MessageBoxIcon.Error);
            }
            finally
            {
                if(conn!=null)
                { 
                    conn.Close();
                }

                this.ChangeControlEnabled(true);
                txtResult.Select(this.txtResult.Text.Length, 0);
                processingFlg = false;
            }
        }

        private void ChangeControlEnabled(bool value)
        {
            btnExecSet.Enabled = value;
            txtResult.Enabled = value;
        }

        private Task OutputProcessTime_Set(RedisCommand cmd, int dataCount)
        {
            return Task.Run(() => {

                const char HALF_SPACE = ' ';
                Dictionary<RedisKey, RedisValue> dic;
                var sw = new System.Diagnostics.Stopwatch();
                TimeSpan ts;

                //データベース初期化
                cmd.FlushDatabases();

                //登録データ取得(データ件数分)
                dic = FrmMain.getInsertData(dataCount);

                // (´・ω・`)ノ  Let's Go!!
                //処理時間計測 スタート
                sw.Restart();

                //Redis Set実行
                cmd.Set(dic);

                //処理時間計測 ストップ
                sw.Stop();

               //処理時間を出力
                ts = sw.Elapsed;
                long dbSize = cmd.DbSize(); //登録済のデータ件数を取得
                string outputText
                    = $"{txtResult.Text}" +
                      $"{dataCount.ToString().PadLeft(7, HALF_SPACE)}件" +
                      $"{HALF_SPACE}" +
                      $"処理時間 = " +
                      $"{ts.Minutes.ToString().PadLeft(2, HALF_SPACE)}[m] " +
                      $"{ts.Seconds.ToString().PadLeft(2, HALF_SPACE)}[s] " +
                      $"{ts.Milliseconds.ToString().PadLeft(3, HALF_SPACE)}[ms]" +
                      $"{HALF_SPACE}" +
                      $"(登録データ件数 = " +
                      $"{dbSize.ToString().PadLeft(7, HALF_SPACE)}件)" +
                      $"\r\n";
                this.SetText(txtResult, outputText);

            });
        }

        private void SetText(TextBox textBox, string text)
        {
            if (textBox.IsDisposed) return;
            if (textBox.InvokeRequired)
            {
                this.Invoke((MethodInvoker) delegate {
                    SetText(textBox, text);
                });
            }
            else
            {
                textBox.Text 
                    = text;
            }
        }

        private static Dictionary<RedisKey,RedisValue>getInsertData(int dataCount)
        {
            var dic = new Dictionary<RedisKey, RedisValue>(dataCount);
            var sbKey = new StringBuilder();
            var sbValue = new StringBuilder();

            for (int count = 1; count <= dataCount; ++count)
            {
                sbKey.Clear();
                sbKey.Append("key");
                sbKey.Append(count.ToString().PadLeft(7, '0'));

                sbValue.Clear();
                sbValue.Append("value");
                sbValue.Append(count.ToString().PadLeft(7, '0'));

                dic.Add(sbKey.ToString(), sbValue.ToString());
            }

            return dic;
        }

        private void FrmMain_FormClosing(object sender, FormClosingEventArgs e)
        {
            if(processingFlg)
            {
                MessageBox.Show("処理実行中のため、画面を閉じることはできません。"
                               ,"情報"
                               , MessageBoxButtons.OK
                               , MessageBoxIcon.Information);
                e.Cancel = true;
            }
        }
    }
}

RedisConnection.cs

using StackExchange.Redis;
using Redis.DataAccess.Client.Properties;

namespace Redis.DataAccess.Client
{
    public class RedisConnection
    {
        private static ConnectionMultiplexer redisStore;
        private static IDatabase db;

        private const string CONFIGURATION_OPTIONS = "allowAdmin=true";

        private static readonly string configurationStringsDefault
            = Settings.Default.IPAddress
              + ":" + Settings.Default.PortNo.ToString()
              + "," + CONFIGURATION_OPTIONS;

        public RedisConnection()
        {
        }

        public void Open()
        {
            redisStore
                 = ConnectionMultiplexer.Connect(configurationStringsDefault);
            db = redisStore.GetDatabase();
        }

        public void Close()
        {
            if(redisStore!=null)
            {
                redisStore.Close();
                redisStore.Dispose();
            }
        }

        public IServer Server
        { get => redisStore.GetServer(Settings.Default.IPAddress, 
                                       Settings.Default.PortNo); }

        public string ConfigurationStrings
        { get => configurationStringsDefault; }

        public IDatabase Database { get => db; }

        public ConnectionMultiplexer RedisStore { get => redisStore; }

    }
}

RedisCommand.cs

using StackExchange.Redis;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Redis.DataAccess.Client
{
    public class RedisCommand
    {
        private static IDatabase db { get; set; }
        private static IServer server { get; set; }

        public RedisCommand(RedisConnection conn)
        {
            server = conn.Server;
            db = conn.Database;
        }
        public long DbSize()
        {
            return server.DatabaseSize();
        }
        public void FlushDatabases()
        {
            server.FlushAllDatabases();
        }
        public bool Set(RedisKey key, RedisValue value)
        {
            return db.StringSet(key, value);
        }
        public void Set(Dictionary<RedisKey, RedisValue> dic)
        {
            var setTasks = new List<Task>();
            foreach (KeyValuePair<RedisKey, RedisValue> kvp in dic)
            {
                Task<bool> setAsync = db.StringSetAsync(kvp.Key, kvp.Value);
                setTasks.Add(setAsync);
            }
            Task[] tasks = setTasks.ToArray();
            setTasks.Clear();
            setTasks.TrimExcess(); // (´・ω・`)ノ Clear! and TrimExcess !
            Task.WaitAll(tasks);
        }
    }
}

◆メモ「StackExchange.Redis pipelining」
optimization - Pipelining vs Batching in Stackexchange.Redis - Stack Overflow
Redis for .NET Developers – Redis Pipeline Batching | Taswar BhattiTaswar Bhatti