一、前言
運行環境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" });