Redis進階實踐之十九 Redis如何使用lua腳本html
1、引言
redis學了一段時間了,基本的東西都沒問題了。從今天開始講寫一些redis和lua腳本的相關的東西,lua這個腳本是一個好東西,能夠運行在任何平臺上,也能夠嵌入到大多數語言當中,來擴展其功能。lua腳本是用C語言寫的,體積很小,運行速度很快,而且每次的執行都是做爲一個原子事務來執行的,咱們能夠在其中作不少的事情。因爲篇幅不少,一次沒法概述所有,這個系列可能要經過多篇文章的形式來寫,好了,今天咱們進入正題吧。
2、lua簡介
Lua 是一個小巧的腳本語言。是巴西里約熱內盧天主教大學(Pontifical Catholic University of Rio de Janeiro)裏的一個研究小組,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所組成並於1993年開發。 其設計目的是爲了嵌入應用程序中,從而爲應用程序提供靈活的擴展和定製功能。Lua由標準C編寫而成,幾乎在全部操做系統和平臺上均可以編譯,運行。Lua並無提供強大的庫,這是由它的定位決定的。因此Lua不適合做爲開發獨立應用程序的語言。Lua 有一個同時進行的JIT項目,提供在特定平臺上的即時編譯功能。
Lua腳本能夠很容易的被C/C++ 代碼調用,也能夠反過來調用C/C++的函數,這使得Lua在應用程序中能夠被普遍應用。不只僅做爲擴展腳本,也能夠做爲普通的配置文件,代替XML,ini等文件格式,而且更容易理解和維護。 Lua由標準C編寫而成,代碼簡潔優美,幾乎在全部操做系統和平臺上均可以編譯,運行。一個完整的Lua解釋器不過200k,在目前全部腳本引擎中,Lua的速度是最快的。這一切都決定了Lua是做爲嵌入式腳本的最佳選擇。
3、EVAL命令的詳解
一、EVAL簡介(Introduction to EVAL)
Redis從其2.6.0版本或者更高的版本之後,可使用 Lua腳本解釋器的 EVAL命令和 EVALSHA 命令測試評估腳本。
EVAL命令的第一個參數是一個 Lua5.版本1的腳本。腳本不須要定義一個Lua函數(不該該)。它僅僅是一個在Redis服務器的上下文中運行的Lua程序。
EVAL命令的第二個參數緊跟Lua腳本後面的那個參數,這個參數表示KEYS參數的個數,從第三個參數開始表明Redis鍵名稱。Lua腳本能夠訪問由KEYS全局變量(如KEYS [1],KEYS [2],...)組成的一維數據的參數。
EVAL最後附加的參數表示的是對應KEYS鍵名所對應的值,而且Lua腳本能夠經過使用ARGV全局變量的訪問其值,和KEYS數組的狀況差很少(因此ARGV[1],ARGV[2],...)。
如下示例應該闡明上述內容:
node
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"
注意:正如你所看到的,做爲Redis批量回復的形式返回了Lua數組,這是Redis返回的一種類型,在咱們實現的客戶端庫(針對某種語言實現的Redis操做庫)中應該會將其轉換爲針對該編程語言中的特定Array類型。
可使用兩個不一樣的Lua函數從Lua腳本調用Redis命令:
redis.call()
redis.pcall()
redis.call()與redis.pcall()很是相似,惟一的區別是,若是Redis命令調用發生了錯誤,redis.call() 將拋出一個Lua類型的錯誤,再強制EVAL命令把錯誤返回給命令的調用者,而redis.pcall()將捕獲錯誤並返回表示錯誤的Lua表類型。
redis.call()和redis.pcall()函數的參數是Redis命令和命令所須要的參數:
redis
> eval "return redis.call('set','foo','bar')" 0 OK
上面的腳本的意思是:將鍵foo的值設置爲字符串的bar,和(set foo bar)命令意義相同。可是它違反了EVAL命令的語義,由於Lua腳本使用的全部鍵應該經過使用KEYS數組來傳遞進來:
數據庫
> eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK
在執行以前,必須分析全部的Redis命令,以肯定命令將在哪些鍵上運行。爲了使EVAL命令執行成功,必須明確傳遞所需的鍵。這在不少方面都頗有用,但特別要確保Redis羣集能夠將您的請求轉發到適當的羣集節點。
(All Redis commands must be analyzed before execution to determine which keys the command will operate on. In order for this to be true for EVAL, keys must be passed explicitly. This is useful in many ways, but especially to make sure Redis Cluster can forward your request to the appropriate cluster node.)
請注意,此規則未實施,爲用戶提供濫用Redis單實例配置的機會,這是以編寫與Redis集羣不兼容的腳本爲代價的。
(Note this rule is not enforced in order to provide the user with opportunities to abuse the Redis single instance configuration, at the cost of writing scripts not compatible with Redis Cluster.)
Lua腳本可使用一組轉換規則返回從Lua類型轉換爲Redis協議的值。
(Lua scripts can return a value that is converted from the Lua type to the Redis protocol using a set of conversion rules.)
二、Lua和Redis數據類型之間的轉換(Conversion between Lua and Redis data types)
當Lua腳本使用call()或pcall()調用Redis命令時,Redis返回值將轉換爲Lua數據類型。一樣,在調用Redis命令和Lua腳本返回值時,Lua數據類型將轉換爲Redis協議類型,以便腳本能夠控制EVAL返回給客戶端的內容。
數據類型之間的轉換原則是,若是將Redis類型轉換爲Lua類型,而後將結果轉換回Redis類型,則結果與初始值相同。
換句話說,Lua和Redis類型之間存在一對一的轉換。下表顯示了全部轉換規則:
Redis to Lua 轉換對應表。
編程
Redis integer reply -> Lua number Redis bulk reply -> Lua string Redis multi bulk reply -> Lua table (may have other Redis data types nested) Redis status reply -> Lua table with a single ok field containing the status Redis error reply -> Lua table with a single err field containing the error Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type
Lua to Redis 轉換對應表.
json
Lua number -> Redis integer reply (the number is converted into an integer) Lua string -> Redis bulk reply Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside the Lua array if any) Lua table with a single ok field -> Redis status reply Lua table with a single err field -> Redis error reply Lua boolean false -> Redis Nil bulk reply.
還有一個額外的Lua-to-Redis轉換規則沒有對應的Redis到Lua轉換規則:
Lua boolean true -> Redis integer reply with value of 1。
還有兩條重要規則須要注意:
2.一、Lua腳本有一個數字類型,Lua數字。 整數和浮點數是沒有區別。所以咱們老是將Lua數字轉換爲整數回覆,若是有的小數的話,會刪除數字的小數部分。若是你想從Lua腳本中返回一個浮點數,你應該像字符串同樣返回它,就像Redis本身作的那樣(參見例如 ZSCORE (https://redis.io/commands/zscore)命令)。
2.二、沒有簡單的方法在Lua數組中包含有nil(www.lua.org/pil/19.1.html),這是Lua表語義決定的,因此當Redis將Lua數組轉換爲Redis協議類型時,若是遇到nil,轉換就會中止。
如下是幾個轉換示例:c#
> 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"
最後一個例子顯示瞭如何從Lua腳本接收redis.call()或redis.pcall()的確切返回值,若是該命令是直接調用的,將會返回該值。
在下面的例子中,咱們能夠看到如何處理帶有nils的浮點數和數組:api
> eval "return {1,2,3.3333,'foo',nil,'bar'}" 0 1) (integer) 1 2) (integer) 2 3) (integer) 3 4) "foo"
正如你所看到的,3.333被轉換成3,因爲在 bar 字符串以前是nil值,所以 bar 字符串永遠不會被返回。
三、Helper函數返回Redis類型(Helper functions to return Redis types)
有兩個幫助函數能夠從Lua腳本返回Redis類型。
3.一、redis.error_reply(error_string)返回錯誤回覆。這個函數只是返回一個字段表,其中err字段特殊指定的字符串。
3.二、redis.status_reply(status_string)返回一個狀態回覆。這個函數只是返回一個字段表,其中的ok字段設置爲指定的字符串。
使用輔助函數或直接以指定格式返回表是沒有區別,因此如下兩種形式是等價的:
(There is no difference between using the helper functions or directly returning the table with the specified format, so the following two forms are equivalent:)數組
return {err="My Error"} return redis.error_reply("My Error")
四、腳本的原子性(Atomicity of scripts)
Redis使用相同的Lua解釋器來運行全部命令。另外,Redis保證以原子方式執行腳本:執行腳本時不會執行其餘腳本或Redis命令。與 MULTI/EXEC 事務的概念類似。從全部其餘客戶端的角度來看,腳本要不已經執行完成,要不根本不執行。
然而運行一個緩慢的腳本就是一個很愚蠢的主意。建立快速執行的腳本並不難,由於腳本開銷很是低。可是,若是您要使用了執行緩慢的腳本,因爲其的原子性,其餘客戶端的命令都是得不到執行的,這並非咱們想要的結果,你們要切記。
五、錯誤處理(Error handling)
如前所述,調用redis.call() 致使Redis命令錯誤會中止腳本的執行並返回一個錯誤,很明顯錯誤是由腳本生成的:緩存
> del foo (integer) 1 > lpush foo a (integer) 1 > eval "return redis.call('get','foo')" 0 (error) ERR Error running script (call to f_6b1bf486c81ceb7edf3c093f4c48582e38c0e791): ERR Operation against a key holding the wrong kind of value
使用redis.pcall() 方法調用是不會引起錯誤,但會以上面指定的格式(做爲具備err字段的Lua表類型)返回錯誤對象。 該腳本經過調用redis.pcall() 返回的錯誤對象將確切的錯誤傳遞給用戶。
六、帶寬和EVALSHA(Bandwidth and EVALSHA)
EVAL命令強制您一次又一次發送腳本正文。 Redis不須要每次從新編譯腳本,由於它使用內部緩存機制,可是在許多狀況下,大量的屢次的發送腳本正文佔用了額外帶寬的,這個成本也是不容忽視的。
另外一方面,使用特殊命令或經過redis.conf定義命令也會有相應的問題,緣由以下:
6.一、不一樣的實例可能有不一樣的命令實現。
6.二、若是咱們必須確保全部實例都包含給定命令,特別是在分佈式環境中,則部署很是困難。
6.三、閱讀應用程序代碼,完整的語義可能並非十分清晰明瞭,由於應用程序調用的命令都是定義在服務器端的。
爲避免這些問題,同時避免帶寬損失,Redis實現了EVALSHA命令。
EVALSHA的工做方式與EVAL徹底相同,但不是將腳本做爲第一個參數,而是使用腳本的SHA1摘要。 行爲以下:
6.一、若是服務器仍然記住具備匹配的SHA1摘要的腳本,則執行該腳本。
6.二、若是服務器不記得具備此SHA1摘要的腳本,則會返回一個特殊錯誤,告訴客戶端使用EVAL。
示例代碼:
> 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).
雖然客戶端(這個客戶端能夠指的是應用程序的代碼,調用端,這個客戶端必須經過「客戶端庫」來實現操做Redis)使用的就是EVAL命令,可是客戶端庫(這個客戶端庫指的是針對某種語言封裝的對Redis的操做,好比針對c#操做Redis的封裝就是StackExchange.Redis,這個就是客戶端庫,和客戶端不一樣意思)內部的實現能夠換個思路,先使用的EVALSHA命令,若是腳本已在服務器上存在,就順利執行。若是返回NOSCRIPT錯誤,說明,服務器上並無相應的腳本,而後在切換到EVAL命令繼續執行。(有點明修棧道暗度陳倉的意思)
將鍵和參數做爲額外的EVAL參數傳遞在這種狀況下也很是有用,由於腳本字符串保持不變而且能夠由Redis高效緩存。
七、腳本緩存語義(Script cache semantics)
執行過的腳本保證會永遠緩存在Redis實例中,只要運行腳本的Redis實例是運行的。這意味着在Redis實例中若是執行了一次EVAL命令,全部後續的EVALSHA調用都將成功。
腳本能夠長時間緩存的緣由是編寫良好的應用程序不可能有足夠的不一樣腳原本引發內存問題。每個腳本在概念上都像是一個新的命令,甚至一個大型的應用程序可能只有幾百個。 即便應用程序被屢次修改而且腳本會改變,所使用的內存也能夠忽略不計的。
(The reason why scripts can be cached for long time is that it is unlikely for a well written application to have enough different scripts to cause memory problems. Every script is conceptually like the implementation of a new command, and even a large application will likely have just a few hundred of them. Even if the application is modified many times and scripts will change, the memory used is negligible.)
清除腳本緩存的惟一方法是顯示的調用SCRIPT FLUSH命令,該命令將完全清空到目前爲止全部已經執行過的緩存的腳本。
這種狀況僅僅發生在當Redis實例將要爲雲環境中的另外一個客戶或應用程序實例化時才須要執行Script Flush命令。
另外,如前所述,從新啓動Redis實例會清空腳本緩存,這不是持久性的。可是從客戶端的角度來看,只有兩種方法能夠確保Redis實例在兩個不一樣的命令之間不會從新啓動。
7.一、咱們與服務器的鏈接是持久的,而且從未關閉。
7.二、客戶端顯式檢查INFO命令中的runid字段以確保服務器未從新啓動而且仍然是相同的進程。
實際上,對於客戶端來講,簡單地假定在給定鏈接的上下文中,保證緩存腳本在那裏,除非管理員顯式調用SCRIPT FLUSH命令。
在管道上下文中,用戶但願Redis實例不要刪除腳本中在語義上頗有用。
(The fact that the user can count on Redis not removing scripts is semantically useful in the context of pipelining.)
例如,與Redis實例保持持久鏈接的應用程序能夠肯定的事情是,若是腳本一旦發送就永久保存在內存中,那麼EVALSHA命令能夠用於管道中的這些腳本,而不會因爲未知腳本而產生錯誤(咱們稍後會詳細看到這個問題)。
一種常見的方法是調用SCRIPT LOAD命令加載將出如今管道中的全部腳本,而後直接在管道內部使用EVALSHA命令,而不須要檢查因爲未識別腳本哈希值而致使的錯誤。
八、Script命令(The SCRIPT command)
Redis提供了一個可用於控制腳本子系統的SCRIPT命令。 SCRIPT目前接受三種不一樣的命令:
8.一、SCRIPT FLUSH(https://redis.io/commands/script-flush)
該命令是強制Redis刷新腳本緩存的惟一方法。在同一個實例能夠從新分配給不一樣用戶的雲環境中,它很是有用。測試客戶端庫的腳本功能實現也頗有用。
8.二、SCRIPT EXISTS sha1 sha2 ... shaN
給定一個SHA1摘要列表做爲參數,這個命令返回一個1或0的數組,其中1表示特定的被SHA1標識的腳本已經存在於腳本緩存中,而0表示具備該SHA1標識的腳本並無存在腳本緩存中(或者在最新的SCRIPT FLUSH命令以後至少從未見過)。
8.三、SCRIPT LOAD script
該命令將指定的腳本註冊到Redis腳本緩存中。該命令在咱們但願確保EVALSHA命令執行不會失敗的全部上下文中都頗有用(例如在管道或 MULTI/EXEC 操做期間),並不會執行腳本。
8.四、SCRIPT KILL(https://redis.io/commands/script-kill)
當腳本的執行時間達到配置的腳本最大執行時間時,此命令是中斷長時間運行的腳本的惟一方法。 SCRIPT KILL命令只能用於在執行期間沒有修改數據集的腳本(由於中止只讀腳本不會違反腳本引擎的所保證的原子性)。有關長時間運行的腳本的更多信息,請參閱下一節。
九、腳本做爲純粹的功能(Scripts as pure functions)
腳本的一個很是重要的做用是編寫純粹功能的腳本。默認狀況下,在Redis實例中執行的腳本經過發送腳本自己而不是生成的命令將其複製到Slave從節點上和AOF文件中。
緣由是將腳本發送到其餘的Redis實例一般比發送腳本生成的多個命令要快得多,所以若是客戶端發送大量腳本給Master主設備,並將這些腳本轉換爲針對 slave從節點/AOF文件相應操做的一個個的命令,將會致使複製鏈路或追加的文件的佔用太多的網絡帶寬(因爲經過網絡調度接收到的命令須要CPU作大量的工做,成功很高,相對於Redis而言,經過Lua腳本的調用來分派命令就要容易不少)。
一般狀況下,複製腳本代替腳本執行的效果是有意義的,但不是全部狀況。所以,從Redis 3.2開始,腳本引擎可以複製由腳本執行產生的寫入命令序列,而不是複製腳本自己。 有關更多信息,請參閱下一節。 在本節中,咱們假設經過發送整個腳原本複製腳本。咱們稱這種複製模式爲整個腳本複製(whole scripts replication.)。
整個腳本複製方法的主要缺點是腳本須要具備如下屬性:
腳本必須始終使用給定相同的輸入數據集的相同參數來評估相同的Redis寫入命令。 腳本執行的操做不能依賴任何隱藏的(非顯式的)信息或狀態,這些信息或狀態可能隨腳本執行的進行或因爲不一樣運行的腳本而改變,也不能依賴於來自 I/O 設備的任何外部輸入。
像使用系統時間,調用Redis隨機命令(如RANDOMKEY)或使用Lua隨機數生成器,可能會使腳本有不一樣的結果。
爲了在腳本中強制執行此行爲,Redis執行如下操做:
9.一、Lua不會導出命令來訪問系統時間或其餘外部狀態。
9.二、若是腳本調用Redis命令,Redis命令在Redis隨機命令(如RANDOMKEY,SRANDMEMBER,TIME)執行以後更改數據集,則Redis將返回錯誤並阻塞該腳本的執行。 這意味着若是腳本是隻讀的而且不會修改數據集,則能夠自由調用這些命令。請注意,隨機命令不必定意味着使用隨機數的命令:任何非肯定性命令都被視爲隨機命令(這方面的最佳示例是TIME命令)。
9.三、按照隨機順序返回元素的這些Redis命令(如SMEMBERS(由於Redis集合是無序的)),當從Lua腳本調用這些命令時會具備不一樣的行爲,在將數據返回到Lua腳本以前經歷一個的詞典排序過濾器(a silent lexicographical sorting filter)。所以,redis.call(「smembers」,KEYS [1])將始終以相同的順序返回Set元素,而從普通客戶端調用的相同命令可能會返回不一樣的結果,即便該鍵包含徹底相同的元素。
9.四、Lua僞隨機數生成函數math.random和math.randomseed被修改,以便每次執行新腳本時始終擁有相同的種子。 這意味着若是不使用 math.randomseed 函數,而僅僅使用math.random 函數,每次執行腳本時候都會生成相同的數字序列。
可是,用戶仍然可使用如下簡單的技巧編寫具備隨機行爲的命令。 想象一下,我想編寫一個Redis腳本,它將用N個隨機整數填充一個列表。
我能夠從這個小小的Ruby程序開始:
require 'rubygems' require 'redis' r = Redis.new RandomPushScript = <<EOF local i = tonumber(ARGV[1]) local res while (i > 0) do res = redis.call('lpush',KEYS[1],math.random()) i = i-1 end return res EOF r.del(:mylist) puts r.eval(RandomPushScript,[:mylist],[10,rand(2**32)])
每次執行該腳本時,結果列表都將具備如下元素:
> lrange mylist 0 -1 1) "0.74509509873814" 2) "0.87390407681181" 3) "0.36876626981831" 4) "0.6921941534114" 5) "0.7857992587545" 6) "0.57730350670279" 7) "0.87046522734243" 8) "0.09637165539729" 9) "0.74990198051087" 10) "0.17082803611217"
爲了使它成爲一個真正的隨機函數,仍然要確保每次調用腳本都會生成不一樣的隨機元素,咱們能夠簡單地添加一個額外的參數給腳本,這個參數將做爲 math.randomseed 函數的種子,而後,腳本使用 math.random 函數再生成隨機數 。 新腳本以下:
RandomPushScript = <<EOF local i = tonumber(ARGV[1]) local res math.randomseed(tonumber(ARGV[2])) while (i > 0) do res = redis.call('lpush',KEYS[1],math.random()) i = i-1 end return res EOF r.del(:mylist) puts r.eval(RandomPushScript,1,:mylist,10,rand(2**32))
咱們在這裏所作的就是將PRNG的種子做爲參數之一來發送給腳本。這樣,給定相同參數的腳本輸出將是相同的,可是咱們正在改變每次調用中的種子參數,生成隨機種子的客戶端。。做爲參數之一的種子將做在複製連接和AOF文件中的傳播,以保證在從新加載AOF或從屬進程處理腳本時將生成相同的輸出。
注意:不管運行Redis的系統的體系結構如何,Redis做爲針對PRNG實現的math.random函數和math.randomseed函數都會保證具備相同的輸出。32位,64位,大端( big-endian)和小端(little-endian)系統都會產生相同的輸出。
十、複製命令代替腳本(Replicating commands instead of scripts)
從Redis 3.2開始,能夠選擇另外一種複製方法。咱們能夠複製腳本生成的單個寫入命令,而不是複製整個腳本。咱們稱之爲【腳本影響複製】(script effects replication)。
在這種複製模式下,當執行Lua腳本時,Redis會收集由Lua腳本引擎執行的全部實際修改數據集的命令。當腳本執行完成後,由腳本生成的命令序列將被包裝到 MULTI/EXEC 事務中,併發送到從節點和進行AOF持久化保存。
根據用例,這在幾個方面頗有用:
10.一、當腳本的計算速度慢時,咱們能夠經過執行一些寫入命令來大概的瞭解這些影響,此時若是在從服務器節點上或從新加載AOF時還須要從新計算腳本,這是一件使人遺憾的事情。在這種狀況下,只複製腳本的效果要好得多。
(When the script is slow to compute, but the effects can be summarized by a few write commands, it is a shame to re-compute the script on the slaves or when reloading the AOF. In this case to replicate just the effect of the script is much better.)
10.二、當啓用【腳本影響複製】(script effects replication)時,有關一些非肯定性的功能將被開啓(非肯定行功能能夠理解爲具備隨機功能的一些命令,SPOP、SRandMember),咱們能夠大膽使用這些具備隨機(非肯定性)功能的命令。例如,您能夠在任意位置隨意使用腳本中的 TIME 或 SRANDMEMBER 命令。
(When script effects replication is enabled, the controls about non deterministic functions are disabled. You can, for example, use the TIME or SRANDMEMBER commands inside your scripts freely at any place.)
10.三、在這種模式下的Lua PRNG 每此都是隨機的調用。
(The Lua PRNG in this mode is seeded randomly at every call.)
爲了啓用腳本特效複製,您須要在腳本進行任何寫操做以前發出如下Lua命令:
redis.replicate_commands()
若是啓用【腳本影響複製】(script effects replication),則該函數返回true;不然,若是在腳本已經調用了某些寫入命令後調用該函數,則返回false,並使用正常的整個腳本複製。
十一、命令的選擇性複製(Selective replication of commands)
當選擇【腳本影響複製】(script effects replication)後(請參閱上一節),能夠更多的控制命令複製到Slave從節點和AOF的上的方式。這是一個很是高級的功能,若是濫用就會違反了在Master主節點、Slave從節點 和 AOF 上的邏輯內容必須保持一致的契約。
然而,這是一個有用的功能,由於有時候咱們只須要在主服務器上執行某些命令來建立中間值。
試想一下,在lua腳本中,有兩個sets集合執行了交集的操做。而後從結果集中選擇5個隨機元素,並用這個5個元素建立一個新的set集合。最後,咱們刪除了由在兩個原始sets集合執行交集後所獲得的結果集。咱們想要複製的只是建立具備五個元素的新集合,複製建立臨時鍵值的命令是沒有用的。
(Think at a Lua script where we perform an intersection between two sets. Pick five random elements, and create a new set with this five random elements. Finally we delete the temporary key representing the intersection between the two original sets. What we want to replicate is only the creation of the new set with the five elements. It's not useful to also replicate the commands creating the temporary key.)
所以,Redis 3.2 引入了一個新命令,該命令僅在腳本特效複製啓用時纔有效,而且可以控制腳本複製引擎。該命令稱爲redis.set_repl(),若是禁用腳本特技複製時調用,則會引起錯誤。
該命令能夠用四個不一樣的參數調用:
redis.set_repl(redis.REPL_ALL) -- 複製到 AOF 和 slave從節點。 redis.set_repl(redis.REPL_AOF) -- 僅僅複製到 AOF。 redis.set_repl(redis.REPL_SLAVE) -- 僅僅複製到slave從節點。 redis.set_repl(redis.REPL_NONE) -- 不復制。
默認狀況下,腳本引擎始終設置爲REPL_ALL。 經過調用此函數,用戶能夠打開/關閉AOF和/或從節點的複製,並稍後根據本身的意願將其恢復。
一個簡單的例子以下:
redis.replicate_commands() -- 啓用效果複製(Enable effects replication) redis.call('set','A','1') redis.set_repl(redis.REPL_NONE) redis.call('set','B','2') redis.set_repl(redis.REPL_ALL) redis.call('set','C','3')
在運行上面的腳本以後,結果是隻有A和C鍵值將在從站和AOF上建立。
十二、全局變量保護(Global variables protection)
Redis腳本不容許建立全局變量,以免用戶的狀態數據和Lua全局狀態混亂。若是腳本須要在調用之間保持狀態(很是罕見),應該使用Redis鍵。
192.168.127.130: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調試功能或其餘方法(例如更改用於實現全局保護的元表以免全局保護)並不難。 然而,意外地作到這一點很困難。 若是用戶使用Lua全局狀態混亂,AOF和複製的一致性不能保證:不要這樣作。
注意Lua新手:爲了不在lua腳本中使用全局變量,只需使用local關鍵字聲明要使用的每一個變量。
1三、在腳本中使用SELECT(Using SELECT inside scripts)
能夠像使用普通客戶端同樣在Lua腳本中調用SELECT。可是,在Redis 2.8.11和Redis 2.8.12之間有一個細微的行爲點發生了變化。在2.8.12以前發行的版本中,由Lua腳本選擇的數據庫做爲當前數據庫被傳輸到調用腳本,。從Redis 2.8.12版本開始,由Lua腳本選擇的數據庫僅影響腳本自己的執行(lua腳本選擇的數據庫只有其腳原本使用,跳出lua腳本,仍是客戶選擇的數據庫),並不會修改客戶端調用腳本或者命令選擇的數據庫,也就是說Lua腳本選擇的數據庫和客戶端選擇的數據庫是不相關的。
(It is possible to call SELECT inside Lua scripts like with normal clients, However one subtle aspect of the behavior changes between Redis 2.8.11 and Redis 2.8.12. Before the 2.8.12 release the database selected by the Lua script was transferred to the calling script as current database. Starting from Redis 2.8.12 the database selected by the Lua script only affects the execution of the script itself, but does not modify the database selected by the client calling the script.)
因爲語義的變化,發佈補丁程序來修改也是必須的,由於舊的行爲自己與Redis複製層不兼容,而且是引發錯誤的緣由。
(The semantic change between patch level releases was needed since the old behavior was inherently incompatible with the Redis replication layer and was the cause of bugs.)
1四、可用的庫(Available libraries)
EVAL命令格式:eval script numkeys key [key ...] arg [arg ...]
script:lua腳本必須是小寫字符
Redis Lua解釋器加載如下Lua庫:
一、base lib.
二、table lib.
三、string lib.
四、math lib.
五、struct lib.
六、cjson lib.
七、cmsgpack lib.
八、bitop lib.
九、redis.sha1hex function.
十、redis.breakpoint和redis.debug 函數在Redis Lua調試器的上下文中。
每一個Redis實例都保證具備上述全部庫文件,所以您能夠相信Redis腳本的環境始終如一。
struct,CJSON和cmsgpack是外部庫,全部其餘庫都是標準的Lua庫。
14.一、struct
struct是一個用於在Lua中打包/解包結構的庫。
有效的格式:
> - big endian
< - little endian
![num] - alignment
x - pading
b/B - signed/unsigned byte
h/H - signed/unsigned short
l/L - signed/unsigned long
T - size_t
i/In - signed/unsigned integer with size `n' (default is size of int)
cn - sequence of `n' chars (from/to a string); when packing, n==0 means the whole string; when unpacking, n==0 means use the previous read number as the string length
s - zero-terminated string
f - float
d - double
' '-ignored
示例以下:
192.168.127.130:6379> eval 'return struct.pack("HH", 1, 2)' 0 "\x01\x00\x02\x00" 192.168.127.130:6379> eval 'return {struct.unpack("HH", ARGV[1])}' 0 "\x01\x00\x02\x00" 1) (integer) 1 2) (integer) 2 3) (integer) 5 192.168.127.130:6379> eval 'return struct.size("HH")' 0 (integer) 4
14.二、CJSON(CJSON)
CJSON庫在Lua中提供極快的JSON操做。
示例以下:
redis 192.168.127.130:6379> eval 'return cjson.encode({["foo"]= "bar"})' 0 "{\"foo\":\"bar\"}" redis 192.168.127.130:6379> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}" "bar"
14.三、cmsgpack
cmsgpack庫在Lua中提供了簡單快速的MessagePack操做。
示例以下:
192.168.127.130:6379> eval 'return cmsgpack.pack({"foo", "bar", "baz"})' 0 "\x93\xa3foo\xa3bar\xa3baz" 192.168.127.130:6379> eval 'return cmsgpack.unpack(ARGV[1])' 0 "\x93\xa3foo\xa3bar\xa3baz" 1) "foo" 2) "bar" 3) "baz"
14.四、bitop
Lua腳本的位操做模塊在數字上添加按位操做。Redis的2.8.18版或者更高的版本均可以在腳本中使用。
示例以下:
192.168.127.130:6379> eval 'return bit.tobit(1)' 0 (integer) 1 192.168.127.130:6379> eval 'return bit.bor(1,2,4,8,16,32,64,128)' 0 (integer) 255 192.168.127.130:6379> eval 'return bit.tohex(422342)' 0 "000671c6"
它支持多種其餘功能:bit.tobit,bit.tohex,bit.bnot,bit.band,bit.bor,bit.bxor,bit.lshift,bit.rshift,bit.arshift,bit.rol,bit.ror,bit.bswap。 全部可用的功能都記錄在《Lua BitOp文檔》中。(http://bitop.luajit.org/api.html)
14.五、redis.sha1hex(sha[1 數字]hex)
獲取輸入字符串的SHA1值。
示例以下:
192.168.127.130:6379> eval 'return redis.sha1hex(ARGV [1])'0「foo」 「0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33」
1五、使用腳本寫Redis日誌(Emitting Redis logs from scripts)
可使用redis.log函數從Lua腳本寫入Redis日誌文件。
redis.log(loglevel,message)
loglevel是如下之一:
redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING
它們直接對應於正常的Redis日誌級別。只有使用等於或大於當前配置的Redis實例日誌級別的日誌級別經過腳本發出的日誌纔會被髮出。
message 參數只是一個字符串。 例:
redis.log(redis.LOG_WARNING,"Something is wrong with this script.")
將生成如下內容:
[32343] 22 Mar 15:21:39 # Something is wrong with this script.
1六、沙箱和最大執行時間(Sandbox and maximum execution time)
lua腳本絕對不該該嘗試訪問外部的系統,如對文件系統的訪問或者調用任何其餘的系統。腳本只能操做Redis上的數據並按需傳遞所需的參數。
固然,lua腳本也受最大執行時間(默認爲5秒)的限制。這個默認的超時時間能夠說有點長,由於腳本運行很快,執行時間一般在毫秒如下。之因此有這個限制主要是爲了處理在開發過程當中產生的意外死循環。
能夠經過redis.conf配置文件或者使用 CONFIG GET/CONFIG SET 命令修改lua腳本以毫秒級精度執行的最長時間。這個修改的配置參數就是 lua-time-limit,這個參數的值就是lua腳本執行最大的執行時間。
當腳本執行超時時,Redis並不會自動終止它的執行,由於這違反了Redis與腳本引擎之間的合約,以確保腳本是原子性的。中斷腳本意味着可能將數據集保留爲半寫入數據。出於這個緣由,當腳本執行超時時,會發生如下狀況:
16.一、Redis日誌記錄腳本運行時間過長。
16.二、此時若是又有客戶端再次向Redis服務器端發送了命令,服務器端則會向全部發送命令的客戶端回覆BUSY錯誤。在這種狀態下惟一容許的命令是SCRIPT KILL和SHUTDOWN NOSAVE。
16.三、可使用SCRIPT KILL命令終止一個只執行只讀命令的腳本。這不會違反腳本語義,由於腳本不會將數據寫入數據集。
16.四、若是腳本已經執行了寫入命令,則惟一容許的命令將是 SHUTDOWN NOSAVE,它會在不保存磁盤上當前數據集(基本上服務器已停止)的狀況下中止服務器。
1七、在管道上下文中EVALSHA命令(EVALSHA in the context of pipelining)
在管道請求的上下文中執行EVALSHA命令時應該當心,由於即便在管道中,命令的執行順序也必須獲得保證。若是EVALSHA命令返回一個NOSCRIPT的錯誤,則該命令不能在稍後從新執行,不然就違反了命令的執行順序。
客戶端軟件庫實現應採用如下方法之一:
17.一、在管道環境中始終使用簡單的EVAL命令。
17.二、收集全部發送到管道中的命令,而後檢查EVAL命令並使用SCRIPT EXISTS命令檢查腳本是否都已定義。若是沒有,按需求在管道頂部添加SCRIPT LOAD命令,並針對全部EVAL命令的調用換成針對EVALSHA命令的調用。
(Accumulate all the commands to send into the pipeline, then check for EVAL commands and use the SCRIPT EXISTS command to check if all the scripts are already defined. If not, add SCRIPT LOAD commands on top of the pipeline as required, and use EVALSHA for all the EVAL calls.)
1八、調試Lua腳本(Debugging Lua scripts)
從Redis 3.2開始,Redis支持原生Lua調試。Redis Lua調試器是一個遠程調試器,由一個服務器(Redis自己)和一個默認爲redis-cli的客戶端組成。
Lua調試器在Redis文檔的Lua腳本調試章節中進行了詳細的描述。
相關命令
EVAL
EVALSHA
SCRIPT DEBUG
SCRIPT EXISTS
SCRIPT FLUSH
SCRIPT KILL
SCRIPT LOAD
4、總結
今天就寫到這裏了,因爲這篇文章的內容比較多,翻譯起來也比較費時間,因此就比較慢了,同時我也須要消化一下,而後才能寫出來。暫時來講,有關Redis的相關知識就到此爲止了,若是之後有了新的東西,再補充進來。下一步開始寫一些關於文件型數據庫MongoDB的文章。對了,若是你們想看英文原文,能夠點擊《這裏》。
天下國家,可均也;爵祿,可辭也;白刃,可蹈也;中庸不可能也