Lua是一種高效的輕量級腳本語言,可以方便地嵌入到其餘語言中使用。在Redis中,藉助Lua腳本能夠自定義擴展命令。redis
Lua的變量分爲全局變量和局部變量,全局變量無需聲明就能夠直接使用,默認值是nil。
全局變量:docker
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,表類型比較的是兩者的引用
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中的鏈接操做符爲'..',用來鏈接兩個字符串。dom
取長度操做符函數
print(#'hello') --5
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.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命令能夠調用腳本,其格式爲:
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兩個表類型的全局變量。
若是腳本比較長,每次調用腳本都將整個腳本傳給Redis會佔用較多的帶寬。而使用EVALSHA命令能夠腳本內容的SHA1摘要來執行腳本,該命令的用法和EVAL同樣,只不過是將腳本內容替換成腳本內容的SHA1摘要。Redis在執行EVAL命令時會計算腳本的SHA1摘要並記錄在腳本緩存中,執行EVALSHA命令時Redis會根據提供的摘要從腳本緩存中查找對應的腳本內容,若是找到了則執行腳本,不然會返回錯誤:「NOSCRIPT No matching script. Please use EVAL.」。
具體使用時,能夠先計算腳本的SHA1摘要,並用EVALSHA命令執行腳本,若是返回NOSCRIPT錯誤,就用EVAL從新執行腳本。
前面提到過向腳本傳遞的參數分爲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.」
EVAL命令會執行腳本,並將腳本計算SHA一、加入到腳本緩存中,若是隻是但願緩存腳本而不執行,就可使用SCRIPT LOAD,返回值是腳本的SHA1結果:
> SCRIPT LOAD "return redis.call('SET',KEYS[1],ARGV[1])" "cf63a54c34e159e75e5a3fe4794bb2ea636ee005"
經過SHA1查詢某個腳本是否被緩存,能夠查詢多個SHA1。參數必須是完整的SHA1,而不能像docker只輸前幾位。返回結果1表示存在。
Redis將腳本加入到緩存後會永久保留,若是要清空緩存可使用SCRIPT FLUSH。
用於終止正在執行的腳本
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將不會進行持久化操做,全部發生在上一次快照後的數據庫修改都會丟失!