只有累積,沒有奇蹟

顯示具有 Redis 標籤的文章。 顯示所有文章
顯示具有 Redis 標籤的文章。 顯示所有文章

2022年5月23日 星期一

[NETCore] ASP.NET Core 限流框架 AspNetCoreRateLimit 整合 Redis

前言
在上一篇介紹了 ASP.NET Core 中的限流框架 AspNetCoreRateLimit,紀錄使用者 Request 的 IP 作為限定流量的判斷來源,並將計數器的值存放在 Server 的 Memory 中,存放在 Memory 中如果 Server 數量單台的話會沒有問題,如果 Server 數量不只一台就需要共用的 Cache Server 來存放 Request 資訊,此時可以搭配 Redis 作為 Cache server 使用,讓所有 Server 在判斷限流時都具備相同的限制條件,這篇就紀錄 AspNetCoreRateLimit 整合 Redis 的操作與設定,若有問題或是錯誤的地方歡迎網路的高手大大給予指導。

安裝 Redis 
首先,起手式先來安裝 Redis,想使用 Redis 可以參考之前的介紹文章

  • Window : 在 Windows 下載與安裝 Redis、使用 Docker 安裝 Redis
  • Linux : 在 CentOS7 上安裝 Redis
  • 這裡直接使用 Docker 指令來起 Redis 服務,在 powershell 輸入下列指令
    > docker run --name redis-throttle -p 6379:6379 -d redis
    啟動成功會顯示 docker 的 container ID 資訊在畫面上,接著在輸入 docker ps -a 確認
    > docker ps -a
    CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS                    NAMES
    23e010857e85        redis               "docker-entrypoint.s…"   4 seconds ago         Up 2 seconds   0.0.0.0:6379->6379/tcp   redis-throttle
    
    Redis 啟動成功,對應的 Port 號為 6379

    設定 AspNetCoreRateLimit  
    在前一篇完整的介紹 AspNetCoreRateLimit 設定方式與代碼,這裡僅說明差異的地方,要使用 Redis Cache 可以使用  IDistributedCache ,IDistributedCache 在 ASP.NET Core 中命名空間為 Microsoft.Extensions.Caching.Redis,因此下一步是進行安裝的動作,在 Nuget Console 輸入下列指令
    Install-Package Microsoft.Extensions.Caching.Redis -Version 2.2.0
    接著到 startup.cs 將原本儲存在 memory 的地方,改成存到 Redis 並且定義 Redis 的連線字串內容
        public void ConfigureServices(IServiceCollection services)
        {
            // 省略
    
            // 注入 counter and IP Rules 
            //services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
            //services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
            services.AddSingleton<IIpPolicyStore, DistributedCacheIpPolicyStore>();
            services.AddSingleton<IRateLimitCounterStore, DistributedCacheRateLimitCounterStore>();
            services.AddDistributedRedisCache(option =>
            {
                option.Configuration = Configuration["Redis:ConnectionString"];
                option.InstanceName = Configuration["Redis:InstanceName"];
            });
        }
    代碼中定義了 Redis Server 的連線字串與名稱,Throttle 的 Rule 設定為 10m 僅能訪問 1次,因此我們需要到 appsettuing.json 加上 Redis 的設定資料
    "Redis": {
        "ConnectionString": "127.0.0.1:6379",
        "InstanceName": "Redis Throttle"
    },

    確認 Redis 資料 
    此時,在重新執行應用程式可以發現當達到設定的次數上限時,就會跳出我們熟悉阻擋的訊息
    API calls quota exceeded! maximum admitted 1 per 10m.
    再來使用 Redis Desktop Manager 連線到 Docker 內的 Redis,可以發現當達到上限次數時 Redis 有存放相關的 Request 資料,如下圖所示
    AspNetCoreRateLimit 將資料儲存在 Redis 成功,大功告成 !!!

    2020年4月30日 星期四

    [Tool] Redis 管理工具 - Another Redis Desktop Manager

    前言
    在之前推薦過 Redis 管理工具 [Redis] Redis 管理工具 - Redis Desktop Manager,方便開發者可以輕鬆的使用 GUI 工具查看或設定 Redis 的資訊,如果要下載新版使用則需要另外付費 (4.99/Month ),最近又發現另一款好用的 Redis 管理工具 Another Redis Desktop Manager,在 Github 說明如下
    A faster, better and more stable redis desktop manager, compatible with Linux, windows, mac. What's more, it won't crash when loading a large number of keys.
    除了支援 Windows、Linux 與 Mac 三種環境之外,語系部分還支援繁體中文說明,經過幾天的使用個人覺得不輸給 Redis Desktop Manager,今天這篇文章就來推坑這款好用的工具,若有問題歡迎提出來一起討論。

    安裝
    下載位置可以從 Github 頁面 Another Redis Desktop Manager 的 release 進行下載安裝檔的動作,舉例來說目前最新版本為 v1.3.5,裡面就提供各個環境的安裝檔案,如果是使用 Windows 可以選擇 Another.Redis.Desktop.Manager.1.3.5.exe 進行下載的動作

    接著就是大家熟悉的安裝流程,點選下一步到完成為止

    建立連線
    在開啟應用程式之後,第一步就是要連線到 Redis Server,為了 Demo 方便這裡我先使用 Docker 在本機啟用 Redis,port 為預設的 6379,接著是點選左上角的 New connection,輸入要連線到的 Redis Server 內容,按下 OK 選擇下一步

    Redis 資訊
    與 Redis Server 建立連線成功之後,即可以看到目前 Redis 目前的資訊像是使用 Redis 使用的版本為和、記憶體的使用量,目前的連線數等資訊,另外在右下方也可以看的到目前我們存放在 Redis key 的資料數據,這些數據分別存放在哪一個 Redis 中哪一個 db 位置
    Redis Console
    如果習慣 command 指令的朋友,也可以在應用程式直接下指令來達到你的目的,舉例來說在 Console 輸入 Ping 則會回覆 PONG 關鍵字

    新增 key & 資料
    也支援在介面上新增 Redis key 與資料,舉例來說我新增一個 key 為 test 資料型態為 hash,接著可以繼續在介面上新增內容資料,這功能最近在開發新專案時很常用到,可以很方便的加入所需要的資料針對專案與 Redis 的介接與測試,不用在另外熟記指令來進行新增/調整資料的動作 (我比較懶惰...)

    感想
    另外還有支援繁體中文語系,進階搜尋 Redis key 與切換 dark 介面等功能,整體來說目前使用上功能蠻完善的,使用了幾天也未遇到當機或是異常 crash 等問題,對於 Redis 管理工具有需求的朋友,或許可以嘗試看看 Another Redis Desktop Manager,如果有不清楚的地方歡迎一起討論,hope it helps !


    參考
    AnotherRedisDesktopManager


    2019年3月12日 星期二

    [Redis] 在 CentOS7 上安裝 Redis

    前言
    之前已經介紹過 Redis 在 Windows 上的安裝與使用,今天要分享的是在 CentOS7 上安裝 Redis 的使用步驟與說明,由於是全新的機器,因此此篇會一起介紹 Docker 的安裝,若有問題歡迎提出一起討論或是給予指導

    CentOS 安裝 Docker
    Docker 支援 CentOS7 之後的版本,因此不用在安裝套件,連線到 Linux 的工具可以參考 跨平台 SSH Client 工具 - Termius

    首先,輸入以下指令直接安裝 Docker 
    sudo yum install docker
    安裝後啟動服務並設定系統開啟時載入
    sudo service docker start
    sudo chkconfig docker on
    下載完 Docker 之後先確認目前版本 ( 個人習慣 XDDD) 
    Client:
     Version:         1.13.1
     API version:     1.26
     Package version: docker-1.13.1-91.git07f3374.el7.centos.x86_64
     Go version:      go1.10.3
     Git commit:      07f3374/1.13.1
     Built:           Wed Feb 13 17:10:12 2019
     OS/Arch:         linux/amd64
    
    Server:
     Version:         1.13.1
     API version:     1.26 (minimum version 1.12)
     Package version: docker-1.13.1-91.git07f3374.el7.centos.x86_64
     Go version:      go1.10.3
     Git commit:      07f3374/1.13.1
     Built:           Wed Feb 13 17:10:12 2019
     OS/Arch:         linux/amd64
     Experimental:    false
    在輸入指令前都要加上   sudo  以 root 的權限去執行指令,如果未加上則會跳出錯誤訊息
    Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post http://%2Fvar%2Frun%2Fdocker.sock/v1.26/images/create?fromImage=redis&tag=latest: dial unix /var/run/docker.sock: connect: permission denied

    CentOS 安裝 Redis
    Docker 指令與之前在 Windows 上安裝 Redis 時相同,詳細可以參考 官方說明 

    Step 1 : 取得 Redis 印象檔,使用  docker pull  指令從 docker hub 倉庫取得 redis 最新的映像檔,下載過程中會輸出映像檔每一層資訊
    sudo docker pull redis
    Step 2 : 下載完 image 之後,使用  docker run  指令啟動容器
    sudo docker run --name redis -p 6379:6379 -d redis
    參數  -p  是指定 port
    參數  -d  指定容器在背景執行
    Step 3 : 使用  docker ps  指令確認目前 container ID
    CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
    1928595e0ae5        redis               "docker-entrypoint..."   11 seconds ago      Up 10 seconds       0.0.0.0:6379->6379/tcp   redis
    Step 4 : 得知 container ID 之後使用  exec  進入容器內測試 Redis cli 指令
    sudo docker exec -it 192 bash
    Step 5 : 接著在輸入  redis-cli ,測試 Redis 功能是否正常 PING > PONG 以及測試 String get/set
    root@1928595e0ae5:/data# redis-cli
    127.0.0.1:6379> ping
    PONG
    127.0.0.1:6379> set test "hello world"
    OK
    127.0.0.1:6379> get test
    "hello world"
    測試正常,安裝成功

    感想
    對於一個 Linux 新手來說整個過程是挺好玩的,漸漸感受到沒有 GUI 工具直接下 command 的威力,但在安裝與查指令上也確實花了不少時間,日後希望也可以加深對於 Linux 的了解,讓自己接觸的領域可以更廣一些 :)
      參考
      Docker Commandline

      2019年2月26日 星期二

      [.NET] StackExchange.Redis.RedisServerException : 'ERR Error compiling script'

      問題 
      這幾天在對新專案做 POC,開始對 Redis Server 新增資料做測試,有個 Test Case 在使用 Lua script 新增多筆資料到 Redis Server 時候發生錯誤,錯誤訊息為  "StackExchange.Redis.RedisServerException: 'ERR Error compiling script (new function): user_script:1: function or expression too complex near ''insert script'' ' " ,這篇就針對此案例作簡單紀錄與分享,若是有不清楚或是錯誤的地方歡迎討論予糾正。

      解決方法 
      首先先看一下案發現場的 code 如下
      private static void InsertBy_LuaScript(IEnumerable<string> input)
      {
          var insertValues = String.Join(",", input.Select(o => $"'{o}'"));
          string luaScript = $"redis.call('rpush', 'Luascript', {insertValues})";
      
          var prepared = LuaScript.Prepare(luaScript);
          RedisConnection.GetDatabase().ScriptEvaluate(prepared);    
      } 
      針對 function 所傳遞進來的集合做字串相加,最後在將所有相加後的字串當作 redis.call 的參數,舉例輸入集合為 test01、test02、test03,其變數 luaScript 結果 redis.call('rpush','Luascript','test01', 'test02', 'test03') ,最後將此 script 傳入 LuaScript.Prepare API 執行;此語法在測試數量少的時候正常,但後期逐漸地把測試筆數加到 300 筆時,就會產生執行失敗跳出下列錯誤訊息
      經 debug 後發現異常時其 lua script 輸出如下
      redis.call('rpush', 'Luascript', '0','1','2'...'299')
      該 script 當遇到新增筆數量大時,就會變成參數過度擴增的狀況,有可能是因為 redis.call 支援不了這麼多參數,且用此段語法在 lua script online 進行測試也是 compiler error,因此心中判定此種寫法失格出局,改良後的寫法如下
      private static void InsertBy_LuaScript(IEnumerable<string> input)
      {    
          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);
      } 
      使用 stringBuilder 將輸入集合內容匯總為字串,每筆輸出都單獨定義其 redis.call 其設定值參數,改完後測試筆數可超越原本的限制 300 筆,測試 10000 筆資料也沒問題,宣告結案打完收工 

      心得
      對於 lua script 不熟悉因此踩到這基本的雷,但轉個念頭想也是因為自己因為對於基本語法不熟悉,才會造成錯誤的使用,也提醒下次遇到類似情況時可以先搞清楚觀念,或許很多雷就不會在踩到。

      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

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

      Design by Anders Noren | Blogger Theme by NewBloggerThemes.com