Redis对Lua脚本的支持是从Redis 2.6.0版开始引入的,它可以让用户在Redis服务器内置的Lua解释器中执行指定的Lua脚本。被执行的Lua脚本可以直接调用 Redis命令,并使用Lua语言及其内置的函数库处理命令结果。
引入Lua脚本特性,为Redis带来了如下直观的变化:
使用EVAL命令可以执行给定的Lua脚本:
EVAL script numkeys key [key ... ] arg [arg ... ]
其中:
127.0.0.1:6379> EVAL "return 'hello world'" 0
"hello world"
Lua脚本的强大之处在于它可以让用户直接在脚本中执行Redis命令,这一点可以通过在脚本中调用 redis.call() 函数或者 redis.pcall() 函数来完成:
redis.call(command, ...)
redis.pcall(command, ...)
这两个函数接受的第一个参数都是被执行的Redis命令的名字,而后面跟着的则是任意多个命令参数。在Lua脚本中执行Redis命令所使用的格式与在redis-cli客户端中执行Redis命令的格式完全一样。
127.0.0.1:6379> eval "return redis.call('SET',KEYS[1],ARGV[1])" 1 "message" "hello world"
OK
127.0.0.1:6379> eval "return redis.call('ZADD',KEYS[1],ARGV[1],ARGV[2])" 1 "fruit-price" "8.5" "apple"
(integer) 1
127.0.0.1:6379> zrange "fruit-price" 0 -1 WITHSCORES
1) "apple"
2) "8.5"
redis.call() 函数和 redis.pcall() 函数都可以用于执行Redis命令,它们之间唯一的区别是处理错误的方式:
在 EVAL命令出现之前,Redis服务器中只有一个环境,那就是Redis命令执行器所在的环境。但是,随着 EVAL命令以及 Lua 解释器的出现,使得Redis服务器中同时存在了两种不同的环境。因为这两种环境使用的是不同的输入和输出,所以在这两种环境之间传递值将引发相应的转换操作:
下表列出了Redis协议值转换成Lua值的规则:
下表是Lua值转换成Redis协议值的规则:
如上面表格中的规则所言,因为带有小数部分的Lua数字将被转换为Redis整数回复:
127.0.0.1:6379> eval "return 3.14" 0
(integer) 3
所以如果你想要向Redis返回一个小数,那么可以先使用Lua内置的 tostring() 函数将它转换成字符串,然后再返回:
127.0.0.1:6379> eval "return tostring(3.14)" 0
"3.14"
为了防止预定义的Lua环境被污染,Redis只允许用户在Lua脚本中创建局部变量,而不允许创建全局变量,尝试在脚本中创建全局变量将引发错误:
127.0.0.1:6379> eval "local database='redis'; return database" 0
"redis"
127.0.0.1:6379> eval "number=10" 0
(error) ERR Error running script (call to f_a2754fa2d614ad76ecfd143acc06993bedf1f691): @enable_strict_lua:8: user_script:1: Script attempted to create global variable 'number'
与普通的Redis客户端一样,Lua脚本也允许用户执行 SELECT 命令来切换数据库,但需要注意的是,不同版本的Redis在脚本中执行 SELECT 命令的效果不同:
Redis的Lua脚本与Redis的事务一样,都是以原子方式执行的:在Redis服务器开始执行 EVAL 命令之后,直到 EVAL 命令执行完毕并向调用者返回结果之前,Redis服务器只会执行 EVAL 命令给定的脚本及其包含的Redis命令调用,至于其他的客户端发送的命令请求则会被阻塞,直到 EVAL 命令执行完毕为止。
基于上述原因,用户在使用Lua脚本的时候,必须尽可能的保证脚本能够高效,快速的执行,从而避免因为独占服务器导致阻塞其他客户端。
除了可以在redis-cli客户端中使用 EVAL 命令执行脚本之外,还可以通过 redis-cli客户端的 eval 选项,以命令行的方式执行脚本文件。
如下test.lua脚本文件:
redis.call("SET", KEYS[1], ARGV[1])
return redis.call("GET", KEYS[1])
在命令行中执行如下命令:
redis-cli --eval test.lua 'msg' , 'hello'
"hello"
注意:使用命令行执行脚本文件时,键名与参数之间的逗号前后必须要有空格间隔,如果缺少空格将引发错误
复杂度:EVAL命令的复杂度由被执行的脚本决定
版本要求:EVAL命令从Redis 2.6.0版本开始可用。
在定义脚本后,程序通常会重复的执行脚本。如果客户端每次执行脚本都需要将相同的脚本重新发送一次,显然会浪费网络带宽。为了解决上述问题,Redis提供了Lua脚本缓存功能,这一功能允许用户将给定的Lua脚本缓存在服务器中,然后根据Lua脚本的SHA1校验和直接调用脚本,从而避免了需要重复发送相同脚本的麻烦。
SCRIPT LOAD script
SCRIPT LOAD命令可以将用户发送的脚本缓存在服务器中,并返回脚本的SHA1校验和。
127.0.0.1:6379> script load "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
然后就可以通过EVALSHA命令来执行被缓存的脚本:
EVALSHA sha1 numkeys key [key ... ] arg [arg ... ]
除了第一个参数是脚本 SHA1校验和之外,其他的参数和EVAL命令的一样。
其实在使用EVAL命令执行脚本的时候,也会把脚本给缓存起来,只不过EVAL并不返回脚本的SHA1校验和,因此需要自己根据脚本生成SHA1校验和。
除了SCRIPT LOAD命令之外,Redis还提供了SCRIPT EXISTS、SCRIPT FLUSH 和 SCRIPT KILL 这3个命令来管理脚本。
SCRIPT EXISTS命令接受一个或多个 SHA1校验和作为参数,检查这些校验和对应的脚本是否已经被缓存到了服务器中:
SCRIPT EXISTS sha1 [sha1 ... ]
如果某个校验和对应的脚本已经缓存到了服务器中,则返回 1,否则返回 0
执行SCRIPT FLUSH命令,可以移除所有已经缓存在服务器中的脚本:
SCRIPT FLUSH
成功移除所有缓存脚本之后,返回 0
因为Lua脚本在执行的时候会独占Redis服务器,所以如果Lua脚本运行的时间太长,又或者因为编程错误导致脚本无法退出,那么就会导致其他客户端一直无法执行命令。因此Redis提供了 SCRIPT KILL命令,可以用来强制停止正在运行的脚本:
SCRIPT KILL
用户在执行SCRIPT KILL命令之后,服务器可能会有以下反应:
Redis在Lua环境中内置了一些函数库,用户可以通过这些函数库对Redis服务器进行操作,或者对给定的数据进行处理,这些函数库分别是:
其中base、table、string以及math包均为Lua标准库;redis包为调用Redis功能的专用包,而 bit、srtuct、cjson以及cmsgpack包则是从外部引入的数据处理包。
除了前面介绍过的 redis.call() 函数和**redis.pcall()**函数之外,redis包还包含了以下函数:
**redis.log()**函数用于在脚本中向Redis服务器写入日志,它接受一个日志等级和一条消息作为参数:
redis.log(loglevel, message)
其中loglevel的值可以是以下4个日志等级中的一个,这些日志等级与Redis服务器本身的日志等级完全一致:
当给定的日志等级超过或等同于Redis服务器当前设置的日志等级时,Redis服务器就会把给定的消息写入日志中。
**redis.sha1hex()**函数可以计算出给定字符串的SHA1校验和:
redis.sha1hex(string)
127.0.0.1:6379> eval "return redis.sha1hex('show me your sha1')" 0
"e00ecdbe6ea77b31972c28dccad6aceba9822a12"
**redis.error_reply()函数和redis.status_reply()**是两个辅助函数,分别用于返回Redis的错误回复以及状态回复:
redis.error_reply(error_message)
redis.status_reply(status_message)
redis.error_reply() 函数会返回一个包含 err 字段的Lua表格,而 err 字段的值则是给定的错误消息;同样的,redis.status_reply() 函数将返回一个只包含 ok 字段的Lua表格,而 ok 字段的值则是给定的状态消息。
127.0.0.1:6379> eval "return redis.error_reply('something wrong')" 0
(error) something wrong
127.0.0.1:6379> eval "return redis.status_reply('all is well')" 0
all is well
bit 包可以对Lua脚本中的数字执行二进制按位操作,这个包从Redis 2.8.18版本开始可用。bit 包提供了将数字转换为十六进制字符串的 tohex() 函数,以及对给定数字执行 按位反、按位或、按位并 以及 按位异或 的 bnot()、bor()、band()、bxor() 等函数。
bit.tohex(x [, n])
bit.bnot(x)
bit.bor(x1 [, x2 ...])
bit.band(x1 [, x2 ... ])
bit.bxor(x1 [, x2 ... ])
127.0.0.1:6379> eval "return bit.tohex(65535)" 0
"0000ffff"
127.0.0.1:6379> eval "return bit.tohex(65535,4)" 0
"ffff"
127.0.0.1:6379> eval "return bit.tohex(bit.bnot(0xFFFF))" 0
"ffff0000"
127.0.0.1:6379> eval "return bit.tohex(bit.bor(0xF00F,0x0F00))" 0
"0000ff0f"
struct包提供了能够在Lua值以及C结构之间进行转换的基本设施,这个包提供了 pack()、unpack()、size() 这3个函数:
struct.pack(fmt, v1, v2, ...)
struct.unpack(fmt, s, [i])
struct.size(fmt)
其中 struct.pack() 用于将给定的一个或多个Lua值打包为一个类结构字符串,struct.unpack() 用于从给定的类结构字符串中解包出多个Lua值,而 struct.size() 函数则用于计算按照给定格式进行打包需要耗费的字节数量。
-- 打包一个浮点数、一个无符号长整数以及一个11字节长的字符串
127.0.0.1:6379> eval "return struct.pack('fLc11',3.14,10086,'hello world')" 0
"\xc3\xf5H@f'\x00\x00\x00\x00\x00\x00hello world"
-- 计算打包耗费的字节数
127.0.0.1:6379> eval "return struct.size('fLc11')" 0
(integer) 23
-- 对给定的类结构字符串进行解包
127.0.0.1:6379> eval "return {struct.unpack('fLc11',ARGV[1])}" 0 "\xc3\xf5H@f'\x00\x00\x00\x00\x00\x00hello world"
1) (integer) 3
2) (integer) 10086
3) "hello world"
4) (integer) 24
cjson包能够为Lua脚本提供快速的JSON编码和解码操作,这个包中最常用的就是将Lua值编码为JSON数据的编码函数 encode(),以及将JSON数据解码为Lua值的解码函数 decode() :
cjson.encode(value)
cjson.decode(json_text)
127.0.0.1:6379> eval "return cjson.encode({true,128,'hello world'})" 0
"[true,128,\"hello world\"]"
127.0.0.1:6379> eval "return cjson.decode(ARGV[1])" 0 "[true,128,\"hello world\"]"
1) (integer) 1
2) (integer) 128
3) "hello world"
cmsgpack 函数能够为Lua脚本提供快速的 MessagePack 打包和解包操作,这个包中最常用的就是打包函数 pack() 以及解包函数 unpack(),前者可以将给定的任意多个Lua值打包为msgpack包,而后者则可以将给定的 msgpack 包解包为任意多个Lua值:
cmsgpack.pack(arg1, arg2, arg3 ... )
cmsgpack.unpack(msgpack)
127.0.0.1:6379> eval "return cmsgpack.pack({true,128,'hello world'})" 0
"\x93\xc3\xcc\x80\xabhello world"
127.0.0.1:6379> eval "return cmsgpack.unpack(ARGV[1])" 0 "\x93\xc3\xcc\x80\xabhello world"
1) (integer) 1
2) (integer) 128
3) "hello world"
在早期支持的Lua脚本功能的Redis版本中,为了对脚本进行调试,通常需要重复执行同一个脚本多次,并通过查看返回值的方式验证计算结果,这给脚本调试带来了很大的麻烦。为了解决这个问题,Redis从3.2版本开始引入了新的Lua调试器,这个调试器被称为 Redis Lua调试器,简称 LDB, 用户可以通过 LDB 实现单步调试、添加断点、返回日志、打印调用链、重载脚本 等多种功能。
redis-cli --ldb --eval demo.lua "msg" , "hello world"
如上述命令就可以对脚本 demo.lua 开启调试模式。然后可以使用 step命令或者 next命令进行单步调试,使用 print 命令来查看程序当前已有的局部变量以及它们的值。
除了 next 命令和 print 命令外,Lua脚本调试器还提供了很多不同的调试命令,如下表所示:
可以使用 break 命令给脚本添加断点,然后使用 continue 命令执行代码,直到遇到下一个断点为止。
例如如下脚本代码:
1 redis.call("echo", "line 1")
2 redis.call("echo", "line 2")
3 redis.call("echo", "line 3")
4 redis.call("echo", "line 4")
5 redis.call("echo", "line 5")
我们可以通过执行命令 break 3 5
,分别在脚本的第3行和第5行添加断点:
lua debugger> break 3 5
除了可以使用 break 命令添加断点之外,Redis还允许用户在脚本中通过调用 redis.breakpoint() 函数来添加动态断点,当调试器执行至 redis.breakpoint() 调用所在行时,调试器就会暂停执行过程并等待用户的指示。
if condition == true then
redis.breakpoint()
...
end
使用 redis.debug() 函数能够直接把给定的值输出到调试客户端,使得用户可以方便的得知给定变量或者表达式的值。
Lua调试器提供了 eval 和 redis 这两个调试命令,用户可以使用前者来执行指定的Lua代码,并使用后者来执行指定的Redis命令,也可以通过这两个命令来快速地验证一些想法以及结果。
lua debugger> eval redis.sha1hex('hello world')
当在调试过程中需要知道某个键的值时,可以使用 redis 命令:
lua debugger> redis GET msg
GET msg
"hello world"
trace调试命令可以用来打印出脚本的调用链信息,这些信息在研究脚本的调用路径时非常有用。
restart 是一个非常重要的调试器命令,它可以让调试客户端重新载入被调试的脚本,并开启一个新的调试会话。
Redis的Lua调试器支持两种不同的调试模式,一种是异步调试,另外一种是同步调试。当用户以 ldb 选项启动调试会话时,Redis服务器将以异步方式调试脚本:
redis-cli --ldb --eval script.lua
运行在异步调试模式下的Redis服务器会为每个调试器会话分别创建新的子进程,并将其用作调试进程:
当用户以 ldb-sync-model 选项启动调试会话时,Redis服务器将以同步的方式调试脚本:
redis-cli --ldb-sync-model --eval script.lua
在同步调试模式下:
在调试Lua脚本时,有3种方式可以终止调试会话:
上一篇:Redis学习手册12—流水线与事务
下一篇:Redis学习手册14—持久化