最近在作K線的項目中,須要計算商品的分時數據。爲了保證多臺機器對同一商品的計算的有序性,因此在Redis中進行計算,同時爲了保證在分時數據計算過程的原子性因此使用了LUA腳本,Redis內置了對LUA腳本的支持,而且在計算過程當中保證了腳本中執行的原子性。所以在開發過程當中對Redis對Lua的支持進行了學習。從 Redis 2.6.0 版本開始,經過內置的 Lua 解釋器,可使用EVAL命令對 Lua 腳本進行求值。如下將Redis對LUA的支持進行總結。redis
從Redis2.6.0版本開始,經過內置的Lua解釋器,可使用EVAL命令對Lua腳本進行求值。EVAL命令的格式以下:數據庫
EVAL script numkeys key [key ...] arg [arg ...]
script參數是一段Lua腳本程序,它會被運行在Redis服務器上下文中,這段腳本沒必要(也不該該)定義爲一個Lua函數。numkeys參數用於指定鍵名參數的個數。鍵名參數 key [key ...] 從EVAL的第三個參數開始算起,表示在腳本中所用到的那些Redis鍵(key),這些鍵名參數能夠在 Lua中經過全局變量KEYS數組,用1爲基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。在命令的最後,那些不是鍵名參數的附加參數 arg [arg ...] ,能夠在Lua中經過全局變量ARGV數組訪問,訪問的形式和KEYS變量相似( ARGV[1] 、 ARGV[2] ,諸如此類)。例如編程
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"
其中 "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是被求值的Lua腳本,數字2指定了鍵名參數的數量, key1和key2是鍵名參數,分別使用 KEYS[1] 和 KEYS[2] 訪問,而最後的 first 和 second 則是附加參數,能夠經過 ARGV[1] 和 ARGV[2] 訪問它們。在 Lua 腳本中,可使用兩個不一樣函數來執行Redis命令,它們分別是:json
redis.call() redis.pcall()
這兩個函數的惟一區別在於它們使用不一樣的方式處理執行命令所產生的錯誤。redis.call() 和 redis.pcall() 兩個函數的參數能夠是任何格式良好(well formed)的 Redis 命令:數組
> eval "return redis.call('set','foo','bar')" 0 OK
須要注意的是,上面這段腳本的確實現了將鍵 foo 的值設爲 bar 的目的,可是,它違反了 EVAL 命令的語義,由於腳本里使用的全部鍵都應該由 KEYS 數組來傳遞,就像這樣:緩存
> eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK
要求使用正確的形式來傳遞鍵(key)是有緣由的,由於不只僅是EVAL這個命令,全部的Redis命令,在執行以前都會被分析,籍此來肯定命令會對哪些鍵進行操做。所以,對於EVAL命令來講,必須使用正確的形式來傳遞鍵,才能確保分析工做正確地執行。除此以外,使用正確的形式來傳遞鍵還有不少其餘好處,它的一個特別重要的用途就是確保Redis集羣能夠將你的請求發送到正確的集羣節點。服務器
當Lua經過call()或 pcall()函數執行Redis命令的時候,命令的返回值會被轉換成Lua數據結構。一樣地,當Lua腳本在 Redis 內置的解釋器裏運行時Lua腳本的返回值也會被轉換成Redis協議(protocol),而後由EVAL將值返回給客戶端。Lua 類型和 Redis 類型之間存在着一一對應的轉換關係。如下列出的是詳細的轉換規則:數據結構
如下是幾個類型轉換的例子:dom
> eval "return 10" 0 (integer) 10 > eval "return {1,2,{3,'Hello World!'}}" 0 1) (integer) 1 2) (integer) 2 3) 1) (integer) 3 2) "Hello World!" > eval "return redis.call('get','foo')" 0 "bar"
Redis使用單個 Lua 解釋器去運行全部腳本,而且, Redis 也保證腳本會以原子性(atomic)的方式執行:當某個腳本正在運行的時候,不會有其餘腳本或 Redis 命令被執行。這和使用 MULTI / EXEC 包圍的事務很相似。在其餘別的客戶端看來,腳本的效果(effect)要麼是不可見的(not visible),要麼就是已完成的(already completed)。另外一方面,這也意味着,執行一個運行緩慢的腳本並非一個好主意。寫一個跑得很快很順溜的腳本並不難,由於腳本的運行開銷(overhead)很是少,可是當你不得不使用一些跑得比較慢的腳本時,請當心,由於當這些蝸牛腳本在慢吞吞地運行的時候,其餘客戶端會由於服務器正忙而沒法執行命令。函數
redis.call() 和 redis.pcall() 的惟一區別在於它們對錯誤處理的不一樣。當 redis.call() 在執行命令的過程當中發生錯誤時,腳本會中止執行,並返回一個腳本錯誤,錯誤的輸出信息會說明錯誤形成的緣由:
redis> lpush foo a (integer) 1 redis> eval "return redis.call('get', 'foo')" 0 (error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value
和 redis.call() 不一樣, redis.pcall() 出錯時並不引起(raise)錯誤,而是返回一個帶 err 域的 Lua 表(table),用於表示錯誤:
redis 127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0 (error) ERR Operation against a key holding the wrong kind of value
EVAL 命令要求你在每次執行腳本的時候都發送一次腳本主體(script body)。Redis 有一個內部的緩存機制,所以它不會每次都從新編譯腳本,不過在不少場合,付出無謂的帶寬來傳送腳本主體並非最佳選擇。爲了減小帶寬的消耗, Redis 實現了 EVALSHA 命令,它的做用和 EVAL 同樣,都用於對腳本求值,但它接受的第一個參數不是腳本,而是腳本的 SHA1 校驗和(sum)。EVALSHA 命令的表現以下:
如下是示例:
> set foo bar OK > eval "return redis.call('get','foo')" 0 "bar" > evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0 "bar" > evalsha ffffffffffffffffffffffffffffffffffffffff 0 (error) `NOSCRIPT` No matching script. Please use [EVAL](/commands/eval).
客戶端庫的底層實現能夠一直樂觀地使用 EVALSHA 來代替 EVAL ,並指望着要使用的腳本已經保存在服務器上了,只有當 NOSCRIPT 錯誤發生時,才使用 EVAL 命令從新發送腳本,這樣就能夠最大限度地節省帶寬。這也說明了執行 EVAL 命令時,使用正確的格式來傳遞鍵名參數和附加參數的重要性:由於若是將參數硬寫在腳本中,那麼每次當參數改變的時候,都要從新發送腳本,即便腳本的主體並無改變,相反,經過使用正確的格式來傳遞鍵名參數和附加參數,就能夠在腳本主體不變的狀況下,直接使用 EVALSHA 命令對腳本進行復用,免去了無謂的帶寬消耗。
Redis 保證全部被運行過的腳本都會被永久保存在腳本緩存當中,這意味着,當 EVAL 命令在一個 Redis 實例上成功執行某個腳本以後,隨後針對這個腳本的全部 EVALSHA 命令都會成功執行。刷新腳本緩存的惟一辦法是顯式地調用 SCRIPT FLUSH 命令,這個命令會清空運行過的全部腳本的緩存。緩存能夠長時間儲存而不產生內存問題的緣由是,它們的體積很是小,並且數量也很是少,即便腳本在概念上相似於實現一個新命令,即便在一個大規模的程序裏有成百上千的腳本,即便這些腳本會常常修改,即使如此,儲存這些腳本的內存仍然是微不足道的。事實上,用戶會發現 Redis 不移除緩存中的腳本其實是一個好主意。好比說,對於一個和 Redis 保持持久化連接(的程序來講,它能夠確信,執行過一次的腳本會一直保留在內存當中,所以它能夠在流水線中使用 EVALSHA 命令而沒必要擔憂由於找不到所需的腳本而產生錯誤(稍候咱們會看到在流水線中執行腳本的相關問題)。Redis 提供瞭如下幾個 SCRIPT 命令,用於對腳本子系統(scripting subsystem)進行控制:
在編寫腳本方面,一個重要的要求就是,腳本應該被寫成純函數(pure function)。也就是說,腳本應該具備如下屬性:
使用系統時間(system time)、調用像RANDOMKEY那樣的隨機命令、或者使用 Lua 的隨機數生成器,相似以上的這些操做,都會形成腳本的求值沒法每次都得出一樣的結果。爲了確保腳本符合上面所說的屬性, Redis作了如下工做:
爲了防止沒必要要的數據泄漏進Lua環境, Redis腳本不容許建立全局變量。若是一個腳本須要在屢次執行之間維持某種狀態,它應該使用Redis key來進行狀態保存。企圖在腳本中訪問一個全局變量(不論這個變量是否存在)將引發腳本中止, EVAL命令會返回一個錯誤:
redis 127.0.0.1:6379> eval 'a=10' 0 (error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a'
Lua的debug工具,或者其餘設施,好比打印(alter用於實現全局保護的meta table ,均可以用於實現全局變量保護。實現全局變量保護並不難,不過有時候仍是會不當心而爲之。一旦用戶在腳本中混入了 Lua 全局狀態,那麼 AOF持久化和複製(replication)都會沒法保證,因此,請不要使用全局變量。避免引入全局變量的一個訣竅是:將腳本中用到的全部變量都使用 local 關鍵字定義爲局部變量。
Redis內置的Lua解釋器加載瞭如下Lua庫:base、table、string、math、debug、cjson、cmsgpack。其中cjson庫可讓Lua以很是快的速度處理JSON數據,除此以外,其餘別的都是Lua的標準庫。每一個Redis實例都保證會加載上面列舉的庫,從而確保每一個 Redis 腳本的運行環境都是相同的。
在Lua腳本中,能夠經過調用redis.log函數來寫Redis日誌(log):redis.log(loglevel, message)其中, message 參數是一個字符串,而 loglevel 參數能夠是如下任意一個值:
上面的這些等級(level)和標準 Redis 日誌的等級相對應。對於腳本散發(emit)的日誌,只有那些和當前 Redis 實例所設置的日誌等級相同或更高級的日誌纔會被散發。如下是一個日誌示例:
redis.log(redis.LOG_WARNING, "Something is wrong with this script.")
執行上面的函數會產生這樣的信息:
[32343] 22 Mar 15:21:39 # Something is wrong with this script.
腳本應該僅僅用於傳遞參數和對Redis數據進行處理,它不該該嘗試去訪問外部系統(好比文件系統),或者執行任何系統調用。除此以外,腳本還有一個最大執行時間限制,它的默認值是5秒鐘,通常正常運做的腳本一般能夠在幾分之幾毫秒以內完成,花不了那麼多時間,這個限制主要是爲了防止因編程錯誤而形成的無限循環而設置的。最大執行時間的長短由lua-time-limit選項來控制(以毫秒爲單位),能夠經過編輯 redis.conf 文件或者使用 CONFIG GET 和 CONFIG SET 命令來修改它。當一個腳本達到最大執行時間的時候,它並不會自動被Redis 結束,由於Redis必須保證腳本執行的原子性,而中途中止腳本的運行意味着可能會留下未處理完的數據在數據集(data set)裏面。所以,當腳本運行的時間超過最大執行時間後,如下動做會被執行:
在流水線請求的上下文中使用EVALSHA命令時,要特別當心,由於在流水線中,必須保證命令的執行順序。一旦在流水線中由於EVALSHA命令而發生NOSCRIPT錯誤,那麼這個流水線就再也沒有辦法從新執行了,不然的話,命令的執行順序就會被打亂。爲了防止出現以上所說的問題,客戶端庫實現應該實施如下的其中一項措施:
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
根據給定的sha1校驗碼,對緩存在服務器中的腳本進行求值。將腳本緩存到服務器的操做能夠經過 SCRIPT LOAD 命令進行。這個命令的其餘地方,好比參數的傳入方式,都和 EVAL 命令同樣。
redis> SCRIPT LOAD "return 'hello moto'" "232fd51614574cf0867b83d384a5e898cfd24e5a" redis> EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 0 "hello moto"
SCRIPT EXISTS sha1 [sha1 ...]
給定一個或多個腳本的SHA1校驗和,返回一個包含0和1的列表,表示校驗和所指定的腳本是否已經被保存在緩存當中。返回值:
redis> SCRIPT LOAD "return 'hello moto'" # 載入一個腳本 "232fd51614574cf0867b83d384a5e898cfd24e5a" redis> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a 1) (integer) 1 redis> SCRIPT FLUSH # 清空緩存 OK redis> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a 1) (integer) 0
殺死當前正在運行的Lua腳本,當且僅當這個腳本沒有執行過任何寫操做時,這個命令才生效。這個命令主要用於終止運行時間過長的腳本,好比一個由於BUG而發生無限loop的腳本,諸如此類。SCRIPT KILL執行以後,當前正在運行的腳本會被殺死,執行這個腳本的客戶端會從EVAL命令的阻塞當中退出,並收到一個錯誤做爲返回值。另外一方面,假如當前正在運行的腳本已經執行過寫操做,那麼即便執行 SCRIPT KILL ,也沒法將它殺死,由於這是違反 Lua 腳本的原子性執行原則的。在這種狀況下,惟一可行的辦法是使用 SHUTDOWN NOSAVE 命令,經過中止整個 Redis 進程來中止腳本的運行,並防止不完整(half-written)的信息被寫入數據庫中。