redis翻譯_redislua腳本
Introduction to EVAL 介紹EVAL
EVAL and EVALSHA are used to evaluate scripts using the Lua interpreter built into Redis starting from version 2.6.0.
EVAL和EVALSHA是從Redis2.6.0版本使用內置腳本解釋器引入的。
The first argument of EVAL is a Lua 5.1 script. The script does not need to define a Lua function (and should not). It is just a Lua program that will run in the context of the Redis server.
EVAL的第一個參數是一個lua.5.1的腳本.這段腳本不需要定義lua方法函數(也不應該定義)。僅僅是一個運行在Redis 服務器的一段lua程序。
The second argument of EVAL is the number of arguments that follows the script (starting from the third argument) that represent Redis key names. This arguments can be accessed by Lua using the KEYS global variable in the form of a one-based array (so KEYS[1], KEYS[2], ...).
EVAL的第二個參數是一個數字,它表示緊跟著的腳本(第三個參數開始)中前多少個是Redis中的key的名稱。這些Redis中的key的名稱可以使用lua的數組 KEYS取出value(比如 KEYS[1],KEUS[2],...)。
All the additional arguments should not represent key names and can be accessed by Lua using the ARGV global variable, very similarly to what happens with keys (soARGV[1], ARGV[2], ...).
附加參數不代表key的名稱並且和keys一樣可以使用lua的ARGV全局變量數組訪問(比如:ARGV[1],ARGV[2])。
The following example should clarify what stated above:
下面的例子應該可以說明上面的規定:
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
Note: as you can see Lua arrays are returned as Redis multi bulk replies, that is a Redis return type that your client library will likely convert into an Array type in your programming language.
注意:正如你所見,lua數組的返回是Redis的多重應答,這是redis的一個返回類型,你的客戶端可能會轉換成所用程序語言的數組。
It is possible to call Redis commands from a Lua script using two different Lua functions:
使用兩個不同的lua函數去調用Redis命令:
redis.call()
redis.pcall() redis.call() is similar to redis.pcall(), the only difference is that if a Redis command call will result in an error, redis.call() will raise a Lua error that in turn will force EVAL to return an error to the command caller, while redis.pcall will trap the error and return a Lua table representing the error.
redis.call()和redis.pcall()類似,唯一的不同是,如果命令返回一個錯誤結束時,redis.call()會將這個錯誤提升到lua的error,讓EVAL返回一個lua錯誤給調用者,redis.pcall()將捕獲這個錯誤並且返回一個代表錯誤的 Lua table. The arguments of the redis.call() and redis.pcall() functions are all the arguments of a well formed Redis command:
redis.call()和redis.pcall()的參數可以是Redis所有命令的參數:
> eval "return redis.call('set','foo','bar')" 0
OK
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 Cluster is a work in progress, but the scripting feature was designed in order to play well with it).
所有的命令在執行之前都是先被解析的,以便確定操作在那些keys上。正確使用EVAL,key必須被明確定傳遞。在很多時候,像上面的寫法是沒有錯的,但是特別注意,在redis集群中明確傳遞的key可以確保找到正確節點,上面的寫法就不行(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.
注意上面這條不強制規則,並不是使用戶造成濫用redis實例,編寫腳本不支持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腳本的返回一個lua類型,根據redis協議轉換規則轉換的redis類型的值。
Conversion between Lua and Redis data types redis類型和lua類型的轉換
Redis return values are converted into Lua data types when Lua calls a Redis command using call() or pcall(). Similarly Lua data types are converted into the Redis protocol when a Lua script returns a value, so that scripts can control what EVAL will return to the client.
當lua調用call()或者pcall()函數時,redis的返回類型被轉換成lua的數據類型。同樣的當lua腳本返回值時,lua數據類型被轉換成redis數據類型,因此腳本可以控制EVAL的返回。 This conversion between data types is designed in a way that if a Redis type is converted into a Lua type, and then the result is converted back into a Redis type, the result is the same as the initial value.
兩種數據類型的轉換,被設計成,如果從Redis類型轉換成Lua類型,然後再從lua類型轉換成redis類型,最後得到的結果和最初的值時一樣的。 In other words there is a one-to-one conversion between Lua and Redis types. The following table shows you all the conversions rules:
換句話說,Lua類型和Redis類型中,這是一個一一對應的轉換。下面這個表就是轉換規則:
Redis to Lua conversion table.Redis 類型轉換成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 conversion table.lua類型轉換成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. There is an additional Lua-to-Redis conversion rule that has no corresponding Redis to Lua conversion rule:
還有一個Lua轉換成Redis的附加規則,這條規則沒有對應的Redis轉換成Lua:
Lua boolean true -> Redis integer reply with value of 1. Also there are two important rules to note:並且這裡有兩個重要的規則需要注意:
Lua has a single numerical type, Lua numbers. There is no distinction between integers and floats. So we always convert Lua numbers into integer replies, removing the decimal part of the number if any. If you want to return a float from Lua you should return it as a string, exactly like Redis itself does (see for instance the ZSCORE command). Lua有一個數值類型,number。它沒有整型和浮點型的區別。因此我們總是將Lua的numbers類型轉換成integer replies,如果有小數就刪除小數部分。如果你想返回一個float,那麼你應該把它作為一個string類型返回,就像Redis自己的做法(參照ZSCORE命令用法的例子)
There is no simple way to have nils inside Lua arrays, this is a result of Lua table semantics, so when Redis converts a Lua array into Redis protocol the conversion is stopped if a nil is encountered. 在Lua中數組中沒有nil值得表示方式,因為Lua中數組是一個table的含義,因此當Redis轉換成Lua 數組遇到nil值就停止了(後面的也不轉換了)。 Here are a few conversion examples: 看幾個例子
> 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"
The last example shows how it is possible to receive the exact return value ofredis.call() or redis.pcall() from Lua that would be returned if the command was called directly.
最後一個例子是展示使用redis.call()或者redis.pcall()函數直接調命令,結果被直接從lua類型強制轉換成Redis類型。 In the following example we can see how floats and arrays with nils are handled:
下面例子說float和數組有nil值得處理:
> eval "return {1,2,3.3333,'foo',nil,'bar'}" 0
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) "foo"
As you can see 3.333 is converted into 3, and the bar string is never returned as there is a nil before.
可以看見3.333轉換成3,因為nil的原因string類型的bar沒返回。
Helper functions to return Redis types 返回redis類型的幫助函數
There are two helper functions to return Redis types from Lua.
有兩個從lua腳本返回Redis類型的幫助函數.
redis.error_reply(error_string) returns an error reply. This function simply returns the single field table with the err field set to the specified string for you. redis.error_reply(error_string) 返回一個錯誤應答.這個函數可以簡單地返回你設置提示的 err字段表。
redis.status_reply(status_string) returns a status reply. This function simply returns the single field table with the ok field set to the specified string for you. redis.status_reply(status_string) 返回一個狀態應答。這個函數可以簡單地返回你設置提示的ok字段表。 (下面是譯者執行的例子)
Atomicity of scripts 腳本的原子性
Redis uses the same Lua interpreter to run all the commands. Also Redis guarantees that a script is executed in an atomic way: no other script or Redis command will be executed while a script is being executed. This semantic is similar to the one ofMULTI / EXEC. From the point of view of all the other clients the effects of a script are either still not visible or already completed.
Redis使用相同的Lua解釋器去運行所有的命令。並且保證腳本的實現是原子性的:當一個腳本正在執行的時候沒有其他的腳本或者命令可以被執行。意思和命令 MULTI/EXEC 相似(MULTI/EXEX是Redis事務)。需要注意的是,從其他客戶端的角度來看,腳本的影響是:其他客戶端不能訪問redis或者已經執行完畢(也就是其他客戶端不能執行命令或者腳本)。 However this also means that executing slow scripts is not a good idea. It is not hard to create fast scripts, as the script overhead is very low, but if you are going to use slow scripts you should be aware that while the script is running no other client can execute commands.
然而這也是說,執行一個慢的腳本非常不好。不能快速創建腳本,因為它非常慢,如果你執行這個非常慢的腳本就該意識到,這個腳本在執行的時候其他客戶端是不能執行命令的。
Error handling 錯誤處理
As already stated, calls to redis.call() resulting in a Redis command error will stop the execution of the script and return an error, in a way that makes it obvious that the error was generated by a script:
前面已經提到,調用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_6b1bf486c81ceb7edf3c093f4c48582e3
Using redis.pcall() no error is raised, but an error object is returned in the format specified above (as a Lua table with an err field). The script can pass the exact error to the user by returning the error object returned by redis.pcall().
使用函數redis.pcall()沒有腳本錯誤,它僅僅是被指定格式上的這個錯誤對象被返回(作為Lua 表的err字段)。通過調用redis.pcall()返回得到的錯誤對象可以被用戶使用腳本精確定義. (例如) 127.0.0.1:6379> eval "return redis.pcall('get','foo')" 0 (error) WRONGTYPE Operation against a key holding the wrong kind of value
Bandwidth and EVALSHA 帶寬和EVALSHA
The EVAL command forces you to send the script body again and again. Redis does not need to recompile the script every time as it uses an internal caching mechanism, however paying the cost of the additional bandwidth may not be optimal in many contexts.
eval命令可以讓你重復發送相同腳本。redis內部的緩存機制不需要每次都去重新編譯相同的腳本,但是很多時候腳本附帶的帶寬開銷可能不是最好的。 On the other hand, defining commands using a special command or via redis.confwould be a problem for a few reasons:
另一方面,用特殊命令或者通過redis.conf方式定義EVAL腳本命令,因為下面幾個原因會有問題:
Different instances may have different implementations of a command.
一個命令在不同實例可能有不同的實現
Deployment is hard if we have to make sure all instances contain a given command, especially in a distributed environment.
使得所有的實例都包含給出的命令部署是很困難的,特別是在分布式系統中。
Reading application code, the complete semantics might not be clear since the application calls commands defined server side.
閱讀應用代碼時,因為應用調用的命令在服務器這邊所以整個語義不清晰。 In order to avoid these problems while avoiding the bandwidth penalty, Redis implements the EVALSHA command.
為了避開帶寬問題和上面的問題,redis實現了EVALSHA命令。 EVALSHA works exactly like EVAL, but instead of having a script as the first argument it has the SHA1 digest of a script. The behavior is the following:
EVALSHA酷似EVAL,但它不是以一個腳本作為第一個參數,它的第一個參數腳本是SHA1摘要。它的行為如下:
If the server still remembers a script with a matching SHA1 digest, the script is executed.
如果服務器記得這個腳本的SHA1摘要,腳本就會被執行。
If the server does not remember a script with this SHA1 digest, a special error is returned telling the client to use EVAL instead.
如果服務器不記得這個腳本的SHA1摘要,將返回一個提示使用EVAL代替的錯誤。 Example:例如
> 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).
The client library implementation can always optimistically send EVALSHA under the hood even when the client actually calls EVAL, in the hope the script was already seen by the server. If the NOSCRIPT error is returned EVAL will be used instea
客戶端可以樂觀地這樣實現,在發生EVAL命令前可以認為EVAL腳本已經被服務器知道了,所以發送EVALSHA。如果返回NOSCRIPT 的錯誤在使用EVAL命令。 Passing keys and arguments as additional EVAL arguments is also very useful in this context as the script string remains constant and can be efficiently cached by Redis.
因為腳本字符串保持不變,並且可以通過Redis有效緩存起來,那麼通EVALSHA發EVAL的key參數和附加參數也是非常有用的。
Script cache semantics 緩存腳本的意義
Executed scripts are guaranteed to be in the script cache of a given execution of a Redis instance forever. This means that if an EVAL is performed against a Redis instance all the subsequent EVALSHA calls will succeed.
執行的腳本都保證在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.
腳本緩存很長的時間是不太可能的,因為一個好的寫應用有很多不同的腳本,腳本多了就會引起內存問題。每一個腳本的概念其實就是實現幾個命令,甚至一個大的應用可能一個腳本有幾百個命令。盡管應用有時會修改並且腳本會改變,但是減少的內存是微不足道的。 The only way to flush the script cache is by explicitly calling the SCRIPT FLUSHcommand, which will
completely flush the scripts cache removing all the scripts executed so far.
唯一清除腳本緩存的方法是明確地調用SCRIPT FLUSH命令,這個命令將完全刪除到目前為止所有緩存的腳本。 This is usually needed only when the instance is going to be instantiated for another customer or application in a cloud environment.
在一個實例被用在在集群中或者在雲環境中,初始化時通常都需要這樣做。 Also, as already mentioned, restarting a Redis instance flushes the script cache, which is not persistent. However from the point of view of the client there are only two ways to make sure a Redis instance was not restarted between two different commands.
另外,如前所述,重啟Redis將清除scipt,因為它不是持久化的。對應客戶端來說,有兩種方式使用兩個命令可以確保Redis沒有重啟。
The connection we have with the server is persistent and was never closed so far. 客戶端到目前為止一直和服務器連接著。
The client explicitly checks the runid field in the INFO command in order to make sure the server was not restarted and is still the same process. 執行INFO命令檢查runid確保服務器沒有重啟並且還是同一個進程。 Practically speaking, for the client it is much better to simply assume that in the context of a given connection, cached scripts are guaranteed to be there unless an administrator explicitly called the SCRIPT FLUSH command.
具體來說,為一個一直連接服務器的客戶端做一個假設是比較好的,這個假設就是這個連接緩存的腳本一直都在服務器緩存著的,除非有服務器管理權限的人執行 SCRIPT FLUSH命令。 The fact that the user can count on Redis not removing scripts is semantically useful in the context of pipelining.
在使用管道的情況下,用戶對沒有被刪除的腳本進行計數是很有用的。
For instance an application with a persistent connection to Redis can be sure that if a script was sent once it is still in memory, so EVALSHA can be used against those scripts in a pipeline without the chance of an error being generated due to an unknown script (we'll see this problem in detail later).
例如,一個一直連接Redis的應用就能確保一個腳本發生一次之後就一直在reids內存中,因此EVALSHA可以防備那些在管道裡面的腳本,使之沒有機會產生未知腳錯誤(稍後將看到這個問題)。 A common pattern is to call SCRIPT LOAD to load all the scripts that will appear in a pipeline, then use EVALSHA directly inside the pipeline without any need to check for errors resulting from the script hash not being recognized.
一個普遍的做法是調用SCRIPT LOAD去加載在一個管道裡出現所有的腳本,然後直接在管道裡使用EVALSHA,而不用去檢測腳本沒有被認可產生的錯誤。
The SCRIPT command 腳本命令
Redis offers a SCRIPT command that can be used in order to control the scripting subsystem. SCRIPT currently accepts three different commands:
Redis為了去控制腳本子系統提供SCRIPT命令。SCRIPT通常接收三個不同的命令:
SCRIPT FLUSH. This command is the only way to force Redis to flush the scripts cache. It is most useful in a cloud environment where the same instance can be reassigned to a different user. It is also useful for testing client libraries' implementations of the scripting feature.
SRIPUT FLUSH。這個命令時使Redis清空腳本緩存的唯一方式。在雲環境中當同一個實例被重新指定給不同的用戶時這個命令是最有用的。在客戶端測試腳本的實現時也非常有用。
SCRIPT EXISTS sha1 sha2... shaN. Given a list of SHA1 digests as arguments this command returns an array of 1 or 0, where 1 means the specific SHA1 is recognized as a script already present in the scripting cache, while 0 means that a script with this SHA1 was never seen before (or at least never seen after the latest SCRIPT FLUSH command).
SCRIPT EXISTS sha1 sha2...shaN。命令的參數是SHA1摘要的列表,它將返回一個由0或者1組成的數組,1表示對應的腳本已經被服務器認可放入腳本緩存了,0表對應的腳本服務器一直都沒有見過(或者至少在最後一次執行SCRIPT FLUSH後從來沒有見過)
SCRIPT LOAD script. This command registers the specified script in the Redis script cache. The command is useful in all the contexts where we want to make sure that EVALSHA will not fail (for instance during a pipeline or MULTI/EXEC operation), without the need to actually execute the script.
SCRIPT LOAD script. 這個命令是在Redis腳本緩存中緩存指定的腳本。這個命令在我們想確保EVALSHA不管在任何環境下都不會失敗時非常有用(比如在管道或者 MULTI/EXEC 操作時),並且加載時它不會去執行腳本。
SCRIPT KILL. This command is the only way to interrupt a long-running script that reaches the configured maximum execution time for scripts. The SCRIPT KILL command can only be used with scripts that did not modify the dataset during their execution (since stopping a read-only script does not violate the scripting engine's guaranteed atomicity). See the next sections for more information about long running scripts.
SCRIPT KILL.這個命令是去中斷正在運行並且運行時間達到配置最大時間的腳本的唯一方式。這個命令只能使用於腳本執行不修改數據的情況(因為停止腳本不能違反腳本引擎的原子性)。下一小節將看到關於長時間運行腳本的更多信息。
Scripts as pure functions 腳本作為純粹的方法
A very important part of scripting is writing scripts that are pure functions. Scripts executed in a Redis instance are replicated on slaves by sending the script -- not the resulting commands. The same happens for the Append Only File. The reason is that sending a script to another Redis instance is much faster than sending the multiple commands the script generates, so if the client is sending many scripts to the master, converting the scripts into individual commands for the slave / AOF would result in too much bandwidth for the replication link or the Append Only File (and also too much CPU since dispatching a command received via network is a lot more work for Redis compared to dispatching a command invoked by Lua scripts).
腳本一個非常重要的部分是作為一個純粹的方法寫腳本。腳本運行在Redis實例上可以通過發送該腳本使得該腳本可以 被 slaves 復制--作為沒有返回的命令.和添加只讀文件是一樣的。原因是發送一個腳本到其他Redis實例比發送幾個命令使之產生腳本快,所以如果客戶端發送一些腳本到master上,使腳本為 slave/SOF 轉換成單個的命令的結果是為應答或者增加文件花費更多的帶寬(而且Redis一個命令通過網絡接收調用與使用腳本比較要消耗更多的CPU)。
The only drawback with this approach is that scripts are required to have the following property:
唯一的缺點是,使用的腳本必須有下面的特點:
The script always evaluates the same Redis write commands with the same arguments given the same input data set. Operations performed by the script cannot depend on any hidden (non-explicit) information or state that may change as script execution proceeds or between different executions of the script, nor can it depend on any external input from I/O devices. 腳本的執行結果總是與給出相同參數的命令的執行結果相同。腳本操作不能依賴隱藏的信息或者可變的狀態作為腳本執行的結果或者不同的執行過程,也不能依賴與使用I/O設備的輸入信息。 Things like using the system time, calling Redis random commands like RANDOMKEY, or using Lua random number generator, could result into scripts that will not always evaluate in the same way.
就像使用系統時間,使用Redis 隨機命令 如RANDOMKEY,或者使用Lua隨機數觸發器,都使得同樣的腳本有不同的結果。 In order to enforce this behavior in scripts Redis does the following:
為了強制這個做法,腳本應該這樣做:
Lua does not export commands to access the system time or other external state.
沒有導出命令來訪問系統時間或其他外部狀態。
Redis will block the script with an error if a script calls a Redis command able to alter the data set after a Redis random command like RANDOMKEY,SRANDMEMBER, TIME. This means that if a script is read-only and does not modify the data set it is free to call those commands. Note that a random command does not necessarily mean a command that uses random numbers: any non-deterministic command is considered a random command (the best example in this regard is the TIME command).
修改數據的腳本應該避免調用隨機設置數據的命令,比如RANDOMKEY,SRANDMEMBER,TIME。反過來也就是說,如果一個腳本是只讀的,不修改數據設置的,它就可以自由地調用這些命令。注意這裡說的隨機命令並不只是只那些隨機數字的命令:任何非確定性的命令都被認為是一個隨機命令(最好的例子就是TIME命令)。
Redis commands that may return elements in random order, like SMEMBERS(because Redis Sets are unordered) have a different behavior when called from Lua, and undergo a silent lexicographical sorting filter before returning data to Lua scripts. So redis.call("smembers",KEYS[1]) will always return the Set elements in the same order, while the same command invoked from normal clients may return different results even if the key contains exactly the same elements.
隨機返回元素的命令,像SMEMBERS(因為Set是無序的)被Lua腳本調用時會有不同的結果,並且返回要經過lua過濾排序。因此 redis.call("smembers",KEY[1]) 總是以相同的順序返回元素,當同一個命令客戶端調用時將返回不同的結果,盡管key包含完全相同的元素。
Lua pseudo random number generation functions math.random andmath.randomseed are modified in order to always have the same seed every time a new script is executed. This means that calling math.random will always generate the same sequence of numbers every time a script is executed if math.randomseedis not used.
為了每次調用lua的math.random和math.randomseed方法都是一個相同的隨機種子,Redis對math.random和mathrandomseed被進行了修正。意思是如果math.randomseed沒有被調用,每次調用math.random都生成相同的數字序列。
However the user is still able to write commands with random behavior using the following simple trick. Imagine I want to write a Redis script that will populate a list with N random integers.
但是,使用以下簡單的技巧,用戶仍然能夠編寫與隨機行為的相關的命令。想象一下,我想寫一個Redis的腳本,將N個隨機整數填充一個list。
I can start with this small Ruby program: 我可以用這個小Ruby程序啟動
require 'rubygems'
require 'redis'
r = Redis.new
RandomPushScript = < 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)])
Every time this script executed the resulting list will have exactly the following elements:
每次執行這個腳本列表將會有確切的以下要素:
> 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"
In order to make it a pure function, but still be sure that every invocation of the script will result in different random elements, we can simply add an additional argument to the script that will be used in order to seed the Lua pseudo-random number generator. The new script is as follows:
為了使腳本成為一個純粹的函數,但仍然確保每一次調用有不同的隨機元素,我們可以給腳本添加額外的參數作為Lua偽隨機數字生成器的種子。修改的腳本如下:
RandomPushScript = < 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))
What we are doing here is sending the seed of the PRNG as one of the arguments. This way the script output will be the same given the same arguments, but we are changing
one of the arguments in every invocation, generating the random seed client-side. The
seed will be propagated as one of the arguments both in the replication link and in the
Append Only File, guaranteeing that the same changes will be generated when the AOF is reloaded or when the slave processes the script.
我們這裡做的是發送PRNG的種子作為參數之一,這種方式給定相同的參數輸出也將相同,但是每次調用時我都可以在客戶端這邊改變隨機種子。當AOF重載或者從服務器處理腳本時,種子作為復制鏈接和附加文檔的參數傳輸,保證生成相同的變化。
Note: an important part of this behavior is that the PRNG that Redis implements asmath.random and math.randomseed is guaranteed to have the same output regardless of the architecture of the system running Redis. 32-bit, 64-bit, big-endian and
little-endian systems will all produce the same output.
注意:上面的做法最重要的是Redis 的PRNG實現 math.random和math.randomseed能保證Redis運行底層系統都有相同的輸出。32-bit, 64-bit, big-endian 和little-endian系統都將產生相同的輸出。
Global variables protection 全局變量的保護
Redis scripts are not allowed to create global variables, in order to avoid leaking data into the Lua state. If a script needs to maintain state between calls (a pretty uncommon need) it should use Redis keys instead.
為了Lua狀態避免數據洩露,Redis腳本不允許創建全局變量。如果腳本調用過程需要維護全局狀態變量應該使用Redis 的key代替。 When global variable access is attempted the script is terminated and EVAL returns with an error:
當企圖去使用全局變量時,腳本將被終止,並且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'
Accessing a non existing global variable generates a similar error. 使用不存在的全局變量的錯誤。
Using Lua debugging functionality or other approaches like altering the meta table used to implement global protections in order to circumvent globals protection is not hard. However it is difficult to do it accidentally. If the user messes with the Lua global state, the consistency of AOF and replication is not guaranteed: don't do it.
使用Lua的調試功能或其他方法,如改變使用的元表以規避全局保護並不難。但是很難做到不小心。如果用戶與Lua全局狀態混亂, AOF和復制的一致性是沒有保證的:不要這樣做。 Note for Lua newbies: in order to avoid using global variables in your scripts simply declare every variable you are going to use using the
local keyword.
lua新手要注意:為了避免使用全局變量,聲明變量時應該明確使用local關鍵字。
Using SELECT inside scripts在腳本中使用SELECT
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.
在客戶端的腳本中使用 SELECT就像一般的客戶端一樣,只有在 2.8.11和2.8.12有一點變化。在2.8.12之前選擇的數據庫是通過Lua腳本傳送到Lua腳本當前的數據庫。2.8.12之後,通過Lua選擇的數據庫僅僅影響腳本本身的執行,但是通過客戶端調用腳本不修改數據庫的選擇。 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.
需要修補程序級別版本之間的語義變化,因為舊的行為不符合Redis的復制層的本質,這個是bug的原因。
Available libraries 有效的類庫
The Redis Lua interpreter loads the following Lua libraries:
Redis的解釋器加載了以下類庫:
base lib.
table lib.
string lib.
math lib.
debug lib.
struct lib.
cjson lib.
cmsgpack lib.
bitop lib
redis.sha1hex function. Every Redis instance is
guaranteed to have all the above libraries so you can be sure that the environment for your Redis scripts is always the same.
每一個Redis實例都確保加載了上面所有的類庫,因此你可以確保你的腳本運行環境總是一樣的。 struct, CJSON and cmsgpack are external libraries, all the other libraries are standard Lua libraries.
除了struct,CJSON和cmsgpack 是外部類庫,其他所有都是Lua的標准類庫。
struct
struct is a library for packing/unpacking structures within Lua.
struct 是 壓縮/解壓縮 結構庫。
Valid formats:
> - 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
Example:
127.0.0.1:6379> eval 'return struct.pack("HH", 1, 2)' 0
"\x01\x00\x02\x00"
127.0.0.1:6379> eval 'return {struct.unpack("HH", ARGV[1])}' 0 "\x01\x00\x02\x00"
1) (integer) 1
2) (integer) 2
3) (integer) 5
127.0.0.1:6379> eval 'return struct.size("HH")' 0
(integer) 4
CJSON
The CJSON library provides extremely fast JSON manipulation within Lua.
CJSON提供了快速操作json的類庫。
Example:
redis 127.0.0.1:6379> eval 'return cjson.encode({["foo"]= "bar"})' 0
"{\"foo\":\"bar\"}"
redis 127.0.0.1:6379> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}"
"bar"
cmsgpack
The cmsgpack library provides simple and fast MessagePack manipulation within Lua.
cmsgpack 提供了簡單並且快速的MessagePack操作類庫。
Example:
127.0.0.1:6379> eval 'return cmsgpack.pack({"foo", "bar", "baz"})' 0
"\x93\xa3foo\xa3bar\xa3baz"
127.0.0.1:6379> eval 'return cmsgpack.unpack(ARGV[1])' 0 "\x93\xa3foo\xa3bar\xa3baz"
1) "foo"
2) "bar"
3) "baz
bitop
The Lua Bit Operations Module adds bitwise operations on numbers. It is available for scripting in Redis since version 2.8.18.
Lua位運算模塊增加了對數字的位運算。從2.8.18版本開始可以使用。
Example:
127.0.0.1:6379> eval 'return bit.tobit(1)' 0
(integer) 1
127.0.0.1:6379> eval 'return bit.bor(1,2,4,8,16,32,64,128)' 0
(integer) 255
127.0.0.1:6379> eval 'return bit.tohex(422342)' 0
"000671c6"
It supports several other functions: bit.tobit, bit.tohex, bit.bnot, bit.band, bit.bor,bit.bxor, bit.lshift, bit.rshift, bit.arshift, bit.rol, bit.ror, bit.bswap. All available functions are documented in the Lua BitOp documentation
支持的方法有: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 documentation上。
redis.sha1hex
Perform the SHA1 of the input string.
執行輸入的SHA1字符串。 Example:
127.0.0.1:6379> eval 'return redis.sha1hex(ARGV[1])' 0 "foo"
"0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"
Emitting Redis logs from scripts從腳本記錄日志
It is possible to write to the Redis log file from Lua scripts using the redis.logfunction.
可以使用lua腳本的redis.log函數記錄Redis的日志文件。
redis.log(loglevel,message)
loglevel is one of: loglevel 是下面中的一個
redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING They correspond directly to the normal Redis log levels. Only logs emitted by scripting using a log level that is equal or greater than the currently configured Redis instance log level will be emitted.
與一般Redis日志的級別相同。僅僅是記錄來自腳本,等效或者更高於當前Redis的日志配置的記錄。 The message argument is simply a string. Example:
參數message是一個簡單的字符串。例如:
redis.log(redis.LOG_WARNING,"Something is wrong with this script.")
Will generate the following: 將生產:
[32343] 22 Mar 15:21:39 # Something is wrong with this script.
Sandbox and maximum execution time 研究最大執行時間
Scripts should never try to access the external system, like the file system or any other system call. A script should only operate on Redis data and passed arguments.
腳本應該不要去使用外部的系統,比如文件或者其他的系統。一個腳本應該僅僅是操作Redis的數據和傳遞參數。
Scripts are also subject to a maximum execution time (five seconds by default). This default timeout is huge since a script should usually run in under a millisecond. The limit is mostly to handle accidental infinite loops created during development.
腳本也應該受制於一個最大執行時間(缺省是5秒)。因為腳本應該一般運行時間都是毫米級別的,所以這個缺省的時間是很大的。這個限制主要是去處理腳本運行期間可能發生的死循環。
It is possible to modify the maximum time a script can be executed with millisecond precision, either via redis.conf or using the CONFIG GET / CONFIG SET command. The configuration parameter affecting max execution time is called lua-time-limit.
可以通過redis.conf或者使用CONGIG GET/CONFIG SET命令設置腳本運行時間為毫秒級別。 配置最大腳本運行時間的參數名是 lua-time-limit When a script reaches the timeout it is not automatically terminated by Redis since this violates the contract Redis has with the scripting engine to ensure that scripts are atomic. Interrupting a script means potentially leaving the dataset with half-written data. For this reasons when a script executes for more than the specified time the following happens:
如果腳本達到最大運行時間沒有自動中斷,那是因為Redis的腳本引擎保證了腳本的原子性。中斷腳本可能導致數據只寫了一半。當一個腳本執行時間超過最大運行時間會是發生下面情況:
Redis logs that a script is running too long.
Redis日志腳本運行很長時間。
It starts accepting commands again from other clients, but will reply with a BUSY error to all the clients sending normal commands. The only allowed commands in this status are SCRIPT KILL and SHUTDOWN NOSAVE.
開始接收來自其他客戶端的命令,但是將返回一個BUSY的錯誤。這時僅僅允許運行的命令是 SCRIPT KILL 和SHUTDOWN NOSAVE
It is possible to terminate a script that executes only read-only commands using the SCRIPT KILL command. This does not violate the scripting semantic as no data was yet written to the dataset by the script.
可能會使用SCRIPT KILL命令去終止只讀模式的腳本。因為沒有數據的寫操作所以沒有違反腳本的原子性。
If the script already called write commands the only allowed command becomesSHUTDOWN NOSAVE that stops the server without saving the current data set on disk (basically the server is aborted).
如果腳本已經調用了寫命令,現在唯一允許的命令是 SHUTDOWN NOSAVE,將導致服務器沒有保存當前set的數據到硬盤上(主要是Redis 服務器終止了)
EVALSHA in the context of pipelining 在通道中使用EVALSHA
Care should be taken when executing EVALSHA in the context of a pipelined request, since even in a pipeline the order of execution of commands must be guaranteed. IfEVALSHA will return a NOSCRIPT error the command can not be reissued later otherwise the order of execution is violated.
在管道裡使用EVALSHA要小心,因為即使在管道也必須保證命令的執行順序。如果EVALSHA返回NOSCRIPT的錯誤,之後就不會再被執行,否則就違反了執行順序。
The client library implementation should take one of the following approaches:
客戶端應該采取下面當中的一種方法處理:
Always use plain EVAL when in the context of a pipeline.
在管道環境中總是使用EVAL。
Accumulate all the commands to send into the pipeline, then check for EVALcommands 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.
積累所有的命令到管道裡,然後使用EVAL命令運行檢測,並且使用SCRIPT EXISTS去檢測是否所有的腳本已經明確定義。如果沒有,在管道請求頭添加SCRIPT LOAD 命令,並且使用EVALSHA 代替所有的VAAL調用。