Debugging Redis Lua Script with C# on macOS

分享如何在 .NET Core 使用 C# 正確的使用 Redis Lua Script 開發與除錯流程。

先前分享如何使用 ZeroBrane Studio 協助 Redis 的 Lua Script 開發與除錯介紹如何正確地使用工具來幫我們除錯 Redis Lua Script.

本次結合 C#, .NET Core 與 macOS 環境,重新整理與介紹如何正確地使用 Redis Lua Script.

為什麼使用 Redis Lua Script

在大部分的開發者中,會使用 Redis 相關 framework 的人很普遍,但絕大多數僅使用這些 Framework 已包裹好的指令做操作,鮮少自己將商業邏輯包裹成一個 Lua Script 指令操作.

以 C# + .NET Core 為例,大多使用者都會使用下列的 framework 操作 Redis 內資料:

如果今天要開發的一個資料儲存的情境如下:

  • 確認當前 test 是否存在,如果不存在則在第一次呼叫的時候給予預設值 0
  • 第二次開始的呼叫會針對 test 當前的值每次增加 50

以一般 Redis 的指令操作來說我們需要透過多個指令串接以上內容,這會造成 .NET Core 的程式多次進出 Redis Instance 內.

而透過 Lua Script 以上的指令可以僅透過一個客製的指令進行操作,大幅提升 Redis 效能與反應.

Redis的架構設計單執行緒的設計,在運行Lua script的時候是沒辦法處理其他的請求的,所以Lua script並不能像Database的Stored Procedure一樣運行複雜的商務邏輯,個人認為如果有以下情境可以考慮採用:

  • 避免多次請求來回浪費掉的round-trip network latency
  • 創造出 Redis 與 Redis Framework 沒有支援的command
  • Atomic的資料操作 與 Transaction

Redis Lua Script Development and Debugging on macOS

這邊開始介紹如何在 macOS 的環境開發 Redis Lua Script

大致上的內容請先參考分享如何使用 ZeroBrane Studio 協助 Redis 的 Lua Script 開發與除錯

Redis with Docker

環境使用 Docker 進行 Redis Instance 的建置,指令如下:

docker pull Redis
docker run -P --name redis-lab -d redis

透過 -P 參數,這裡進行動態的 port 配置與對應至 container 內的 6379 port

從 docker ps -a 可以查到當前配置的 port 為 32768

01.png

透過以下指令與 redis-cli 取得當前的所有鍵值

docker exec -it <container-id> bash
redis-cli
KEYS *

02.png

Install ZeroBrane Studio

至官網下載ZeroBrane Studio 與 ZeroBranePackage/redis.lua 這個 plugin

安裝好後可配置使用者設定,載入剛下載的 plugin 至 ZeroBrane Studio 中:

mkdir $HOME/.zbstudio
mkdir $HOME/.zbstudio/packages

接者將下載的 redis.lua plugin 檔案放入剛剛建立的目錄下($HOME/.zbstudio/packages)

接著將程序打開後可以看到下圖及代表設定成功:

03.png

而開發時不要忘記要開啟 watch window 與 stack window 協助觀察變數的變化

04.png

Try round for ZeroBrane Studio debugging

這邊開啟應用程式後選擇 redis 則會跳出連線設定視窗,如果要重設則需要重新啟動 ZeroBrane Studio 才可以變更.

05.png

而如果開發的 Lua Script 本身有帶入參數的需求可以使用 Command Line Parameters 的設定帶入(多個參數可用空白分隔)

06.png

如需求所列,這邊我們建立的 Lua Script 如下:

test.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
local targetKey = KEYS[1] -- target key for redis
local initValue = tonumber(ARGV[1]) -- initial value for target key if not exist and need to create
local incrementValue = tonumber(ARGV[2]) -- increment value for target key when exist and calling for each time

local currentValue = initValue

-- create key with 0 if key is not exist
local isNewKey = redis.call('SETNX',targetKey,currentValue)

-- add current value if key is exist
if isNewKey == initValue then

currentValue = redis.call('GET',targetKey)

if currentValue then

currentValue = currentValue + incrementValue

end

redis.call('SET',targetKey, currentValue)

end

-- return key value
return currentValue

這邊就需要傳入三個參數,所以設定上則變成:

07.png

test 為 Key,透過中間的逗號分隔了後面兩個 Arg ,分別為 0 與 10

而介面上簡單介紹如下(詳細介紹可參考如何使用 ZeroBrane Studio 協助 Redis 的 Lua Script 開發與除錯):

08.png

.NET Core with Redis Lua Script

這邊最後給一段 Sample Code 說明如何透過預先載入的方式避免執行重複的 Redis Lua Script 所造成的內存耗盡問題.

其實 Redis 本身就有這個做法,可透過 SCRIPT LOAD 產生一組 SHA 的編碼後,透過 SHA 與 EVALSHA 來執行.

所以這邊的 C# 與上面已經寫好的 test.lua 的呼叫如下:

program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using StackExchange.Redis;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var redisHost = "localhost:32768";

Console.WriteLine("App Start...");

using (var conn = ConnectionMultiplexer.Connect(redisHost))
{
var db = conn.GetDatabase();
Console.WriteLine($"Current Value:{db.StringGet("test").ToString()}");

var loadedLuaScripts = new Dictionary<LuaScriptEnum, LoadedLuaScript>();
loadedLuaScripts.Add(LuaScriptEnum.AddValueWithTargetKey, PrepareLuaScript(conn, redisHost,
@"local targetKey = KEYS[1] -- target key for redis
local initValue = tonumber(ARGV[1]) -- initial value for target key if not exist and need to create
local incrementValue = tonumber(ARGV[2]) -- increment value for target key when exist and calling for each time

local currentValue = initValue

-- create key with 0 if key is not exist
local isNewKey = redis.call('SETNX',targetKey,currentValue)

-- add current value if key is exist
if isNewKey == initValue then

currentValue = redis.call('GET',targetKey)

if currentValue then

currentValue = currentValue + incrementValue

end

redis.call('SET',targetKey, currentValue)

end

-- return key value
return currentValue"));

var executedReturn = db.ScriptEvaluate(loadedLuaScripts[LuaScriptEnum.AddValueWithTargetKey].Hash,
new RedisKey[] {"test"},
new RedisValue[] {0, 50});
Console.WriteLine(executedReturn);
}
}

private static LoadedLuaScript PrepareLuaScript(ConnectionMultiplexer redis, string defaultServer, string luaScriptContent)
{
return LuaScript
.Prepare(luaScriptContent)
.Load(redis.GetServer(defaultServer));
}
}

public enum LuaScriptEnum
{
AddValueWithTargetKey
}
}

執行結果如下:

10.png

這段程式碼還有呼叫了Load(),這會把Lua script先載入指定的Redis server

一般指定 Master 就可以了,會自動鏡像備份到Slave。

載入後會拿到一個SHA1的 hash code,之後執行時只需傳入這個code,不需重傳整份Lua script,對需要頻繁執行的script有效能上的幫助。