之前文章中提到過,redis官方在考慮lua代替事務,與事務相比,lua腳本也具有原子性,并且lua腳本在執行的時候,也會阻塞其他腳本或者redis指令并發執行,是以要盡量避免執行時間很長的腳本。
另外還要注意之前提到過的lua腳本導緻記憶體洩漏問題,如果一組指令中的第一個就導緻記憶體超出,那麼這段lua腳本不會運作,但是第二個及之後的指令導緻記憶體超出,redis是不會報錯停止的,它會将整段腳本執行完(這也是其原子性的展現)
在Lua腳本執行期間,不執行任何 key 過期操作。當一個Lua腳本運作時,從概念上講,master 中的時間是被當機的,這樣腳本運作的時候,一個給定的鍵要麼存在要麼不存在。這可以防止 key 在腳本中間過期,保證将相同的腳本發送到 slave ,進而在二者的資料集中産生相同的效果。
與事務隻在所有語句執行完才能看到傳回資訊不同,lua可以在運作途中輸出執行結果,好根據此結果判斷接下來要幹啥(if),還支援用戶端緩存lua語句,這可以節省帶寬,加快執行速度。
lua腳本可以嵌入多種語言,體積小巧,在php中可以作為一個擴充安裝運作:https://www.php.net/manual/zh/book.lua.php (使用者筆記0和1介紹php7以上版本安裝lua2.0.7) https://blog.csdn.net/weixin_29504987/article/details/115501223
純函數
腳本應該被寫成純函數,對于同樣的資料集輸入,給定相同的參數,腳本執行的 redis 寫指令總是相同的。腳本執行的操作不能依賴于任何隐藏(非顯式)資料,不能依賴于腳本在執行過程中、或腳本在不同執行時期之間可能變更的狀态,并且它也不能依賴于任何來自 I/O 裝置的外部輸入。
- Lua 沒有通路系統時間或者其他内部狀态的指令。
- 在執行随機指令之後(比如randomkey、srandmember key [count]、time),接着執行可以修改資料集的 Redis 指令,Redis 會傳回一個錯誤。如果腳本隻是執行隻讀操作,那麼就沒有這一限制。注意,随機指令并不一定就指那些帶 rand 字眼的指令,任何帶有非确定性的指令都會被認為是随機指令,比如time。
- 對無序傳回預設(slient)進行字典排序。比如在 Redis 指令行用戶端中直接執行smembers key,傳回的元素是無序的,但是,假如在腳本中執行
,那麼傳回的總是排過序的元素。redis.call("smembers", KEYS[1])
- 對 Lua 的僞随機數生成函數
和math.random
進行修改,每次運作腳本時,math.randomseed
産生的随機數序列總是相同的。(math.random
可以為腳本添加一個額外的參數,讓這個參數作為 Lua 的随機數生成器的 seed 值,這樣的話,隻要給腳本傳入不同的 seed ,腳本就會生成不同的清單元素。(redis 實作保證math.randomseed
和math.random
的輸出和運作 Redis 的系統架構無關,無論是 32 位還是 64 位系統,無論是小端(little endian)還是大端(big endian)系統,這兩個函數的輸出總是相同的)。math.randomseed
全局變量保護
為了防止不必要的資料洩漏進 Lua 環境, redis 腳本不允許建立全局變量。如果一個腳本需要在多次執行之間維持某種狀态,它應該使用 redis key 來進行狀态儲存。
一旦使用者在腳本中混入了 Lua 全局狀态,那麼 AOF 持久化和複制(replication)都會無法保證,可以将腳本中用到的所有變量都使用
local
關鍵字定義為局部變量。
例1:
運作
eval、evalsha
lua腳本運作主要有兩種方式,eval和evalsha。
eval:直接運作lua腳本
evalsha:根據給定的sha1校驗碼,對緩存在伺服器中的腳本進行求值。redis包含一組将腳本寫入緩存、清除、判斷是否存在的script指令,也包含殺死正在運作腳本的指令。
雖然redis不會每次重新編譯腳本,但是用evalsha可以減少腳本資料傳遞,減輕帶寬壓力。
redis.call()、redis.pcall()
執行redis指令的函數有兩種:
redis.call():在運作中出錯時停止運作并傳回錯誤資訊
redis.pcall():出錯時并不引發(raise)錯誤,而是傳回一個帶 err 域的 Lua 表(table),用于表示錯誤
可以在指令行直接運作簡單腳本:
1是指一個鍵名參數KEYS,之後的是附加參數ARGV。
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 name ronaldo
可以執行lua腳本檔案
例2:test.lua
--[[
多行注釋
--]]
local mykey = KEYS[1]
local mykey2 = KEYS[2]
local myvalue = ARGV[1]
local myvalue2 = ARGV[2]
return {mykey,mykey2,myvalue,myvalue2}
執行:因為本地運作,redis-cli --eval 腳本路徑/test.lua -h -p -a都是預設可以省略。之後的參數中,‘,’之前的叫KEY,可以由KEY[1],KEY[2]...擷取,‘,’之後的叫ARGV,由ARGV[1]...擷取。注意‘,’要前後有空格
隻有一個return,需要傳回多個值需要包含在{}中(這實際上是lua的table資料類型,是一個關聯數組),可以自定義格式,例2中的傳回可以改為
return {mykey,{myvalue},mykey2,{myvalue2}}
例3:
test.lua
redis.call('set','name',ARGV[1])
local myname = redis.call('get','name')
if myname == 'kaka' then
return 'just you'
else
return 'sorry,not you'
end
執行:
redis-cli --eval 腳本路徑/test.lua -h -p -a KEY[1] KEY[2]... , ARGV[1] ARGV[2] ...
本例中未用到KEY,‘,’前空格即可,直接寫AGRV參數
更多文法參考:https://www.runoob.com/lua/lua-basic-syntax.html 需要注意執行redis語句的lua腳本的限制
還要注意叢集分區問題
沙箱(sandbox)和最大執行時間
腳本應該僅僅用于傳遞參數和對 Redis 資料進行處理,它不應該嘗試去通路外部系統(比如檔案系統),或者執行任何系統調用。
除此之外,腳本還有一個最大執行時間限制,它的預設值是 5 秒鐘,一般正常運作的腳本通常可以在幾分之幾毫秒之内完成,這個限制主要是為了防止因程式設計錯誤而造成的無限循環而設定的。
當一個腳本達到最大執行時間的時候,為了保證其原子性,它并不會自動被 Redis 結束,當腳本逾時:
- Redis 記錄一個腳本正在逾時運作
- Redis 開始重新接受其他用戶端的指令請求,但是隻有script kill和shutdown nosave兩個指令會被處理,對于其他指令請求, Redis 伺服器隻是簡單地傳回
錯誤。BUSY
- 可以使用 script kill指令将一個僅執行隻讀指令的腳本殺死,因為不修改資料,是以殺死這個腳本并不破壞資料的完整性
- 如果腳本已經執行過寫指令,那麼唯一允許執行的操作就是shutdown nosave ,它通過停止伺服器來阻止目前資料集寫入磁盤
流水線(pipeline)上下文(context)中的evalsha
之前我們提到過,redis事務底層實際上是使用pipeline以提高性能。
在流水線中,必須保證指令的執行順序。一旦在流水線中因為 EVALSHA 指令而發生 NOSCRIPT 錯誤,那麼這個流水線就再也沒有辦法重新執行了,否則的話,指令的執行順序就會被打亂。
為了防止出現以上所說的問題,用戶端庫實作應該實施以下的其中一項措施:
- 總是在流水線中使用eval指令
- 檢查流水線中要用到的所有指令,找到其中的eval指令,并使用 SCRIPT EXISTS sha1 [sha1 …] 指令檢查要用到的腳本是不是全都已經儲存在緩存裡面了。如果所需的全部腳本都可以在緩存裡找到,那麼就可以放心地将所有 EVAL 指令改成 EVALSHA 指令,否則的話,就要在流水線的頂端(top)将缺少的腳本用 SCRIPT LOAD script 指令加上去。
lua文法入門
八大資料類型
nil
無效值、空值,在表達式中相當于false,對于全局變量和 table,nil 還有一個"删除"作用,給全局變量或者 table 表裡的變量賦一個 nil 值,等同于把它們删掉,周遊時可以發現nil值被删除了。
作比較時應加上“”:type(X)=="nil" true,type(X)==nil false。因為type()函數本身傳回的是一個string。
boolean
false 和 nil 看作是 false,其他的都為 true,數字0也是true。
local a = 0
if a then
return '0 is true'
else
return '0 is not true'
end
number
預設隻有double型
string
單雙引号引用,和用[[ ]]引用字元串塊
對一個數字字元串上進行算術操作時,Lua 會嘗試将這個數字字元串轉成一個數字
#string 計算string長度,傳回數字,中文根據編碼長度不同
table
table是一個"關聯數組",索引可以是數字或者是字元串,用它可以實作數組,哈希表,集合,對象。
預設初始索引一般以 1 開始(這就是之前例子中的KEY[1])
local a = {}
for i = 1,10 do
a[i] = i
end
return a
function(函數)
函數可以存在變量裡,函數也可以看做一個值。
通過閉包和table可以很友善地支援面向對象程式設計所需要的一些關鍵機制,比如資料抽象,虛函數,繼承和重載等
thread(線程)
在 Lua 裡,最主要的線程是協同程式(coroutine)。它跟線程(thread)差不多,擁有自己獨立的棧、局部變量和指令指針,可以跟其他協同程式共享全局變量和其他大部分東西。
線程跟協程的差別:線程可以同時多個運作,而協程任意時刻隻能運作一個,并且處于運作狀态的協程隻有被挂起(suspend)時才會暫停。
userdata(自定義類型)
userdata是使用者自定義類型,用于将應用、c、c++等語言建立的類型儲存在userdata裡,如指針等。
參考:
http://redisdoc.com/script/eval.html