- Lua基本語法
- 表類型
- 函數(shù)
- Redis執(zhí)行腳本
- KEYS與ARGV
- 沙盒與隨機數(shù)
- 腳本相關(guān)命令
- 原子性和執(zhí)行時間
Lua是一種高效的輕量級腳本語言蔗崎,能夠方便地嵌入到其他語言中使用。在Redis中便贵,借助Lua腳本可以自定義擴展命令问顷。
Lua基本語法
數(shù)據(jù)類型
- 空(nil)昂秃,沒有賦值的變量或表的字段值都是nil
- 布爾(boolean)
- 數(shù)字(number),整數(shù)或浮點數(shù)
- 字符串(string),字符串可以用單引號或雙引號表示杜窄,可以包含轉(zhuǎn)義字符如\n \r等
- 表(table),表類型是Lua語言中唯一的數(shù)據(jù)結(jié)構(gòu)肠骆,既可以當(dāng)數(shù)組又可以當(dāng)字典,十分靈活
- 函數(shù)(function)塞耕,函數(shù)在Lua中是一等值(first-class-value)蚀腿,可以存儲在變量中、作為函數(shù)的參數(shù)或返回結(jié)果扫外。
變量
Lua的變量分為全局變量和局部變量莉钙,全局變量無需聲明就可以直接使用,默認(rèn)值是nil筛谚。
全局變量:
a=1 -- 為全局變量a賦值
print(b) -- 無需聲明即可使用胆胰,默認(rèn)值是nil
局部變量:
local c -- 聲明一個局部變量c,默認(rèn)值是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
操作符
數(shù)學(xué)操作符,包括常見的+ - * \ %(取模) -(一元操作符扎谎,取負(fù))和冪運算符號^碳想。
比較操作符烧董,包括== ~=(不等于) > < >= <=。
比較操作符不會對兩邊的操作數(shù)進(jìn)行自動類型轉(zhuǎn)換:
pring(1=='1') --結(jié)果為false
print({'a'}=={'a'}) -false,表類型比較的是二者的引用
- 邏輯操作符
包括下面三個:
not胧奔,根據(jù)操作數(shù)的真和假相應(yīng)地返回false和true逊移;
and,a and b中如果a是真則返回b龙填,否則返回a胳泉;
or,a or b中岩遗,如果a是真則返回a扇商,否則返回b。
這些根據(jù)操作符短路的原理可以推斷出宿礁。
print(1 and 5) --5
print(1 or 5) --1
print(not 0) --false
print('' or 1) --''
只要操作數(shù)不是nil或false案铺,邏輯操作符就認(rèn)為操作數(shù)是真,否則是假梆靖。而且即使是0或空字符串也被當(dāng)作真红且,所以上面的代碼中print(not 0)的結(jié)果為false,print('' or 1)的結(jié)果為''。
連接操作符
Lua中的連接操作符為'..'涤姊,用來連接兩個字符串。取長度操作符
print(#'hello') --5
if語句
Lua中if語句的格式為
if condition then
...
else if condition then
...
else
...
end
由于Lua中只有nil和false才認(rèn)為是假嗤放,這里也需要注意避坑思喊,比如Redis中EXISTS命令返回1和0分別表示存在或不存在,類似下面的寫法if條件將始終為true:
if redis.call('EXISTS','key1') then
...
所以需要寫成:
if redis.call('EXISTS','key1')==1 then
...
循環(huán)語句
Lua中的循環(huán)語句有四種形式:
while condition do
...
end
repeat
...
until condition
for i=初值, 終值, 步長 do
...
end
其中步長為1時可以省略。
for 變量1,變量2,...,變量N in 迭代器 do
...
end
表類型
表是Lua中唯一的數(shù)據(jù)結(jié)構(gòu)次酌,可以理解為關(guān)聯(lián)數(shù)組恨课,除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)
當(dāng)索引為整數(shù)的時候表和傳統(tǒng)的數(shù)組一樣岳服,但需要注意的是Lua的索引是從1開始的剂公。
a={}
a[1]='bob'
a[2]='daffy'
上面的定義和賦值的過程可以直接簡化為:
a={'bob','daffy'}
取值:
print(a[1])
表的遍歷
之前介紹的這種類型的for循環(huán)可以用于表的遍歷:
for 變量1,變量2,...,變量N in 迭代器 do
...
end
a={'bob','daffy'}
for index,value in ipairs(a) do
print(index)
print(value)
end
ipairs用于數(shù)組的遍歷,index和value分別為元素的索引和值吊宋,變量名不是必須為index和value纲辽,可以自定義。
或者:
for i=1, #a do
print(i)
print(a[i])
end
通過#a可以去到數(shù)組a的長度璃搜。
對于非數(shù)組的遍歷拖吼,可以使用pairs
b={
name='bom',
age=7
}
for key,value in pairs(b) do
print(key)
print(value)
end
變量名不是必須為key和value,可以自定義这吻。
函數(shù)
函數(shù)的定義為:
function(參數(shù)列表)
...
end
實際使用中可以將其賦值給一個局部變量吊档,如:
local square=function(num)
return num * num
end
還可以簡化為:
local function square(num)
return num * num
end
如果實參的個數(shù)小于形參的個數(shù),則沒有匹配到的形參的值為nil唾糯;如果實參的個數(shù)大于形參的個數(shù)怠硼,則多出的實參會被忽略鬼贱。如果希望參數(shù)可變,可以用...表示形參香璃。
在腳本中調(diào)用Redis命令
在腳本中使用redis.call可以調(diào)用Redis命令
redis.call('SET','foo','bar')
redis.call的返回值就是Redis命令的執(zhí)行結(jié)果这难。針對Redis的不同返回類型,redis.call會將其轉(zhuǎn)換為對應(yīng)的Lua的數(shù)據(jù)類型增显,兩者的對應(yīng)關(guān)系為:
Redis返回類型 | Lua數(shù)據(jù)類型 |
---|---|
整數(shù)回復(fù) | 數(shù)字類型 |
字符串回復(fù) | 字符串類型 |
多行字符串回復(fù) | 表類型(數(shù)組形式) |
狀態(tài)回復(fù) | 表類型(只有一個ok字段存儲狀態(tài)信息) |
錯誤回復(fù) | 表類型(只有一個err字段存儲錯誤信息) |
Redis的nil回復(fù)會被轉(zhuǎn)換為false雁佳。
Lua腳本執(zhí)行完畢后可以通過return將結(jié)果返回給Redis客戶端,這是又會將Lua的數(shù)據(jù)類型轉(zhuǎn)換為Redis的返回類型同云,過程與上面的表格相反糖权。
redis.pcall函數(shù)與redis.call的功能相同,但redis.pcall在執(zhí)行出錯時會記錄錯誤并繼續(xù)執(zhí)行炸站,而redis.call則會中斷執(zhí)行星澳。
Redis執(zhí)行腳本
EVAL
在Redis客戶端通過EVAL命令可以調(diào)用腳本,其格式為:
EVAL 腳本內(nèi)容 key參數(shù)的數(shù)量 [key...] [arg...]
例如用腳本來設(shè)置鍵的值旱易,就是這樣的:
EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1 foo bar
通過key和arg這兩類參數(shù)向腳本傳遞數(shù)據(jù)禁偎,它們的值可以在腳本中分別使用KEYS和ARGV兩個表類型的全局變量訪問。key參數(shù)的數(shù)量是必須指定的阀坏,沒有key參數(shù)時必須設(shè)為0如暖,EVAL會依據(jù)這個數(shù)值將傳入的參數(shù)分別存入KEYS和ARGV兩個表類型的全局變量。
EVALSHA
如果腳本比較長忌堂,每次調(diào)用腳本都將整個腳本傳給Redis會占用較多的帶寬盒至。而使用EVALSHA命令可以腳本內(nèi)容的SHA1摘要來執(zhí)行腳本,該命令的用法和EVAL一樣士修,只不過是將腳本內(nèi)容替換成腳本內(nèi)容的SHA1摘要枷遂。Redis在執(zhí)行EVAL命令時會計算腳本的SHA1摘要并記錄在腳本緩存中,執(zhí)行EVALSHA命令時Redis會根據(jù)提供的摘要從腳本緩存中查找對應(yīng)的腳本內(nèi)容棋嘲,如果找到了則執(zhí)行腳本酒唉,否則會返回錯誤:“NOSCRIPT No matching script. Please use EVAL.”。
具體使用時沸移,可以先計算腳本的SHA1摘要痪伦,并用EVALSHA命令執(zhí)行腳本,如果返回NOSCRIPT錯誤雹锣,就用EVAL重新執(zhí)行腳本流妻。
KEYS與ARGV
前面提到過向腳本傳遞的參數(shù)分為KEYS和ARGV兩類,前者表示要操作的鍵名笆制,后者表示非鍵名參數(shù)绅这。但這一要求并不輸強制的,比如設(shè)置鍵值的腳本:
EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1 foo bar
也可以寫成:
EVAL "return redis.call('SET',ARGV[1],ARGV[2])" 0 foo bar
雖然規(guī)則不是強制的在辆,但不遵守這樣的規(guī)則可能會為后續(xù)帶來不必要的麻煩证薇。比如Redis 3.0之后支持集群功能度苔,開啟集群后會將鍵發(fā)布到不同的節(jié)點上,所以在腳本執(zhí)行前就需要知道腳本會操作哪些鍵以便找到對應(yīng)的節(jié)點浑度,而如果腳本中的鍵名沒有使用KEYS參數(shù)傳遞則無法兼容集群寇窑。
沙盒與隨機數(shù)
Redis限制腳本只能在沙盒中運行,只允許腳本對Redis的數(shù)據(jù)進(jìn)行處理箩张,而禁止使用Lua標(biāo)準(zhǔn)庫中與文件或系統(tǒng)調(diào)用相關(guān)的函數(shù)甩骏,Redis還通過禁用腳本的全局變量的方式保證每個腳本都是相對隔離、不會互相干擾的先慷。
使用沙盒一方面可保證服務(wù)器的安全性饮笛,還可確保可以重現(xiàn)(腳本執(zhí)行的結(jié)果只和腳本本身以及傳遞的參數(shù)有關(guān))论熙。
Redis還替換了math.random和math.randomseed函數(shù)福青,使得每次執(zhí)行腳本時生成的隨機數(shù)列都相同。如果希望獲得不同的隨機數(shù)序列脓诡,可以采用提前生成隨機數(shù)并通過參數(shù)傳遞給腳本无午,或者提前生成隨機數(shù)種子的方式。
集合類型和散列類型的字段是無序的祝谚,所以SMEMBERS和HKEYS命令原本會返回隨機結(jié)果宪迟,但在腳本中調(diào)用這些命令時,Redis會對結(jié)果按照字典順序排序交惯。
對于會產(chǎn)生隨機結(jié)果但無法排序的命令次泽,比如SPOP,SRANDMEMBER, RANDOMKEY, TIME商玫,Redis會在這類命令執(zhí)行后將該腳本狀態(tài)標(biāo)記為lua_random_dirty,此后只允許調(diào)用只讀命令牡借,不允許修改數(shù)據(jù)庫的值拳昌,否則會返回錯誤:“Write commands not allowed after non deterministic commands.”
腳本相關(guān)命令
SCRIPT LOAD
EVAL命令會執(zhí)行腳本,并將腳本計算SHA1钠龙、加入到腳本緩存中炬藤,如果只是希望緩存腳本而不執(zhí)行,就可以使用SCRIPT LOAD碴里,返回值是腳本的SHA1結(jié)果:
> SCRIPT LOAD "return redis.call('SET',KEYS[1],ARGV[1])"
"cf63a54c34e159e75e5a3fe4794bb2ea636ee005"
SCRIPT EXISTS
通過SHA1查詢某個腳本是否被緩存沈矿,可以查詢多個SHA1。參數(shù)必須是完整的SHA1咬腋,而不能像docker只輸前幾位羹膳。返回結(jié)果1表示存在。
SCRIPT FLUSH
Redis將腳本加入到緩存后會永久保留根竿,如果要清空緩存可以使用SCRIPT FLUSH陵像。
SCRIPT KILL
用于終止正在執(zhí)行的腳本
原子性和執(zhí)行時間
Redis的腳本執(zhí)行是原子的就珠,腳本執(zhí)行期間其他命令不會被執(zhí)行,必須等待上一個腳本執(zhí)行完成醒颖。
但為了防止某個腳本執(zhí)行時間過長導(dǎo)致Redis無法提供服務(wù)(比如陷入死循環(huán))妻怎,Redis提供了lua-time-limit參數(shù)限制腳本的最長運行時間,默認(rèn)為5秒鐘泞歉。當(dāng)腳本運行時間超過這一限制后逼侦,Redis將開始接受其他命令,但為了確保腳本的原子性腰耙,新的腳本仍然不會執(zhí)行榛丢,而是會返回“BUSY”錯誤。
可以打開兩個redis-cli實例A和B來驗證沟优,首先在A執(zhí)行一個死循環(huán)腳本:
EVAL "while true do end" 0
這時在實例B執(zhí)行GET key1會返回:
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
如果按照錯誤提示涕滋,在B執(zhí)行SCRIPT KILL,這時在實例A的腳本會被終止挠阁,并返回:
(error) ERR Error running script (call to f_694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7): @user_script:1: Script killed by user with SCRIPT KILL...
但如果A已經(jīng)對Redis的數(shù)據(jù)做了修改宾肺,則SCRIPT KILL無法將其終止,A執(zhí)行:
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命令的區(qū)別在于,SHUTDOWN NOSAVE將不會進(jìn)行持久化操作隘谣,所有發(fā)生在上一次快照后的數(shù)據(jù)庫修改都會丟失增拥!