Redis入門(6) - Lua腳本

  • 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

操作符

  1. 數(shù)學(xué)操作符,包括常見的+ - * \ %(取模) -(一元操作符扎谎,取負(fù))和冪運算符號^碳想。

  2. 比較操作符烧董,包括== ~=(不等于) > < >= <=。
    比較操作符不會對兩邊的操作數(shù)進(jìn)行自動類型轉(zhuǎn)換:

pring(1=='1') --結(jié)果為false
print({'a'}=={'a'}) -false,表類型比較的是二者的引用
  1. 邏輯操作符
    包括下面三個:
    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é)果為''。

  1. 連接操作符
    Lua中的連接操作符為'..'涤姊,用來連接兩個字符串。

  2. 取長度操作符

 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ù)庫修改都會丟失增拥!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市寻歧,隨后出現(xiàn)的幾起案子掌栅,更是在濱河造成了極大的恐慌,老刑警劉巖码泛,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猾封,死亡現(xiàn)場離奇詭異,居然都是意外死亡噪珊,警方通過查閱死者的電腦和手機晌缘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痢站,“玉大人磷箕,你說我怎么就攤上這事≌竽眩” “怎么了岳枷?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我嫩舟,道長氢烘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任家厌,我火速辦了婚禮播玖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘饭于。我一直安慰自己蜀踏,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布掰吕。 她就那樣靜靜地躺著果覆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪殖熟。 梳的紋絲不亂的頭發(fā)上局待,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音菱属,去河邊找鬼钳榨。 笑死,一個胖子當(dāng)著我的面吹牛纽门,可吹牛的內(nèi)容都是我干的薛耻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼赏陵,長吁一口氣:“原來是場噩夢啊……” “哼饼齿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蝙搔,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤缕溉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吃型,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體证鸥,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年败玉,在試婚紗的時候發(fā)現(xiàn)自己被綠了敌土。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片镜硕。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡运翼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出兴枯,到底是詐尸還是另有隱情血淌,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站悠夯,受9級特大地震影響癌淮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沦补,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一乳蓄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧夕膀,春花似錦虚倒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至易猫,卻和暖如春耻煤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背准颓。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工哈蝇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瞬场。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓买鸽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親贯被。 傳聞我的和親對象是個殘疾皇子眼五,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容