只有累積,沒有奇蹟

2019年2月24日 星期日

[.NET] 在 Redis 批次新增 List 資料的方案選擇

前言 
專案上遇到有個情境是針對 Redis 的 List 做大量新增的動作,但在新增的同時又希望兼顧效能,因此這篇文章是研究在 StackExchange.Redis 提供的 API 中幾個新增 List 的方式,如何使用以及簡單測試多筆資料時消耗的時間比較,若是有不清楚或是錯誤的地方歡迎討論予糾正

批次新增 List 型別 
由於考量到使用情境是類似 Queue 順序性是重要的,因此是透過 Redis 中的 List 型別做研究與測試,根據 StackExchange.Redis 提供的 API Document 批次新增多筆 List 型別可以使用下列方式
  • ListRightPush
  • CreateBatch
  • Lua Script
  • FireAndForget
以下就分別針對這三種方式做簡單介紹與說明,在介紹之前先說個重要的觀念 連線管理,在與 Redis 的連線會由 ConnectionMultiplexer 類別所管理,盡量避免每個 Request 都重新建立 TCP 連線與釋放,建議共用已經建立好的 Singleton 物件,連線管理主要 sample code 如下
private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
{
    string cacheConnection = "127.0.0.1:6379,syncTimeout =3000";
    return ConnectionMultiplexer.Connect(cacheConnection);
});

private static ConnectionMultiplexer RedisConnection => lazyConnection.Value; 

方式一 : ListRightPush
使用迴圈方式逐一的呼叫 ListRightPush 方法將資料新增到 Cache Server 上,缺點是每次請求都需要等待回傳的結果,如果在一次新增大量資料或是網路狀況不穩定時,有可能會發生 Timeout 的狀況
private static void InsertBy_ListRightPush(IEnumerable<string> input)
{
    foreach (var entity in input)
    {
        RedisConnection.GetDatabase().ListRightPush(name, entity);
    }
} 

方式二 : CreateBatch
CreateBatch 是把需要執行的指令打包成一個 script 送出去,接著等待指令執行的結果;使用方式是一開始使用 CreateBatch 指令,最後接 Execute 方法送出
private static void InsertBy_CreateBatch(IEnumerable<string> input)
{
    var batch = RedisConnection.GetDatabase().CreateBatch();
    
    foreach (var entity in input)
    {
        RedisConnection.GetDatabase().ListRightPush(name, entity);
    }
    
    batch.Execute();
} 

方式三 : Lua Script
在 Redis 中也支援 Lua script,Lua script 是一個輕量級內嵌式的程式語言,在 StackExchange.Redis 官網有提供使用 Lua script 語法說明文件 script,應用在此情境可以改為下列範例
private static void InsertBy_LuaScript(IEnumerable<string> input)
{
    var name = MethodBase.GetCurrentMethod().Name;

    StringBuilder sb = new StringBuilder();
    foreach (var item in input)
    {
        sb.AppendLine($"redis.call('rpush', 'Luascript', {item})");
    }
    
    var prepared = LuaScript.Prepare(sb.ToString());
    RedisConnection.GetDatabase().ScriptEvaluate(prepared);
} 
其中可以看到 redis.call 第一個參數為 rpush (List type);第二個參數為對應的 RedisKey;第三個參數為需要新增的資料,以此情境來說新增資料為 000001、000002、000003,因此需要針對輸出集合做轉換,將000001 前後加上 ''變為 '000001',對應到 Lua script 指令則為 rpush 'luascript' '000001','000002','000003' 

方式四 : FireAndForget
第一種的方式是會等待 Redis Server 新增結果,但如果你不想等待回傳結果時,可以設定 flags 為 fireAnd Forget,也就是俗稱的射後不理
private static void InsertBy_FireAndForget(IEnumerable<string> input)
{   
    foreach (var entity in input)
    {
        RedisConnection.GetDatabase().ListRightPush(name, entity, flags: CommandFlags.FireAndForget);
    }    
} 

測試花費時間 
以上針對其四種方式介紹其使用方式,接著想要針對特定筆數花費的時間做比較,在每個新增的 function 加上 stopwatch 時間戳記,來了解各自所花費的時間,透過新增成功後的數據資料作為應用前的評估

前置作業
Server : 在本機環境使用 Docker 架設 Redis Server,版本為5.0.3
Client : 測試專案為 .Net Framework Console App
系統資訊 
  • Windows 10 專業版
  • CPU : i7-8550U @1.8G
  • 記憶體 : 16G
套件 : 使用 StackExchange.Redis,安裝方式請參考 [Redis] C# 存取 Redis - 使用 StackExchange.Redis
測試情境 
  • 資料型態 List : 專案使用情境需求是要用到 Queue,測試資料型態為 List 
  • 新增資料方式 : 使用 StackExchange.Redis 提供的 ListRightPush、CreateBatch、Lua Script 以及 FireAndForget

測試代碼
根據前面提到的加上時間戳記,更新後的 sample code 如下
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using StackExchange.Redis;

namespace Redis
{
    class Program
    {
        private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
        {
            string cacheConnection = "127.0.0.1:6379,syncTimeout =3000";
            return ConnectionMultiplexer.Connect(cacheConnection);
        });

        private static ConnectionMultiplexer RedisConnection => lazyConnection.Value;
        private static Stopwatch sw = new Stopwatch();
        static void Main(string[] args)
        {
            cleaUpData();

            // initial Data ex : 000001, 000002, 000003..etc
            var count = 100;
            var data = new HashSet<string>();
            for (int i = 0; i < count; i++)
            {
                data.Add(i.ToString().PadLeft(6, '0'));
            }

            // use single insert 
            InsertBy_ListRightPush(data);

            // createBatch
            InsertBy_CreateBatch(data);

            // Lua script 
            InsertBy_LuaScript(data);
            
            // use single insert & fireAndForget
            InsertBy_FireAndForget(data);

            Console.ReadKey();
        }

        private static void cleaUpData()
        {
            var RedisDB = RedisConnection.GetDatabase();

            RedisDB.KeyDelete("ListRightPush");
            RedisDB.KeyDelete("FireAndForget");
            RedisDB.KeyDelete("CreateBatch");
            RedisDB.KeyDelete("Luascript");
        }

        private static void InsertBy_LuaScript(IEnumerable<string> input)
        {
            var name = MethodBase.GetCurrentMethod().Name;

            sw.Reset();
            sw.Start();

            StringBuilder sb = new StringBuilder();
            foreach (var item in input)
            {
                sb.AppendLine($"redis.call('rpush', 'Luascript', {item})");
            }
            
            var prepared = LuaScript.Prepare(sb.ToString());
            RedisConnection.GetDatabase().ScriptEvaluate(prepared);

            sw.Stop();
            Console.WriteLine($"{name} total cost : {sw.ElapsedMilliseconds}");
        }

        private static void InsertBy_CreateBatch(IEnumerable<string> input)
        {
            var name = MethodBase.GetCurrentMethod().Name;
            var batch = RedisConnection.GetDatabase().CreateBatch();
            sw.Reset();
            sw.Start();
            foreach (var entity in input)
            {
                RedisConnection.GetDatabase().ListRightPush(name, entity);
            }
            sw.Stop();
            batch.Execute();
            Console.WriteLine($"{name} total cost : {sw.ElapsedMilliseconds}");
        }

        private static void InsertBy_FireAndForget(IEnumerable<string> input)
        {
            var name = MethodBase.GetCurrentMethod().Name;
            sw.Reset();
            sw.Start();
            foreach (var entity in input)
            {
                RedisConnection.GetDatabase().ListRightPush(name, entity, flags: CommandFlags.FireAndForget);
            }
            sw.Stop();
            Console.WriteLine($"{name} total cost : {sw.ElapsedMilliseconds}");
        }

        private static void InsertBy_ListRightPush(IEnumerable<string> input)
        {
            var name = MethodBase.GetCurrentMethod().Name;
            sw.Reset();
            sw.Start();
            foreach (var entity in input)
            {
                RedisConnection.GetDatabase().ListRightPush(name, entity);
            }
            sw.Stop();
            Console.WriteLine($"{name} total cost : {sw.ElapsedMilliseconds}");
        }
    }
}

數據結果
method500100010000
ListRightPush4039916665
CreateBatch4476986701
Lua Script3831143
FireandForget912201
使用 Redis Desktop Manager 來確認資料,確認後寫入成功


心得
這篇介紹幾種新增 List 的方式與其簡單測試數據資料,排除掉射後不理來看(因為根本不會知道成功或失敗),用 Lua script 新增大量資料的回應時間都是最快的,其次是 createBatch 方式,可以提供需要使用的人數據與思考方向,原本想測試更多筆數的數據,但由於近期筆電不知原因每小時都會聽到風扇的哀號聲,有時覺得像是飛機要起飛了一樣,對於測試來說是一大挑戰,或許等到筆電康復之後再進行更大筆數據的測試,之後如果有新數據會在分享出來

參考
StackExchange.Redis

0 意見:

張貼留言

Copyright © m@rcus 學習筆記 | Powered by Blogger

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com