- 關於Redis與Lua的更多詳細知識還可以參閱:
- https://blog.csdn.net/qq_41453285/article/details/103473087
- https://blog.csdn.net/qq_41453285/article/details/103473310
- https://blog.csdn.net/qq_41453285/article/details/103473400
- https://blog.csdn.net/qq_41453285/article/details/103473532
- https://blog.csdn.net/qq_41453285/article/details/103481865
- https://blog.csdn.net/qq_41453285/article/details/103482092
- 爲了保證多條命令組合的原子性,Redis提供了簡單的事務功能以及集成Lua腳本來解決這個問題,本文介紹Lua,事務已經在前一篇文章介紹過了
一、Lua概述
- Lua語言是在1993年由巴西一個大學研究小組發明,其設計目標是作爲嵌入式程序移植到其他應用程序,它是由C語言實現的,雖然簡單小巧但是功能強大
- 所以許多應用都選用它作爲腳本語言,尤其是在遊戲領域,例如大名鼎鼎的暴雪公司將Lua語言引入到“魔獸世界”這款遊戲中,Rovio公司將 Lua語言作爲“憤怒的小鳥”這款火爆遊戲的關卡升級引擎,Web服務器Nginx 將Lua語言作爲擴展,增強自身功能
- Redis將Lua作爲腳本語言可幫助開發者定製自己的Redis命令,在這之前,必須修改源
二、Lua的基本語法
- 現在先簡單地介紹一下Lua的基本語法(只介紹部分語法),與Redis無關,純屬於Lua的語法
數據類型
- Lua語言提供瞭如下幾種數據類型:booleans(布爾)、numbers(數值)、strings(字符串)、tables(表格),和許多高級語言相比,相對簡單
全局變量/局部變量
- local代表val是一個局部變量,如果沒有local代表是全局變量
local strings val = "world"
字符串
- 下面定義一個字符串類型的數據:
local strings val = "world"
- print函數可以打印出變量的值,例如下面代碼將打印world
local strings val = "world" printf(val)
註釋
- "--"是Lua語言的註釋
-- 註釋
數組
- 在Lua中,如果要使用類似數組的功能,可以用tables類型
- 下面代碼使用定義了一個tables類型的變量myArray,但和大多數編程語言不同的是, Lua的數組下標從1開始計算:
local tables myArray = {"redis", "jedis", true, 88.0} -- true print(myArray[3])
for
- 下面代碼會計算1到100的和,關鍵字for以end作爲結束符:
local int sum = 0 for i = 1, 100 do sum = sum + i end -- 輸出結果爲5050 print(sum)
- 要遍歷myArray,首先需要知道tables的長度,只需要在變量前加一個# 號即可:
for i = 1, #myArray do print(myArray[i]) end
- 除此之外,Lua還提供了內置函數ipairs,使用for index,value ipairs(tables)可以遍歷出所有的索引下標和值:
for index,value in ipairs(myArray) do print(index) print(value) end
while
- 下面代碼同樣會計算1到100的和,只不過使用的是while循環,while循環同樣以end作爲結束符
local int sum = 0 local int i = 0 while i <= 100 do sum = sum +i i = i + 1 end --輸出結果爲5050 print(sum)
if、else
- 要確定數組中是否包含了jedis,有則打印true,注意if以end結尾,if後 緊跟then:
local tables myArray = {"redis", "jedis", true, 88.0} for i = 1, #myArray do if myArray[i] == "jedis" then print("true") break else --do nothing end end
哈希
- 如果要使用類似哈希的功能,同樣可以使用tables類型
- 例如下面代碼 定義了一個tables,每個元素包含了key和value,其中strings1..string2是將兩個字符串進行連接:
local tables user_1 = {age = 28, name = "tome"} --user_1 age is 28 print("user_1 age is " .. user_1["age"])
- 如果要遍歷user_1,可以使用Lua的內置函數pairs:
for key,value in pairs(user_1) do print(key .. value) end
函數定義
- 在Lua中,函數以function開頭,以end結尾,funcName是函數名,中間部分是函數體
function funcName() ... end
-- contact函數將兩個字符串拼接: function contact(str1, str2) return str1 .. str2 end --"hello world" print(contact("hello ", "world"))
三、在Redis中使用Lua(eval、evalsha)
- 在Redis中執行Lua腳本有兩種方法:eval和evalsha
eval
- EVAL命令可以直接執行Lua腳本
- 格式如下:
eval 腳本內容 key個數 key列表 參數列表
- 例如:下面執行一個Lua腳本,內容爲“hello world”,參數爲0個
- 例如:下面使用了key列表和參數列表來爲Lua腳本提供更多的靈活性:
- 此時KEYS[1]="redis",ARGV[1]="world",所以最終的返回結果是"hello redisworld"。
eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
- 如果Lua腳本較長,還可以使用redis-cli --eval直接執行文件。eval命令和--eval參數本質是一樣的,客戶端如果想執行Lua腳本,首先在客戶端編寫好Lua腳本代碼,然後把腳本作爲字符串發送給服務端,服務端會將執行結果返回給客戶端,整個過程如下圖所示:
evalsha
- 除了使用eval,Redis還提供了evalsha命令來執行Lua腳本
- 格式如下:
evalsha 腳本SHA1值 key個數 key列表 參數列表
- 執行原理如下:
- 首先要將Lua腳本加載到Redis服務端,得到該腳本的SHA1校驗和
- evalsha命令使用SHA1作爲參數可以直接執行對應Lua腳本,避免每次發送Lua腳本的開銷。這樣客戶端就不需要每次執行腳本內容,而腳本也會常駐在服務端,腳本功能得到了複用
演示案例:
- 建立一個名爲lua_get.lua的腳本文件,內容如下:
return "hello " .. KEYS[1] .. ARGV[1]
- 加載腳本:在系統命令行執行script load命令(下面會介紹)可以將腳本內容加載到Redis內存中,例如下面將lua_get.lua加載到Redis中,得到SHA1 爲:"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
redis-cli script load "$(cat lua_get.lua)"
- 執行腳本:所以在redis-cli內部使用上面的SHA1值就可以執行該腳本了
evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
四、Lua中的Redis API
- 下面介紹幾個在Lua中可以使用的Redis API,更多的API可以百度自行查閱
call()
- Lua可以使用redis.call函數實現對Redis的訪問
- 例如,下面代碼是Lua使用redis.call調用了Redis的set和get操作:
redis.call("set", "hello", "world") redis.call("get", "hello")
- 在Redis中執行的效果如下:
pcall()
- 除此之外Lua還可以使用redis.pcall函數實現對Redis的調用
- redis.call和 redis.pcall的不同在於:如果redis.call執行失敗,那麼腳本執行結束會直接返 回錯誤,而redis.pcall會忽略錯誤繼續執行腳本,所以在實際開發中要根據 具體的應用場景進行函數的選擇。
log()
- Lua可以使用redis.log函數將Lua腳本的日誌輸出到Redis的日誌文件中, 但是一定要控制日誌級別
- 備註:Redis3.2提供了Lua Script Debugger功能用來調試複雜的Lua腳本,具體 可以參考:http://redis.io/topics/ldb
五、使用案例
- Lua腳本功能爲Redis開發和運維人員帶來如下三個好處:
- Lua腳本在Redis中是原子執行的,執行過程中間不會插入其他命令
- Lua腳本可以幫助開發和運維人員創造出自己定製的命令,並可以將這些命令常駐在Redis內存中,實現複用的效果
- Lua腳本可以將多條命令一次性打包,有效地減少網絡開銷
演示案例
- ①當前列表記錄着熱門用戶的id, 假設這個列表有5個元素,如下所示
RPUSH host:user:list user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio lrange host:user:list 0 -1
- user:{id}:ratio代表用戶的熱度,它本身又是一個字符串類型的鍵:
set user:1:ratio 986 set user:8:ratio 762 set user:3:ratio 556 set user:99:ratio 400 set user:72:ratio 101
- ②現要求將列表內所有的鍵對應熱度做加1操作,並且保證是原子執行, 此功能可以利用Lua腳本來實現,例如下面是一個名爲lrange_and_mincr.lua腳本的內容
-- 將列表中所有元素取出,賦值給mylist local mylist = redis.call("lrange", KEYS[1], 0, -1) -- 定義局部變量count=0,這個count就是最後incr的總次數 local count = 0 -- 遍歷mylist中所有元素,每次做完count自增,最後返回count for index,key in ipairs(mylist) do redis.call("incr",key) count = count + 1 end return count
- ③並執行如下操作,返回結果爲5:
redis-cli --eval lrange_and_mincr.lua host:user:list
- ④執行後所有用戶的熱度自增1:
mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio
六、Redis管理Lua腳本
- Redis提供了4個命令實現對Lua腳本的管理,下面分別介紹
①script load
script load script
- 此命令用於將Lua腳本加載到Redis內存中,上面evalsha命令的演示案例中有介紹
②script exists
scripts exists sha1 [sha2 …]
- 此命令用於判斷sha1、sha2...是否已經加載到Redis內存中
- 例如,在上面evalsha命令介紹的演示案例中,我們加載了一個SHA1值爲“7413dc2440db1fea7c0a0bde841fa68eefaf149c”的Lua腳本,現在我們進行檢測,結果返回1
③script flush
script flush
- 此命令用於清除Redis內存已經加載的所有Lua腳本
- 例如:例如我們清楚上面那個SHA1值爲“7413dc2440db1fea7c0a0bde841fa68eefaf149c”的腳本,執行script flush後,腳本不再存在了:
④script kill
script kill
- 此命令用於殺掉正在執行的Lua腳本
- 如果Lua腳本比較耗時,甚至Lua腳本存在問題,那麼此時Lua腳本的執行會阻塞Redis,直到腳本執行完畢或者外部進行干預將其結束。例如,下面的Lua腳本會無線循環,因此客戶端會阻塞
- lua-time-limit參數:
- Redis提供了一個lua-time-limit參數,單位爲毫秒,默認爲5000毫秒(5秒),它是Lua腳本的“超時時間”
- 但這個超時時間僅僅是當Lua腳本時間超過lua-time-limit後,向其他命令調用發送BUSY的信號,但是並不會停止掉服務端和客戶端的腳本執行,所以當達到lua-time-limit值之後,其他客戶端在執行正常的命令時,將會收到“Busy Redis is busy running a script”錯誤,並且提示使用script kill或者shutdown nosave命令來殺掉這個busy的腳本(見下圖演示案例)
- script kill比shutdown命令更好:在Lua阻塞時,使用script kill更好,因爲shutdown會讓Redis服務停止,而script不會,其只是關閉Lua腳本的執行
演示案例
- 接着上圖,我們左側的客戶端執行的Lua腳本處於阻塞狀態,此時我們在右邊輸入script kill殺死正在執行的Lua腳本,此時左側客戶端返回,所有客戶端可以繼續執行訪問了
scritp kill命令失效的情況
- 如果Lua腳本正在執行寫操作,那麼script kill命令就會失效
- 例如,下面左側客戶端執行的Lua腳本一直不停的在執行set操作,右側客戶端使用script kill時會顯示出錯(上提示Lua腳本正在向Redis執行寫命令,要麼等待腳本執行結束要麼使用shutdown nosave停掉Redis服務)