Redis Lua腳本


1   介紹

Redis自2.6.0加入了Lua腳本相關的命令,EVAL, EVALSHA, SCRIPT EXISTS, SCRIPT FLUSH, SCRIPT KILL, SCRIPT LOAD,自3.2.0加入了Lua腳本的調試功能和命令。

Lua腳本可以運行在任何平台上,也可以嵌入到大多數語言中,來擴展其功能。Lua腳本是用C語言寫的,體積很小,運行速度很快。

使用Redis Lua腳本功能,用戶可以向服務器發送Lua腳本來執行自定義動作,獲取腳本的相應數據。Redis服務器會單線程原子性執行Lua腳本,保證Lua腳本在執行過程中不會被任意其他請求打斷。

 

生產環境中,推薦使用EVALSHA,相較於EVAL的每次發送腳本主體、占用帶寬,EVALSHA會更高效。

使用Lua腳本的好處:

1)          減少網絡開銷:將腳本發送到服務端,在服務端進行計算,並將結果返回客戶端,避免了傳遞大量數據。

2)          原子性的操作:Redis會將整個腳本作為一個整體執行,中間不會被其他命令插入,因此在編寫腳本的過程中,無需使用事物

3)          代碼復用

使用Lua腳本需注意的問題:

1)          單線程執行。所有Lua命令都在同一個Lua解釋器中執行,當一個腳本執行時,其他腳本或Redis命令都不能執行。如果腳本執行慢,會比較麻煩。

2)          寫純函數腳本

3)          Redis集群模式要求單個Lua腳本操作的Key必須在同一節點上,但是Cluster會將數據自動分布到不同的節點(虛擬的16384個slot)。阿里雲集群版官網也有說明:在Redis集群版實例中,事務、腳本等命令要求的key必須在同一slot中,否則會返回錯誤信息:command keys must in same slot。

2       Redis調用Lua腳本

2.1   EVAL指令

Eval語法:

eval script numkeys key [key ...] arg [arg ...]

script: lua腳本

numkeys:表示有幾個Key,分別是KEYS[1],KEYS[2],….,從第numkeys+1開始是參數值,ARGV[1],ARGV[2],……

注意:EVAL命令根據參數numkeys來將后面的所有參數分別存入腳本中KEYS和ARGV兩個table類型的全局變量。當腳本不需要任何參數時,也不能省略這個參數,應設為0。

示例:

 

 

 

2.2   EVALSHA和SCRIPT LOAD指令

EVAL可以將腳本內容傳遞到服務端執行,可是如果腳本內容很長,而且客戶端頻繁執行的話,會浪費帶寬。Redis提供了SCRIPT LOAD和EVALSHA指令來解決這個問題

 

SCRIPT LOAD 指令用於將客戶端提供的 lua 腳本傳遞到服務器而不執行,但是會得到腳本的唯一ID(腳本的SHA1校驗和),這個唯一ID 是用來唯一標識服務器緩存的這段 lua 腳本,它是由 Redis 使用 sha1 算法揉捏腳本內容而得到的一個很長的字符串。有了這個唯一 ID,后面客戶端就可以通過 EVALSHA 指令反復執行這個腳本了。通過SCRIPT LOAD上傳的腳本會一直存在緩存中,除非調用了清除腳本命令SCRIPT FLUSH。

SCRIPT LOAD指令:

script load luascript

其中luascript為腳本內容,方法返回腳本內容的SHA1值。

EVALSHA指令:

eval scriptsha1 numkeys key [key ...] arg [arg ...]

EVALSHA命令與EVAL命令一致,但是第一個參數是Lua腳本的SHA1值。

示例:

 

 

 

 

2.3   執行Lua腳本文件

在命令行編寫復雜的Lua 腳本不方便,可以將腳本存儲為lua文件,然后運行redis-cli –eval命令來執行腳本。

Redis-cli –h xfraud1 –p 6379 –eval scriptpath key1 key2, arg1 arg2

其中scriptpath為Lua腳本存放的路徑,腳本后面傳入的是參數,通過“,”分隔為兩組,前面是KEYS,后面是ARGV。參數之間要用空格分隔。

示例:

 

 

 

3       Lua腳本中調用Redis命令

在Lua腳本中,可以通過兩種方式調用Redis命令。

  • redis.call
  • redis.pcall

兩個函數的作用類似,區別在於錯誤處理機制不同。在腳本運行出現錯誤時,Redis會保護主線程不會因為腳本的錯誤導致服務器崩潰,近似於在腳本的外圍有一個很大的try catch語句包裹。call調用只會向上拋出異常,客戶端會輸出服務器返回的通用錯誤消息。Lua原生沒有提供try catch語句,但是lua內置了pcall(f)函數,pcall的意思是protected call ,它會讓f函數運行在保護模式下,f如果出現了錯誤,pcall調用會返回false和錯誤信息。

Redis.call函數調用產生錯誤,腳本的返回信息如下圖所示。

 

 

 

客戶端輸出的是一個通用的錯誤信息,而不是incr調用本應該返回的WRONGTYPE類型的錯誤消息。Redis內部在處理redis.call遇到錯誤時是向上拋出異常。Pcall調用捕獲到腳本異常時會向客戶端回復錯誤信息。如果我們將上面的call改成pcall,結果如下。

 

 

 

注意:在Lua腳本執行的過程中遇到了錯誤,同Redis的事務一樣,那些通過redis.call函數已經執行過的指令對服務器狀態產生的影響是無法撤銷的,在編寫Lua腳本時一定要小心,避免沒有考慮到的判斷條件導致腳本沒有完全執行。

 

 

 

4       Lua腳本中記錄日志

redis.log(loglevel,message)

loglevel 如下:

  • redis.LOG_DEBUG
  • redis.LOG_VERBOSE
  • redis.LOG_NOTICE
  • redis.LOG_WARNING

message僅僅接收String類型

舉例:

redis.log(redis.LOG_WARNING,"Something is wrong with this script.")

可以查看redis的conf文件,查看Redis服務日志存放位置。當腳本中輸出日志的級別等於或大於conf中設置的級別時,才會輸出日志。

 

5       Redis與Lua數據結構對應關系

Redis數據結構

Lua數據結構

Integer

Number

Bulk

String

Multi bulk

Table

Status

Lua 的table中有一個OK做對應

Error

Lua 的table中有一個err做對應

Nil bulk, nil multi bulk

Lua的boolean的false

注意:

  • Lua Boolean true會變成Redis中的integer 1
  • Lua中的所有number類型的數據,均會變成redis中的integer,采用截取的方式。如果需要lua返回float類型,請使用string作為返回值。
  • Redis中沒有對nil進行轉換的簡單方法,如果lua的table中的元素有nil,redis無法進行轉換。

6       Lua Debugger

可以使用ldb對Lua腳本進行調試。

  • LDB使用的是 server-client模式,所以它是一個遠程調試器.
  • Redis server 作為一個調試服務器,默認調試client端是redis-cli,其他client端可以根據redis的協議進行擴展。
  • 默認情況下,Redis會fork一個進程進入隔離環境,不會影響Redis正常提供服務,但調試期間,原始Redis執行命令、腳本的結果也不會體現到fork之后的隔離環境中。每一個調試進程都是一個單獨的進程。這意味着在調試一個Lua腳本的同時,Redis不會阻塞,可以進行開發或者並行調試其他腳本。這也意味着調試進程中的所有更改均會回退(roll back),這保證使用同一份數據多次調試lua腳本不會存在問題。
  • redis也提供了另外一種調試模式—ldb-sync-mode,即同步模式,該模式下產生的變化將會保留,並會阻塞其他請求。

調試時,加上參數—ldb即可,示例:

redis-cli  --ldb -h xfraud1 -p 6379  --eval /CFCA/luaScript/amountsum.lua testamount

 

7       其他約定

  • Redis的Lua腳本不允許生命全局變量,防止Lua腳本泄露數據。Lua腳本可以使用2個全局變量KEYS和ARGV,這兩個全局變量用於接收傳遞的KEY和ARG。

8       腳本示例

function string.split(input, delimiter)
  input = tostring(input)
  delimiter = tostring(delimiter)
  if (delimiter=='') then return false end
  local pos,arr = 0, {}
  -- for each divider found
  for st,sp in function() return string.find(input, delimiter, pos, true) end do
    table.insert(arr, string.sub(input, pos, st - 1))
    pos = sp + 1
  end
  table.insert(arr, string.sub(input, pos))
  return arr
end

-- 腳本里所有的鍵都應該由KEYS數組來傳遞,因為所有的redis命令,在執行之前都會被分析以確定命令會對哪些鍵進行操作

--從有序集合中獲取所有元素,每個元素的格式為string-number,以“-”分割,number部分表示數值,代碼實現的功能是求取所有數值的和
local tradeSet = redis.pcall('zrange',KEYS[1],0,-1)
local sum = 0;
for k,v in pairs(tradeSet) do
    local tmp = v
    redis.log(redis.LOG_NOTICE,"tmp value:" .. tmp)
    local tmpSplitArray = string.split(tmp,'-')
    local length = table.getn(tmpSplitArray)
    if(length == 2) then
        local tmpAmount = tmpSplitArray[2]
        redis.log(redis.LOG_NOTICE,"tmp amount:" .. tmpAmount)
        sum = sum + tonumber(tmpAmount)
    end

end
return tostring(sum) 

9       常見問題

1)          純函數

在Lua腳本中加入redis.call("TIME"),使用的時候會報錯,這是因為Redis默認情況復制Lua腳本到備機和持久化中,如果腳本是一個非純函數,備庫中執行的時候或者宕機恢復的時候可能產生不一致的情況。Redis在3.2版本中加入了redis.replicate_commands函數來解決這個問題,在腳本第一行執行這個函數,Redis會將修改數據的命令收集起來,然后用MULTI/EXEC包裹起來,這種方式稱為script effects replication,這個類似於mysql中的基於行的復制模式,將非純函數的值計算出來,用來持久化和主從復制。

2)          @user_script:1: @user_script: 1: Lua script attempted to access a non local key in a cluster node

Redis集群中,會將key分配到不同的slot,然后分配到對應的機器上,當Redis執行腳本的節點與Key存放的節點不同時,會返回錯誤: @user_script:1: @user_script: 1: Lua script attempted to access a non local key in a cluster node。Redis集群模式要求單個Lua腳本操作的Key必須在同一個節點上,阿里雲集群版官網也有說明:在Redis集群版實例中,事務、腳本等命令要求的key必須在同一個slot中,如果不在同一個slot將返回錯誤信息:command keys must in same slot.

3)          @enable_strict_lua:8: user_script:2: Script attempted to create global variable '***'

Lua默認變量是全局的,在腳本中應使用local變量,避免在持久化、復制的時候產生各種問題。應使用 local *** 定義局部變量


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM