Redis(開發與運維):21---常用功能之(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

  • 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服務

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章