.Net Core使用分散式快取Redis:Lua指令碼
一、前言
執行環境window,redis版本3.2.1。此處暫不對Lua進行詳細講解,只從Redis的方面講解。
二、Redis的Lua指令碼
在Redis的2.6版本推出了指令碼功能,允許開發者使用Lua語言編寫指令碼傳到Redis中執行,在Lua指令碼中也可以呼叫大部分的Redis命令。使用指令碼有以下三個好處:
(1) 減少網路開銷:有些時候需要多次請求Redis獲取處理資料,而使用指令碼功能就可以只使用一次請求完成相同操作,減少了網路往返時延。
(2) 原子操作:Redis會將整個指令碼作為一個整體執行,中間不會被其他命令插入。也就是說在編寫指令碼的過程中無須擔心會出現競態條件,也就是無須使用事務。事務可以完成的所有功能,都可以用指令碼來完成。
(3) 複用:客戶端傳送的指令碼會永久儲存在Redis中,這就意味著其他客戶端(可以是其他語言開發的專案)可以複用這一指令碼而不需要使用程式碼完成同樣的邏輯。
三、Redis呼叫Lua
1、EVAL命令
編寫完指令碼後最重要的就是在程式中執行指令碼。Redis提供了EVAL命令可以使開發者像呼叫其他Redis內建命令一樣呼叫指令碼。EVAL的命令格式如下:
127.0.0.1:6379> eval script numkeys key [key ...] arg [arg ...]
script:指令碼內容。numkeys:key引數的數量。key和arg:這兩個引數向指令碼傳遞資料,它們的值可以在指令碼中分別使用KEYS[index]和ARGV[index]兩個表型別的全域性變數訪問,numkeys為key的數量和其索引的最大值,argv的索引為key和argv數量總和減去numkeys,它們的索引都是從1開始,超出則返回nil。如下:
C:\Users\Xu>redis-cli 127.0.0.1:6379> eval 'return ARGV[3]' 2 key1 key2 value1 value2 value3 "value3" 127.0.0.1:6379> eval 'return KEYS[2]' 2 key1 key2 value1 value2 value3 "key2" 127.0.0.1:6379> eval 'return KEYS[3]' 2 key1 key2 value1 value2 value3 (nil)
其中要讀寫的鍵名應該為key引數,其他資料都作為arg引數。
除了上面直接寫lua指令碼,還可以讀取lua指令碼檔案來執行指令碼,命令如下:
C:\Users\Xu>redis-cli --eval lua_file_path key1 key2 , arg1 arg2 arg3
注意不需要numkeys,逗號前後必須有空格,否則會被認為一個連起來的字串。
//lua檔案內容 return ARGV[2] //執行命令 C:\Users\Xu>redis-cli.exe --eval e:\redis\a.lua key1 , value1 value2 "value2" C:\Users\Xu>redis-cli.exe --eval e:\redis\a.lua key1 , value1 value2,value3 "value2,value3"
2、EVALSHA命令
考慮到在指令碼比較長的時候,如果每次呼叫指令碼都需要將整個指令碼傳給Redis會佔用較多的頻寬。所以,Redis提供了EVALSHA命令允許開發者通過指令碼內容的SHA1摘要來執行指令碼,該命令的用法和EVAL一樣,不過就是將指令碼內容的script替換為它的SHA1摘要。
Redis在執行EVAL命令時會計算指令碼的SHA1摘要並記錄在指令碼快取中,如果執行EVALSHA命令時沒有從指令碼快取中找到相應的摘要,則返回錯誤。
127.0.0.1:6379> evalsha c349a436bd639369c62c971941fc5f7a80626ee6 1 key1 value1 (integer) 666 127.0.0.1:6379> evalsha c349a436bd639369c62c971941fc5f7a80626ee61 1 key1 value1 (error) NOSCRIPT No matching script. Please use EVAL.
在程式中使用EVALSHA的流程如下:
(1) 先計算指令碼SHA1摘要,並使用EVALSHA執行。
(2) 獲得返回值,如果返回錯誤則使用EVAL重新執行指令碼。
3、SCRIPT LOAD命令
如果只是想將指令碼加入到指令碼快取中而不執行則則可以用SCRIPT LOAD命令,返回值時指令碼的SHA1摘要。
127.0.0.1:6379> script load 'return 666' "c349a436bd639369c62c971941fc5f7a80626ee6"
4、SCRIPT EXISTS命令
SCRIPT EXISTS命令可以同時查詢一個或者多個指令碼的SHA1摘要是否已經本快取,1為存在0為不存在。
127.0.0.1:6379> script exists c349a436bd639369c62c971941fc5f7a80626ee6 123ls436bd639369c62c971941fc5f7a80626ee6 1) (integer) 1 2) (integer) 0
5、SCRIPT FLUSH命令
Redis將指令碼的SHA1摘要加入到指令碼快取後會永久儲存,不會刪除,但是可以用SCRIPT FLUSH刪除所有指令碼快取。
127.0.0.1:6379> script flush OK (1.51s)
6、SCRIPT KILL 和 SHUTDOWN NOSAVE
由於Redis的指令碼是原子性的,指令碼執行期間不會執行其他命令。為了防止某個指令碼執行時間過長導致Redis無法提供服務(比如死迴圈),Redis提供了lua-time-limit引數限制指令碼最長執行時間,預設是5秒。再指令碼執行期間,執行其他命令會返回“BUSY”錯誤,如下:
(A)127.0.0.1:6379> eval 'while true do end' 0
(B)127.0.0.1:6379> get foo (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
此時Redis只會接受並執行兩個命令:SCRIPT KILL 和 SHUTDOWN NOSVAE。
通過SCRIPT KILL 可以終止當前指令碼的執行,指令碼停止並返回錯誤:
(B)127.0.0.1:6379> script kill OK (B)127.0.0.1:6379> get foo (nil) (A)127.0.0.1:6379> eval 'while true do end' 0 (error) ERR Error running script (call to f_694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7): @user_script:1: Script killed by user with SCRIPT KILL... (175.99s)
如果當前執行的指令碼對Redis的資料進行了修改,則SCRIPT KILL不會終止指令碼的執行,因為這樣違背了原子性。那麼需要通過SHUTDOWN NOSAVE來強制終止Redis將原先指令碼的修改操作返回,不進行持久化操作,這意味著所有傳送在上一次的快照後的資料庫修改都會丟失。
四、Redis獲取指令碼中的返回值
很多情況下,都需要指令碼通過return返回值,如果沒有執行return則預設返回nil。因為我們可以像呼叫其他Redis內建命令一樣呼叫我們自己寫的指令碼,所以同樣Redis會自動將指令碼返回值的Lua資料型別轉化成Redis的返回值型別。具體的轉換規則如下:
(1) Lua的數字型別,Redis為整數型別。
127.0.0.1:6379> eval 'return 1.1' 0 (integer) 1
(2) Lua的字串型別,Redis也是字串型別
(3) Lua的表型別(陣列形式),Redis會返回多行字串
127.0.0.1:6379> eval 'return {0,1}' 0 1) (integer) 0 2) (integer) 1
(4) Lua表型別(只有一個ok欄位儲存狀態資訊),Redis為成功狀態回覆
127.0.0.1:6379> eval 'return {ok="this is ok"}' 0 this is ok
(5)Lua表型別(只有一個err欄位儲存狀態資訊),Redis為錯誤狀態回覆
127.0.0.1:6379> eval 'return {err="so bad"}' 0 (error) so bad
(6)Lua的bool型別中true為Redis的1,false為nil
127.0.0.1:6379> eval 'return true' 0 (integer) 1 127.0.0.1:6379> eval 'return false' 0 (nil)
五、沙盒與隨機數
Redis指令碼禁止使用Lua標準庫中與檔案或系統呼叫相關的函式,在指令碼中只允許對Redis的資料進行處理。並且Redis還通過禁用指令碼的全域性變數的方式保證每個指令碼都是相對隔離的,不會互相干擾。
使用沙盒不僅是為了保證伺服器的安全性,而且還確保了指令碼的執行結果只有和指令碼本身和執行時傳遞的引數有關,不依賴外界條件(如系統時間、系統中某個檔案的內容、其他指令碼執行結果登)。這是因為在執行復制和AOF持久話操作時記錄的指令碼的內容而不是指令碼呼叫的命令,所以必須保證在指令碼內容和引數一樣的前提下指令碼的執行結果必須一樣。
對於隨機數,Redis替換了math.random和math。randomseed函式使得每次執行指令碼時生成的隨機數列都相同,如果希望獲得不同的隨機數序列,最簡單的方法時由程式生成隨機數並通過引數傳遞給指令碼,或者採用更靈活的方法,即在程式中生成隨機數傳給指令碼作為隨機數種子。
六、在net core中使用指令碼
很簡單,直接上程式碼,這裡舉例最基本的,還有很多的重寫方法大家可以自己試試。最簡單的使用eval。
var script = " return KEYS[1];"; var keys = new RedisKey[]{ "key1","key2"}; var values = new RedisValue[] { "value1", "value2" }; return await redisConnection.GetDatabase().ScriptEvaluateAsync(script, keys, values);
快取指令碼,並使用。
var bytes = await redisConnection.GetServer(Config.Get("ConnectionStrings:Redis:ConnectionString")).ScriptLoadAsync("return 1"); var result = await redisConnection.GetDatabase().ScriptEvaluateAsync(bytes, null, null);
指令碼是否已快取。
bool exist = await redisConnection.GetServer(Config.Get("ConnectionStrings:Redis:ConnectionString")).ScriptExistsAsync("return 1");
刪除所有指令碼快取,這個操作需要連線的ConfigurationOptions配置中AllowAdmin = true,沒有會報錯哦。
redisConnection.GetServer(Config.Get("ConnectionStrings:Redis:ConnectionString")).ScriptFlush();
還有LuaScript和LoadedLuaScript兩個類可以對指令碼進行更多複雜的指令碼,LuaScript將@myVar形式的指令碼中的變數重寫為redis所需的合適的ARGV[someIndex]。如果傳遞的引數是RedisKey型別,它將作為KEYS集合的一部分自動傳送。如下。
var lua = LuaScript.Prepare("return @key"); var result = redisConnection.GetDatabase().ScriptEvaluate(lua,new {key= (RedisKey)"key1",value = "value1" });