Lua基本語法 表類型 函數 Redis執行腳本 KEYS與ARGV 沙盒與隨機數 腳本相關命令 原子性和執行時間 Lua是一種高效的輕量級腳本語言,能夠方便地嵌入到其他語言中使用。在Redis中,藉助Lua腳本可以自定義擴展命令。 Lua基本語法 數據類型 空(nil),沒有賦值的變數或表的欄位值 ...
- Lua基本語法
- 表類型
- 函數
- Redis執行腳本
- KEYS與ARGV
- 沙盒與隨機數
- 腳本相關命令
- 原子性和執行時間
Lua是一種高效的輕量級腳本語言,能夠方便地嵌入到其他語言中使用。在Redis中,藉助Lua腳本可以自定義擴展命令。
Lua基本語法
數據類型
- 空(nil),沒有賦值的變數或表的欄位值都是nil
- 布爾(boolean)
- 數字(number),整數或浮點數
- 字元串(string),字元串可以用單引號或雙引號表示,可以包含轉義字元如\n \r等
- 表(table),表類型是Lua語言中唯一的數據結構,既可以當數組又可以當字典,十分靈活
- 函數(function),函數在Lua中是一等值(first-class-value),可以存儲在變數中、作為函數的參數或返回結果。
變數
Lua的變數分為全局變數和局部變數,全局變數無需聲明就可以直接使用,預設值是nil。
全局變數:
a=1 -- 為全局變數a賦值
print(b) -- 無需聲明即可使用,預設值是nil
局部變數:
local c -- 聲明一個局部變數c,預設值是nil
local d=1 -- 聲明一個局部變數d並賦值為1
local e,f -- 可以同時聲明多個局部變數
但在Redis中,為了防止腳本之間相互影響,只允許使用局部變數。
賦值
Lua支持多重賦值,如:
local a,b=1,2 --a的值是1,b的值是2
local c,d=1,2,3 --c的值是1,d的值是2,3被捨棄了
local e,f =1 --e的值是1,f的值是nil
操作符
-
數學操作符,包括常見的+ - * \ %(取模) -(一元操作符,取負)和冪運算符號^。
-
比較操作符,包括== ~=(不等於) > < >= <=。
比較操作符不會對兩邊的操作數進行自動類型轉換:
pring(1=='1') --結果為false
print({'a'}=={'a'}) -false,表類型比較的是二者的引用
- 邏輯操作符
包括下麵三個:
not,根據操作數的真和假相應地返回false和true;
and,a and b中如果a是真則返回b,否則返回a;
or,a or b中,如果a是真則返回a,否則返回b。
這些根據操作符短路的原理可以推斷出。
print(1 and 5) --5
print(1 or 5) --1
print(not 0) --false
print('' or 1) --''
只要操作數不是nil或false,邏輯操作符就認為操作數是真,否則是假。而且即使是0或空字元串也被當作真,所以上面的代碼中print(not 0)的結果為false,print('' or 1)的結果為''。
-
連接操作符
Lua中的連接操作符為'..',用來連接兩個字元串。 -
取長度操作符
print(#'hello') --5
if語句
Lua中if語句的格式為
if condition then
...
else if condition then
...
else
...
end
由於Lua中只有nil和false才認為是假,這裡也需要註意避坑,比如Redis中EXISTS命令返回1和0分別表示存在或不存在,類似下麵的寫法if條件將始終為true:
if redis.call('EXISTS','key1') then
...
所以需要寫成:
if redis.call('EXISTS','key1')==1 then
...
迴圈語句
Lua中的迴圈語句有四種形式:
while condition do
...
end
repeat
...
until condition
for i=初值, 終值, 步長 do
...
end
其中步長為1時可以省略。
for 變數1,變數2,...,變數N in 迭代器 do
...
end
表類型
表是Lua中唯一的數據結構,可以理解為關聯數組,除nil之外的任何類型的值都可以作為表的索引。
表的定義和賦值
-- 表的定義
a={} --將變數a賦值為一個空表
-- 表的賦值
a['field']='value' --將field欄位賦值為value
print(a.field) --a['field']可以簡化為a.field
-- 定義的同時賦值
b={
name='bom',
age=7
}
-- 取值
print(b['age'])
print(b.age)
當索引為整數的時候表和傳統的數組一樣,但需要註意的是Lua的索引是從1開始的。
a={}
a[1]='bob'
a[2]='daffy'
上面的定義和賦值的過程可以直接簡化為:
a={'bob','daffy'}
取值:
print(a[1])
表的遍歷
之前介紹的這種類型的for迴圈可以用於表的遍歷:
for 變數1,變數2,...,變數N in 迭代器 do
...
end
a={'bob','daffy'}
for index,value in ipairs(a) do
print(index)
print(value)
end
ipairs用於數組的遍歷,index和value分別為元素的索引和值,變數名不是必須為index和value,可以自定義。
或者:
for i=1, #a do
print(i)
print(a[i])
end
通過#a可以去到數組a的長度。
對於非數組的遍歷,可以使用pairs
b={
name='bom',
age=7
}
for key,value in pairs(b) do
print(key)
print(value)
end
變數名不是必須為key和value,可以自定義。
函數
函數的定義為:
function(參數列表)
...
end
實際使用中可以將其賦值給一個局部變數,如:
local square=function(num)
return num * num
end
還可以簡化為:
local function square(num)
return num * num
end
如果實參的個數小於形參的個數,則沒有匹配到的形參的值為nil;如果實參的個數大於形參的個數,則多出的實參會被忽略。如果希望參數可變,可以用...表示形參。
在腳本中調用Redis命令
在腳本中使用redis.call可以調用Redis命令
redis.call('SET','foo','bar')
redis.call的返回值就是Redis命令的執行結果。針對Redis的不同返回類型,redis.call會將其轉換為對應的Lua的數據類型,兩者的對應關係為:
Redis返回類型 | Lua數據類型 |
---|---|
整數回覆 | 數字類型 |
字元串回覆 | 字元串類型 |
多行字元串回覆 | 表類型(數組形式) |
狀態回覆 | 表類型(只有一個ok欄位存儲狀態信息) |
錯誤回覆 | 表類型(只有一個err欄位存儲錯誤信息) |
Redis的nil回覆會被轉換為false。
Lua腳本執行完畢後可以通過return將結果返回給Redis客戶端,這是又會將Lua的數據類型轉換為Redis的返回類型,過程與上面的表格相反。
redis.pcall函數與redis.call的功能相同,但redis.pcall在執行出錯時會記錄錯誤並繼續執行,而redis.call則會中斷執行。
Redis執行腳本
EVAL
在Redis客戶端通過EVAL命令可以調用腳本,其格式為:
EVAL 腳本內容 key參數的數量 [key...] [arg...]
例如用腳本來設置鍵的值,就是這樣的:
EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1 foo bar
通過key和arg這兩類參數向腳本傳遞數據,它們的值可以在腳本中分別使用KEYS和ARGV兩個表類型的全局變數訪問。key參數的數量是必須指定的,沒有key參數時必須設為0,EVAL會依據這個數值將傳入的參數分別存入KEYS和ARGV兩個表類型的全局變數。
EVALSHA
如果腳本比較長,每次調用腳本都將整個腳本傳給Redis會占用較多的帶寬。而使用EVALSHA命令可以腳本內容的SHA1摘要來執行腳本,該命令的用法和EVAL一樣,只不過是將腳本內容替換成腳本內容的SHA1摘要。Redis在執行EVAL命令時會計算腳本的SHA1摘要並記錄在腳本緩存中,執行EVALSHA命令時Redis會根據提供的摘要從腳本緩存中查找對應的腳本內容,如果找到了則執行腳本,否則會返回錯誤:“NOSCRIPT No matching script. Please use EVAL.”。
具體使用時,可以先計算腳本的SHA1摘要,並用EVALSHA命令執行腳本,如果返回NOSCRIPT錯誤,就用EVAL重新執行腳本。
KEYS與ARGV
前面提到過向腳本傳遞的參數分為KEYS和ARGV兩類,前者表示要操作的鍵名,後者表示非鍵名參數。但這一要求並不輸強制的,比如設置鍵值的腳本:
EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1 foo bar
也可以寫成:
EVAL "return redis.call('SET',ARGV[1],ARGV[2])" 0 foo bar
雖然規則不是強制的,但不遵守這樣的規則可能會為後續帶來不必要的麻煩。比如Redis 3.0之後支持集群功能,開啟集群後會將鍵發佈到不同的節點上,所以在腳本執行前就需要知道腳本會操作哪些鍵以便找到對應的節點,而如果腳本中的鍵名沒有使用KEYS參數傳遞則無法相容集群。
沙盒與隨機數
Redis限制腳本只能在沙盒中運行,只允許腳本對Redis的數據進行處理,而禁止使用Lua標準庫中與文件或系統調用相關的函數,Redis還通過禁用腳本的全局變數的方式保證每個腳本都是相對隔離、不會互相干擾的。
使用沙盒一方面可保證伺服器的安全性,還可確保可以重現(腳本執行的結果只和腳本本身以及傳遞的參數有關)。
Redis還替換了math.random和math.randomseed函數,使得每次執行腳本時生成的隨機數列都相同。如果希望獲得不同的隨機數序列,可以採用提前生成隨機數並通過參數傳遞給腳本,或者提前生成隨機數種子的方式。
集合類型和散列類型的欄位是無序的,所以SMEMBERS和HKEYS命令原本會返回隨機結果,但在腳本中調用這些命令時,Redis會對結果按照字典順序排序。
對於會產生隨機結果但無法排序的命令,比如SPOP,SRANDMEMBER, RANDOMKEY, TIME,Redis會在這類命令執行後將該腳本狀態標記為lua_random_dirty,此後只允許調用只讀命令,不允許修改資料庫的值,否則會返回錯誤:“Write commands not allowed after non deterministic commands.”
腳本相關命令
SCRIPT LOAD
EVAL命令會執行腳本,並將腳本計算SHA1、加入到腳本緩存中,如果只是希望緩存腳本而不執行,就可以使用SCRIPT LOAD,返回值是腳本的SHA1結果:
> SCRIPT LOAD "return redis.call('SET',KEYS[1],ARGV[1])"
"cf63a54c34e159e75e5a3fe4794bb2ea636ee005"
SCRIPT EXISTS
通過SHA1查詢某個腳本是否被緩存,可以查詢多個SHA1。參數必須是完整的SHA1,而不能像docker只輸前幾位。返回結果1表示存在。
SCRIPT FLUSH
Redis將腳本加入到緩存後會永久保留,如果要清空緩存可以使用SCRIPT FLUSH。
SCRIPT KILL
用於終止正在執行的腳本
原子性和執行時間
Redis的腳本執行是原子的,腳本執行期間其他命令不會被執行,必須等待上一個腳本執行完成。
但為了防止某個腳本執行時間過長導致Redis無法提供服務(比如陷入死迴圈),Redis提供了lua-time-limit參數限制腳本的最長運行時間,預設為5秒鐘。當腳本運行時間超過這一限制後,Redis將開始接受其他命令,但為了確保腳本的原子性,新的腳本仍然不會執行,而是會返回“BUSY”錯誤。
可以打開兩個redis-cli實例A和B來驗證,首先在A執行一個死迴圈腳本:
EVAL "while true do end" 0
這時在實例B執行GET key1會返回:
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
如果按照錯誤提示,在B執行SCRIPT KILL,這時在實例A的腳本會被終止,並返回:
(error) ERR Error running script (call to f_694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7): @user_script:1: Script killed by user with SCRIPT KILL...
但如果A已經對Redis的數據做了修改,則SCRIPT KILL無法將其終止,A執行:
EVAL "redis.call('SET','foo','bar') while true do end" 0
如果在B嘗試KILL腳本,會返回錯誤:
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.
這時就只能通過SHUTDOWN NOSAVE命令強行終止Redis。SHUTDOWN NOSAVE與SHUTDOWN命令的區別在於,SHUTDOWN NOSAVE將不會進行持久化操作,所有發生在上一次快照後的資料庫修改都會丟失!