Redis进阶实践之十九 Redis如何使用lua脚本

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的文章。对了,若是你们想看英文原文,能够点击《这里》。

天下国家,可均也;爵禄,可辞也;白刃,可蹈也;中庸不可能也

相关文章
相关标签/搜索