只有累積,沒有奇蹟

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 如下
  1. private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
  2. {
  3. string cacheConnection = "127.0.0.1:6379,syncTimeout =3000";
  4. return ConnectionMultiplexer.Connect(cacheConnection);
  5. });
  6.  
  7. private static ConnectionMultiplexer RedisConnection => lazyConnection.Value;

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

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

方式三 : Lua Script
在 Redis 中也支援 Lua script,Lua script 是一個輕量級內嵌式的程式語言,在 StackExchange.Redis 官網有提供使用 Lua script 語法說明文件 script,應用在此情境可以改為下列範例
  1. private static void InsertBy_LuaScript(IEnumerable<string> input)
  2. {
  3. var name = MethodBase.GetCurrentMethod().Name;
  4.  
  5. StringBuilder sb = new StringBuilder();
  6. foreach (var item in input)
  7. {
  8. sb.AppendLine($"redis.call('rpush', 'Luascript', {item})");
  9. }
  10. var prepared = LuaScript.Prepare(sb.ToString());
  11. RedisConnection.GetDatabase().ScriptEvaluate(prepared);
  12. }
其中可以看到 redis.call 第一個參數為 rpush (List type);第二個參數為對應的 RedisKey;第三個參數為需要新增的資料,以此情境來說新增資料為 000001、000002、000003,因此需要針對輸出集合做轉換,將000001 前後加上 ''變為 '000001',對應到 Lua script 指令則為 rpush 'luascript' '000001','000002','000003' 

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

測試花費時間 
以上針對其四種方式介紹其使用方式,接著想要針對特定筆數花費的時間做比較,在每個新增的 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 如下
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Linq;
  5. using System.Reflection;
  6. using StackExchange.Redis;
  7.  
  8. namespace Redis
  9. {
  10. class Program
  11. {
  12. private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
  13. {
  14. string cacheConnection = "127.0.0.1:6379,syncTimeout =3000";
  15. return ConnectionMultiplexer.Connect(cacheConnection);
  16. });
  17.  
  18. private static ConnectionMultiplexer RedisConnection => lazyConnection.Value;
  19. private static Stopwatch sw = new Stopwatch();
  20. static void Main(string[] args)
  21. {
  22. cleaUpData();
  23.  
  24. // initial Data ex : 000001, 000002, 000003..etc
  25. var count = 100;
  26. var data = new HashSet<string>();
  27. for (int i = 0; i < count; i++)
  28. {
  29. data.Add(i.ToString().PadLeft(6, '0'));
  30. }
  31.  
  32. // use single insert
  33. InsertBy_ListRightPush(data);
  34.  
  35. // createBatch
  36. InsertBy_CreateBatch(data);
  37.  
  38. // Lua script
  39. InsertBy_LuaScript(data);
  40. // use single insert & fireAndForget
  41. InsertBy_FireAndForget(data);
  42.  
  43. Console.ReadKey();
  44. }
  45.  
  46. private static void cleaUpData()
  47. {
  48. var RedisDB = RedisConnection.GetDatabase();
  49.  
  50. RedisDB.KeyDelete("ListRightPush");
  51. RedisDB.KeyDelete("FireAndForget");
  52. RedisDB.KeyDelete("CreateBatch");
  53. RedisDB.KeyDelete("Luascript");
  54. }
  55.  
  56. private static void InsertBy_LuaScript(IEnumerable<string> input)
  57. {
  58. var name = MethodBase.GetCurrentMethod().Name;
  59.  
  60. sw.Reset();
  61. sw.Start();
  62.  
  63. StringBuilder sb = new StringBuilder();
  64. foreach (var item in input)
  65. {
  66. sb.AppendLine($"redis.call('rpush', 'Luascript', {item})");
  67. }
  68. var prepared = LuaScript.Prepare(sb.ToString());
  69. RedisConnection.GetDatabase().ScriptEvaluate(prepared);
  70.  
  71. sw.Stop();
  72. Console.WriteLine($"{name} total cost : {sw.ElapsedMilliseconds}");
  73. }
  74.  
  75. private static void InsertBy_CreateBatch(IEnumerable<string> input)
  76. {
  77. var name = MethodBase.GetCurrentMethod().Name;
  78. var batch = RedisConnection.GetDatabase().CreateBatch();
  79. sw.Reset();
  80. sw.Start();
  81. foreach (var entity in input)
  82. {
  83. RedisConnection.GetDatabase().ListRightPush(name, entity);
  84. }
  85. sw.Stop();
  86. batch.Execute();
  87. Console.WriteLine($"{name} total cost : {sw.ElapsedMilliseconds}");
  88. }
  89.  
  90. private static void InsertBy_FireAndForget(IEnumerable<string> input)
  91. {
  92. var name = MethodBase.GetCurrentMethod().Name;
  93. sw.Reset();
  94. sw.Start();
  95. foreach (var entity in input)
  96. {
  97. RedisConnection.GetDatabase().ListRightPush(name, entity, flags: CommandFlags.FireAndForget);
  98. }
  99. sw.Stop();
  100. Console.WriteLine($"{name} total cost : {sw.ElapsedMilliseconds}");
  101. }
  102.  
  103. private static void InsertBy_ListRightPush(IEnumerable<string> input)
  104. {
  105. var name = MethodBase.GetCurrentMethod().Name;
  106. sw.Reset();
  107. sw.Start();
  108. foreach (var entity in input)
  109. {
  110. RedisConnection.GetDatabase().ListRightPush(name, entity);
  111. }
  112. sw.Stop();
  113. Console.WriteLine($"{name} total cost : {sw.ElapsedMilliseconds}");
  114. }
  115. }
  116. }

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


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

參考
StackExchange.Redis

Related Posts:

  • [Azure] App Service Diagnostics - 應用服務診斷 前言 目前工作主要服務都是放在 Azure 服務上,因此接觸到 Azure 時間也越來越多,當應用程式發生問題時要如何進行診斷找到 Root cause 呢 ? 過去在地端機房上可能可以先取得執行當下的 Memory Dump 檔案,再把抓下來的 dump file 透過一些記憶體分析工具像是 DebugDiag Tools 或是 WinDBG 來分析原因,那如果今天我們使用的是雲端 Azure 的 Paas 服務該如何處理呢 ? 今天就… Read More
  • [WEB API] Swagger - 在 Headers 中新增 API Token 驗證問題 在開發 API 時都會在網站加上 API Token 機制,當收到一個 Request 請求時 API 會驗證 Token 的正確性,確認請求參數中的 Token 是否是有效 / 已授權 / 有沒有過期或是用來當 SSO (Single sign-on) 的使用,驗證無誤後才會進入接口的邏輯處理,目前公司內部 API 專案也有驗證 Token 的設計,之前文章介紹了 Swagger 基本使用… Read More
  • [WEBAPI] Swagger - 用 Swashbuckle.Examples 加上有意義的測試數據 問題 Swagger 是一個可以將 WebAPI 快速文件化的套件,產生出來的線上文件除了可以列出 API 詳細資料外還可以直接在網頁上進行測試的動作,對開發者和接 API 的使用者來說十分方便,上一篇文章介紹了 Swagger 基本使用 說明,最近想要在公司內部推廣使用 Swagger 服務,資深同事提到過去有陣子曾經使用過 Swagger 服務,但  每次要使用 API 接口服務時參數 (params)… Read More
  • [Azure] App Service Diagnostics - Collect Memory Dump 前言 前兩篇分別介紹了 App Service Diagnostics 中的 Collect .Net Profiler Trace 與 Auto heal,分別都可以透過工具來蒐集雲端伺服器的緩慢問題分析與蒐集記憶體資訊,這一篇則是介紹如何 dump 目前伺服器 memory 的資料,以及有多個伺服器的時候該如何抓取特定的 Server memory data。若對於上述內容有問題或是不清楚的地方,歡迎提出來一起討論。 Colle… Read More
  • [Azure] App Service Diagnostics - Auto-Heal 前言 上一篇提到了如何在 Azure 取得當下的 memory dump 資訊 App Service Diagnostics - 應用服務診斷,這一篇則是透過另外一種方式使用 Auto-heal 的方式設定 memory 達到一定的水位時,觸發自動收集 memory 的使用狀況,並在自訂的條件下抓取 memory 資料,並指定 dump file 放置在某個 storage account 帳號中。若對於上述內容有問題或是不清楚的地方,… Read More

0 意見:

張貼留言

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

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com