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的文章。对了,如果大家想看英文原文,可以点击《这里》。

天下国家,可均也;爵禄,可辞也;白刃,可蹈也;中庸不可能也
标签: Redis, eval, lua
posted @ 2020-09-03 01:16  独孤败天大神  阅读(3169)  评论(0编辑  收藏  举报