Redis進階實踐之十九 Redis如何使用lua腳本

Redis進階實踐之十九 Redis如何使用lua腳本

一、引言

               redis學了一段時間了,基本的東西都沒問題了。從今天開始講寫一些redis和lua腳本的相關的東西,lua這個腳本是一個好東西,可以運行在任何平臺上,也可以嵌入到大多數語言當中,來擴展其功能。lua腳本是用C語言寫的,體積很小,運行速度很快,並且每次的執行都是作爲一個原子事務來執行的,我們可以在其中做很多的事情。由於篇幅很多,一次無法概述全部,這個系列可能要通過多篇文章的形式來寫,好了,今天我們進入正題吧。

二、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是作爲嵌入式腳本的最佳選擇。


三、EVAL命令的詳解


             1、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],...)。

                      以下示例應該闡明上述內容:
 

> 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命令和命令所需要的參數:
 

> 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.)


             2、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 轉換對應表.
 

複製代碼

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.1、Lua腳本有一個數字類型,Lua數字。 整數和浮點數是沒有區別。因此我們總是將Lua數字轉換爲整數回覆,如果有的小數的話,會刪除數字的小數部分。如果你想從Lua腳本中返回一個浮點數,你應該像字符串一樣返回它,就像Redis自己做的那樣(參見例如 ZSCORE (https://redis.io/commands/zscore)命令)。

                         2.2、沒有簡單的方法在Lua數組中包含有nil(www.lua.org/pil/19.1.html),這是Lua表語義決定的,所以當Redis將Lua數組轉換爲Redis協議類型時,如果遇到nil,轉換就會停止。

                     以下是幾個轉換示例:

複製代碼

> 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的浮點數和數組:

> 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 字符串永遠不會被返回。


           3、Helper函數返回Redis類型(Helper functions to return Redis types)

                       有兩個幫助函數可以從Lua腳本返回Redis類型。

                            3.1、redis.error_reply(error_string)返回錯誤回覆。這個函數只是返回一個字段表,其中err字段特殊指定的字符串。

                            3.2、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")


               4、腳本的原子性(Atomicity of scripts)

                       Redis使用相同的Lua解釋器來運行所有命令。另外,Redis保證以原子方式執行腳本:執行腳本時不會執行其他腳本或Redis命令。與 MULTI/EXEC 事務的概念相似。從所有其他客戶端的角度來看,腳本要不已經執行完成,要不根本不執行。

                       然而運行一個緩慢的腳本就是一個很愚蠢的主意。創建快速執行的腳本並不難,因爲腳本開銷非常低。但是,如果您要使用了執行緩慢的腳本,由於其的原子性,其他客戶端的命令都是得不到執行的,這並不是我們想要的結果,大家要切記。


               5、錯誤處理(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() 返回的錯誤對象將確切的錯誤傳遞給用戶。

               6、帶寬和EVALSHA(Bandwidth and EVALSHA)

                        EVAL命令強制您一次又一次發送腳本正文。 Redis不需要每次重新編譯腳本,因爲它使用內部緩存機制,但是在許多情況下,大量的多次的發送腳本正文佔用了額外帶寬的,這個成本也是不容忽視的。

                       另一方面,使用特殊命令或通過redis.conf定義命令也會有相應的問題,原因如下:

                           6.1、不同的實例可能有不同的命令實現。

                           6.2、如果我們必須確保所有實例都包含給定命令,特別是在分佈式環境中,則部署非常困難。

                           6.3、閱讀應用程序代碼,完整的語義可能並不是十分清晰明瞭,因爲應用程序調用的命令都是定義在服務器端的。

                      爲避免這些問題,同時避免帶寬損失,Redis實現了EVALSHA命令。

                      EVALSHA的工作方式與EVAL完全相同,但不是將腳本作爲第一個參數,而是使用腳本的SHA1摘要。 行爲如下:

                           6.1、如果服務器仍然記住具有匹配的SHA1摘要的腳本,則執行該腳本。

                           6.2、如果服務器不記得具有此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高效緩存。
                       

               7、腳本緩存語義(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.1、我們與服務器的連接是持久的,並且從未關閉。

                             7.2、客戶端顯式檢查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命令,而不需要檢查由於未識別腳本哈希值而導致的錯誤。


               8、Script命令(The SCRIPT command)

                       Redis提供了一個可用於控制腳本子系統的SCRIPT命令。 SCRIPT目前接受三種不同的命令:

                       8.1、SCRIPT FLUSH(https://redis.io/commands/script-flush)
                          
                             該命令是強制Redis刷新腳本緩存的唯一方法。在同一個實例可以重新分配給不同用戶的雲環境中,它非常有用。測試客戶端庫的腳本功能實現也很有用。

                       8.2、SCRIPT EXISTS sha1 sha2 ... shaN

                             給定一個SHA1摘要列表作爲參數,這個命令返回一個1或0的數組,其中1表示特定的被SHA1標識的腳本已經存在於腳本緩存中,而0表示具有該SHA1標識的腳本並沒有存在腳本緩存中(或者在最新的SCRIPT FLUSH命令之後至少從未見過)。

                       8.3、SCRIPT LOAD script

                             該命令將指定的腳本註冊到Redis腳本緩存中。該命令在我們希望確保EVALSHA命令執行不會失敗的所有上下文中都很有用(例如在管道或 MULTI/EXEC 操作期間),並不會執行腳本。

                       8.4、SCRIPT KILL(https://redis.io/commands/script-kill)

                        當腳本的執行時間達到配置的腳本最大執行時間時,此命令是中斷長時間運行的腳本的唯一方法。 SCRIPT KILL命令只能用於在執行期間沒有修改數據集的腳本(因爲停止只讀腳本不會違反腳本引擎的所保證的原子性)。有關長時間運行的腳本的更多信息,請參閱下一節。


               9、腳本作爲純粹的功能(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.1、Lua不會導出命令來訪問系統時間或其他外部狀態。

                               9.2、如果腳本調用Redis命令,Redis命令在Redis隨機命令(如RANDOMKEY,SRANDMEMBER,TIME)執行之後更改數據集,則Redis將返回錯誤並阻塞該腳本的執行。 這意味着如果腳本是隻讀的並且不會修改數據集,則可以自由調用這些命令。請注意,隨機命令不一定意味着使用隨機數的命令:任何非確定性命令都被視爲隨機命令(這方面的最佳示例是TIME命令)。

                               9.3、按照隨機順序返回元素的這些Redis命令(如SMEMBERS(因爲Redis集合是無序的)),當從Lua腳本調用這些命令時會具有不同的行爲,在將數據返回到Lua腳本之前經歷一個的詞典排序過濾器(a silent lexicographical sorting filter)。因此,redis.call(“smembers”,KEYS [1])將始終以相同的順序返回Set元素,而從普通客戶端調用的相同命令可能會返回不同的結果,即使該鍵包含完全相同的元素。

                               9.4、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)系統都會產生相同的輸出。
                         

               10、複製命令代替腳本(Replicating commands instead of scripts)

                         從Redis 3.2開始,可以選擇另一種複製方法。我們可以複製腳本生成的單個寫入命令,而不是複製整個腳本。我們稱之爲【腳本影響複製】(script effects replication)。

                         在這種複製模式下,當執行Lua腳本時,Redis會收集由Lua腳本引擎執行的所有實際修改數據集的命令。當腳本執行完成後,由腳本生成的命令序列將被包裝到 MULTI/EXEC 事務中,併發送到從節點和進行AOF持久化保存。

                         根據用例,這在幾個方面很有用:

                              10.1、當腳本的計算速度慢時,我們可以通過執行一些寫入命令來大概的瞭解這些影響,此時如果在從服務器節點上或重新加載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.2、當啓用【腳本影響複製】(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.3、在這種模式下的Lua PRNG 每此都是隨機的調用。
                             (The Lua PRNG in this mode is seeded randomly at every call.)


                         爲了啓用腳本特效複製,您需要在腳本進行任何寫操作之前發出以下Lua命令:

redis.replicate_commands()


                         如果啓用【腳本影響複製】(script effects replication),則該函數返回true;否則,如果在腳本已經調用了某些寫入命令後調用該函數,則返回false,並使用正常的整個腳本複製。



               11、命令的選擇性複製(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上創建。


               12、全局變量保護(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關鍵字聲明要使用的每個變量。
 

               13、在腳本中使用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.)


               14、可用的庫(Available libraries)

                        EVAL命令格式:eval script numkeys key [key ...] arg [arg ...]

                                script:lua腳本必須是小寫字符

                        Redis Lua解釋器加載以下Lua庫:

                          1、base lib.
                          2、table lib.
                          3、string lib.
                          4、math lib.
                          5、struct lib.
                          6、cjson lib.
                          7、cmsgpack lib.
                          8、bitop lib.
                          9、redis.sha1hex function.
                         10、redis.breakpoint和redis.debug 函數在Redis Lua調試器的上下文中。

                        每個Redis實例都保證具有上述所有庫文件,因此您可以相信Redis腳本的環境始終如一。

                        struct,CJSON和cmsgpack是外部庫,所有其他庫都是標準的Lua庫。

                        14.1、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.2、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.3、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.4、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.5、redis.sha1hex(sha[1 數字]hex)

                              獲取輸入字符串的SHA1值。

                              示例如下:

192.168.127.130:6379> eval 'return redis.sha1hex(ARGV [1])'0“foo”
                                “0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33”



               15、使用腳本寫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.


               16、沙箱和最大執行時間(Sandbox and maximum execution time)
                    
                        lua腳本絕對不應該嘗試訪問外部的系統,如對文件系統的訪問或者調用任何其他的系統。腳本只能操作Redis上的數據並按需傳遞所需的參數。

                        當然,lua腳本也受最大執行時間(默認爲5秒)的限制。這個默認的超時時間可以說有點長,因爲腳本運行很快,執行時間通常在毫秒以下。之所以有這個限制主要是爲了處理在開發過程中產生的意外死循環。

                        可以通過redis.conf配置文件或者使用 CONFIG GET/CONFIG SET 命令修改lua腳本以毫秒級精度執行的最長時間。這個修改的配置參數就是 lua-time-limit,這個參數的值就是lua腳本執行最大的執行時間。

                        當腳本執行超時時,Redis並不會自動終止它的執行,因爲這違反了Redis與腳本引擎之間的合約,以確保腳本是原子性的。中斷腳本意味着可能將數據集保留爲半寫入數據。出於這個原因,當腳本執行超時時,會發生以下情況:

                            16.1、Redis日誌記錄腳本運行時間過長。

                            16.2、此時如果又有客戶端再次向Redis服務器端發送了命令,服務器端則會向所有發送命令的客戶端回覆BUSY錯誤。在這種狀態下唯一允許的命令是SCRIPT KILL和SHUTDOWN NOSAVE。

                            16.3、可以使用SCRIPT KILL命令終止一個只執行只讀命令的腳本。這不會違反腳本語義,因爲腳本不會將數據寫入數據集。

                            16.4、如果腳本已經執行了寫入命令,則唯一允許的命令將是 SHUTDOWN NOSAVE,它會在不保存磁盤上當前數據集(基本上服務器已中止)的情況下停止服務器。


               17、在管道上下文中EVALSHA命令(EVALSHA in the context of pipelining)

                   
                       在管道請求的上下文中執行EVALSHA命令時應該小心,因爲即使在管道中,命令的執行順序也必須得到保證。如果EVALSHA命令返回一個NOSCRIPT的錯誤,則該命令不能在稍後重新執行,否則就違反了命令的執行順序。

                       客戶端軟件庫實現應採用以下方法之一:

                          17.1、在管道環境中始終使用簡單的EVAL命令。

                          17.2、收集所有發送到管道中的命令,然後檢查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.)



               18、調試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

        
四、總結

           今天就寫到這裏了,由於這篇文章的內容比較多,翻譯起來也比較費時間,所以就比較慢了,同時我也需要消化一下,然後才能寫出來。暫時來說,有關Redis的相關知識就到此爲止了,如果以後有了新的東西,再補充進來。下一步開始寫一些關於文件型數據庫MongoDB的文章。對了,如果大家想看英文原文,可以點擊《這裏》。

天下國家,可均也;爵祿,可辭也;白刃,可蹈也;中庸不可能也

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