Lua脚本特性的出现给Redis带来了很大的变化,其中最重要的就是使得用户可以按需对Redis服务器的功能进行扩展:在Lua脚本特性出现之前,用户如果想要给Redis服务器增加新功能,那么只能自行修改Redis服务器源码,这样做不仅麻烦,还会给Redis服务器带来升级困难、无法与标准Redis服务器兼容等问题,而Lua脚本的出现则为用户提供了一种标准的、无后顾之忧的方法来扩展Redis服务器的功能。
Lua脚本带来的第二个变化与它的执行机制有关:Redis服务器以原子方式执行Lua脚本,在执行完整个Lua脚本及其包含的Redis命令之前,Redis服务器不会执行其他客户端发送的命令或脚本,因此被执行的Lua脚本天生就具有原子性。
Lua脚本的另一个好处是它能够在保证原子性的同时,一次在脚本中执行多个Redis命令:对于需要在客户端和服务器之间往返通信多次的程序来说,使用Lua脚本可以有效地提升程序的执行效率。虽然使用流水线加上事务同样可以达到一次执行多个
Redis命令的目的,但Redis提供的Lua脚本缓存特性能够更为有效地减少带宽占用。
Redis在Lua环境中内置了一些非常有用的包,通过使用这些包,用户可以直接在服务器端对数据进行处理,然后把它们存储到数据库中,这可以有效地减少不必要的网络传输。
用户可以使用EVAL
命令来执行给定的Lua脚本:EVAL script numkeys key [key ...] arg [arg ...]
script
参数用于传递脚本本身。因为Redis目前内置的是Lua 5.1
版本的解释器,所以用户在脚本中也只能使用Lua 5.1
版本的语法。numkeys
参数用于指定脚本需要处理的键数量,而之后的任意多个key
参数则用于指定被处理的键。通过key
参数传递的键可以在脚本中通过KEYS
数组进行访问。根据Lua的惯例,KEYS
数组的索引将以1为开始:访问KEYS[1]
可以取得第一个传入的key
参数,访问KEYS[2]
可以取得第二个传入的key
参数,以此类推。arg
参数用于指定传递给脚本的附加参数,这些参数可以在脚本中通过ARGV
数组进行访问。与KEYS
参数一样,ARGV
数组的索引也是以1为开始的。redis> EVAL "return 'hello world'" 0
"hello world"
这个命令将脚本"return 'hello world'"传递给了Lua环境执行,其中Lua关键字return
用于将给定值返回给脚本调用者,而'hello world'则是被返回的字符串值。跟在脚本后面的是numkeys
参数的值0,说明这个脚本不需要对Redis的数据库键进行处理。除此之外,这个命令也没有给定任何arg
参数,说明这个脚本也不需要任何附加参数。
Lua脚本的强大之处在于它可以让用户直接在脚本中执行Redis命令,这一点可以通过在脚本中调用redis.call()
函数或者redis.pcall()
函数来完成:
redis.call(command, ...)
, redis.pcall(command, ...)
redis> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 "message" "hello world"
OK
redis> GET "message"
"hello world"
脚本中的redis.call('SET',KEYS[1],ARGV[1])
表示被执行的是Redis的SET
命令,而传给命令的两个参数则分别是KEYS[1]
和ARGV[1]
,其中KEYS[1]
为"message",而ARGV[1]
则为"hello world"。
redis.call()
函数和redis.pcall()
函数都可以用于执行Redis命令,它们之间唯一不同的就是处理错误的方式。前者在执行命令出错时会引发一个Lua错误,迫使EVAL
命令向调用者返回一个错误;而后者则会将错误包裹起来,并返回一个表示错误的Lua表格:
-- Lua的type()函数用于查看给定值的类型
redis> EVAL "return type(redis.call('WRONG_COMMAND'))" 0
(error) ERR Error running script (call to f_2c59998e8c4eb7f9fdb467ba67ba43dfaf8a6592): @user_scr
ipt:1: @user_script: 1: Unknown Redis command called from Lua script
redis> EVAL "return type(redis.pcall('WRONG_COMMAND'))" 0
"table"
在第一个EVAL
命令调用中,redis.call()
无视type()
函数引发了一个错误;而在第二个EVAL
命令调用中,redis.pcall()
向type()
函数返回了一个包含出错信息的表格,因此脚本返回的结果为"table"。
在EVAL
命令出现之前,Redis服务器中只有一种环境,那就是Redis命令执行器所处的环境,这一环境接受Redis协议值作为输入,然后返回Redis协议值作为输出。
但是随着EVAL
命令以及Lua解释器的出现,使得Redis服务器中同时出现了两种不同的环境:一种是Redis命令执行器所处的环境,而另一种则是Lua解释器所处的环境。因为这两种环境使用的是不同的输入和输出,所以在这两种环境之间传递值将引发相应的转换操作:
1) 当Lua脚本通过redis.call()
函数或者redis.pcall()
函数执行Redis命令时,传入的Lua值将被转换成Redis协议值;比如,当脚本调用redis.call('SET',KEYS[1],ARGV[1])
的时候,'SET'
、KEYS[1]
以及ARGV[1]
都会从Lua值转换为Redis协议值。
2) 当redis.call()
函数或者redis.pcall()
函数执行完Redis命令时,命令返回的Redis协议值将被转换成Lua值。比如,当redis.call('SET',KEYS[1],ARGV[1])
执行完毕的时候,执行SET
命令所得的结果OK
将从Redis协议值转换为Lua值。
3) 当Lua脚本执行完毕并向EVAL
命令的调用者返回结果时,Lua值将被转换为Redis协议值。比如,当脚本"return'hello world'"执行完毕的时候,Lua值'hello world'将转换为相应的Redis协议值。
redis> EVAL "return 3.14" 0 -- 因为带有小数部分的Lua数字将被转换为Redis整数回复
(integer) 3
redis> EVAL "return tostring(3.14)" 0 -- 先使用Lua内置的tostring()函数将它转换为字符串
"3.14"
redis> SET dbnumber 0 -- 将0号数据库的dbnumber键的值设置为0
OK
redis> SELECT 1 -- 切换至1号数据库
OK
redis[1]> SET dbnumber 1 -- 将1号数据库的dbnumber键的值设置为1
OK
redis[1]> SELECT 0 -- 切换回0号数据库
OK
redis> EVAL "redis.call('SELECT', ARGV[1]); return redis.call('GET', KEYS[1])" 1 "dbnumber" 1
"1" -- 在脚本中切换至1号数据库,并获取dbnumber键的值
redis> GET dbnumber
"0" -- dbnumber 键的值为0,这表示客户端的当前数据库仍然是0号数据库
Redis的Lua脚本与Redis的事务一样,都是以原子方式执行的:在Redis服务器开始执行EVAL
命令之后,直到EVAL
命令执行完毕并向调用者返回结果之前,Redis服务器只会执行EVAL
命令给定的脚本及其包含的Redis命令调用,至于其他客户端发送的命令请求则会被阻塞,直到EVAL
命令执行完毕为止。
用户除了可以在redis-cli
客户端中使用EVAL
命令执行给定的脚本之外,还可以通过redis-cli
客户端的eval
选项,以命令行方式执行给定的脚本文件。在使用eval
选项执行Lua脚本时,用户不需要像执行EVAL
命令那样指定传入键的数量,只需要在传入键和附加参数之间使用逗号进行分割即可。
执行命令: redis-cli -a 密码 --eval Lua脚本路径 key [key …] , arg [arg …]
/script/set_and_get.lua
redis.call("SET", KEYS[1], ARGV[1])
return redis.call("GET", KEYS[1])
在命令行执行
$ ./redis-cli --eval ../script/set_and_get.lua 'msg' , 'ciao'
"ciao"
在定义脚本之后,程序通常会重复地执行脚本。一个简单的脚本可能只有几十到上百字节,而一个复杂的脚本可能会有数百字节甚至数千字节,如果客户端每次执行脚本都需要将相同的脚本重新发送一次,这对于宝贵的网络带宽来说无疑是一种浪费。
为了解决上述问题,Redis提供了Lua脚本缓存功能,这一功能允许用户将给定的Lua脚本缓存在服务器中,然后根据Lua脚本的SHA1
校验和直接调用脚本,从而避免了需要重复发送相同脚本的麻烦。
命令SCRIPT LOAD
可以将用户给定的脚本缓存在服务器中,并返回脚本对应的SHA1
校验和作为结果:SCRIPT LOAD script
redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]); return redis.call('GET', KEYS[1])"
"18d788194860a281b19910d462b1e96dabf3c984"
可以通过EVALSHA命令来执行已被缓存的脚本:EVALSHA sha1 numkeys key [key ...] arg [arg ...]
redis> EVALSHA "5332031c6b470dc5a0dd9b4bf2030dea6d65de91" 0
"hello world"
redis> EVALSHA "18d788194860a281b19910d462b1e96dabf3c984" 1 "msg" "Ciao!"
"Ciao!"
除了SCRIPT LOAD
之外,EVAL
命令在执行完脚本之后也会把被执行的脚本缓存起来,以供之后使用。
SCRIPT EXISTS
命令接受一个或多个SHA1
校验和作为参数,检查这些校验和对应的脚本是否已经被缓存到了服务器中:SCRIPT EXISTS sha1 [sha1 ...]
redis> SCRIPT LOAD "return 10086" -- 载入两个脚本
"a9b6689bf0d2962188a5fb8f2502e8de5a19fc26"
redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> SCRIPT EXISTS "a9b6689bf0d2962188a5fb8f2502e8de5a19fc26" "5332031c6b470dc5a0dd9b4bf2030dea6d65de91" "not-exists-sha1-123456789abcdefghijklmno"
1) (integer) 1 -- 脚本存在
2) (integer) 1 -- 脚本存在
3) (integer) 0 -- 脚本不存在
执行SCRIPT FLUSH
命令将移除服务器已缓存的所有脚本,这个命令一般只会在调试时使用:SCRIPT FLUSH
redis> SCRIPT LOAD "return 10086" -- 载入两个脚本
"a9b6689bf0d2962188a5fb8f2502e8de5a19fc26"
redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> SCRIPT EXISTS "a9b6689bf0d2962188a5fb8f2502e8de5a19fc26" "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
1) (integer) 1 -- 脚本存在
2) (integer) 1
redis> SCRIPT FLUSH -- 移除所有已载入的脚本
OK
redis> SCRIPT EXISTS "a9b6689bf0d2962188a5fb8f2502e8de5a19fc26" "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
1) (integer) 0 -- 脚本不再存在
2) (integer) 0
因为Lua脚本在执行时会独占整个服务器,所以如果Lua脚本的运行时间过长,又或者因为编程错误而导致脚本无法退出,那么就会导致其他客户端一直无法执行命令。
配置选项lua-time-limit
的值定义了Lua脚本可以不受限制运行的时长,这个选项的默认值为5000:lua-time-limit <milliseconds>
当脚本的运行时间低于lua-time-limit
指定的时长时,其他客户端发送的命令请求将被阻塞;相反,当脚本的运行时间超过lua-time-limit
指定的时长时,向服务器发送请求的客户端将得到一个错误回复,提示用户可以使用SCRIPT KILL
或者SHUTDOWN NOSAVE
命令来终止脚本或者直接关闭服务器。
-- 客户端1
client-1> EVAL "repeat until false" 0 -- 无限循环
等待5s
-- 客户端2
client-2> set msg "hello" -- 有脚本仍在运行
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
client-2> script kill
OK -- 返回OK表示已经成功终止脚本
client-2> set msg "hello"
OK -- 现在服务器可以如常执行命令请求了
用户在执行SCRIPT KILL
命令之后,服务器可能会有以下两种反应:
SHUTDOWN nosave
命令,在不执行持久化操作的情况下关闭服务器,然后通过手动重启服务器来让它回到正常状态。redis> SCRIPT KILL
(error) UNKILLABLE Sorry the script already executed write commands against the
dataset. You can either wait the script termination or kill the server in a hard way using the S
HUTDOWN NOSAVE command.
redis> SHUTDOWN nosave
not connected> -- 服务器已被关闭
Redis在Lua环境中内置了一些函数库,用户可以通过这些函数库对Redis服务器进行操作,或者对给定的数据进行处理,这些函数库分别是:
除了前面已经介绍过的redis.call()
函数和redis.pcall()
函数之外,redis
包还包含了以下函数:
redis.log()
函数用于在脚本中向Redis服务器写入日志,它接受一个日志等级和一条消息作为参数:redis.log(loglevel, message)
其中loglevel
的值可以是以下4个日志等级的其中一个,这些日志等级与Redis服务器本身的日志等级完全一致:
redis> EVAL "redis.log(redis.LOG_WARNING, 'Something wrong!')" 0
redis-server 将写一条日志:
1276417:M 27 Sep 2022 14:19:08.395 # Something wrong!
redis.sha1hex()
函数可以计算出给定字符串输入的SHA1
校验和:redis.sha1hex(string)
redis> 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字段的值则是给定的状态消息。
redis> EVAL "return redis.error_reply('something wrong')" 0
(error) something wrong
redis> EVAL "return redis.status_reply('all is well')" 0
all is well
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...])
redis> EVAL "return bit.tohex(65535)" 0
"0000ffff"
redis> EVAL "return bit.tohex(65535, 4)" 0
"ffff"
redis> EVAL "return bit.tohex(bit.bnot(0xFFFF))" 0
"ffff0000"
redis> 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-like string),struct.unpack()
用于从给定的类结构字符串中解包出多个Lua值,而struct.size()
则用于计算按照给定格式进行打包需要耗费的字节数量。
-- 打包一个浮点数、一个无符号长整数以及一个11字节长的字符串
redis> EVAL "return struct.pack('fLc11', 3.14, 10086, 'hello world')" 0
"\xc3\xf5H@f'\x00\x00\x00\x00\x00\x00hello world"
-- 计算打包需要耗费的字节数
redis> EVAL "return struct.size('fLc11')" 0
(integer) 23
-- 对给定的类结构字符串进行解包
redis> EVAL "return {struct.unpack('fLc11', ARGV[1])}" 0 "\xc3\xf5H@f'\x00\x00\x00\x00\x00\x00hello world"
1) (integer) 3 -- 根据Lua和Redis的转换规则,浮点数3.14被转换成了整数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)
redis> EVAL "return cjson.encode({true, 128, 'hello world'})" 0
"[true,128,\"hello world\"]"
redis> EVAL "return cjson.decode(ARGV[1])" 0 "[true,128,\"hello world\"]"
1) (integer) 1 -- 根据转换规则,Lua布尔值true被转换成了数字1
2) (integer) 128
3) "hello world"
cmsgpack
包能够为Lua脚本提供快速的MessagePack
打包和解包操作,这个包中最常用的就是打包函数pack()
以及解包函数unpack()
,前者可以将给定的任意多个Lua值打包为msgpack
包,而后者则可以将给定的msgpack
包解包为任意多个Lua值:
cmsgpack.pack(arg1, arg2, ..., argn)
cmsgpack.unpack(msgpack)
redis> EVAL "return cmsgpack.pack({true, 128, 'hello world'})" 0
"\x93\xc3\xcc\x80\xabhello world"
redis> EVAL "return cmsgpack.unpack(ARGV[1])" 0 "\x93\xc3\xcc\x80\xabhello world"
1) (integer) 1 -- 根据转换规则,Lua布尔值true被转换成了数字1
2) (integer) 128
3) "hello world"
/script/debug.lua
local ping_result = redis.call("PING")
local set_result = redis.call("SET", KEYS[1], ARGV[1])
return {ping_result, set_result}
-- 要将--ldb选项、--eval选项、脚本文件名debug.lua、键名"msg"以及附加参数"hello world"全部传递给redis-cli客户端
$ ./redis-cli --ldb --eval ../script/debug.lua "msg" , "hello" -- 注意逗号前后的空格
Lua debugging session started, please use:
quit -- End the session.
restart -- Restart the script in debug mode again.
help -- Show Lua script debugging commands.
* Stopped at 1, stop reason = step over
-> 1 local ping_result = redis.call("PING")
-- 此时处于单步调试模式,可以通过输入调试命令step或者next,让调试器运行当前的代码行
lua debugger> next
<redis> PING -- 服务器执行的命令ping
<reply> "+PONG" -- 服务器在执行命令之后返回的结果
* Stopped at 2, stop reason = step over
-> 2 local set_result = redis.call("SET", KEYS[1], ARGV[1])
-- 处于单步调试模式,可以通过输入调试命令print来查看程序当前已有的局部变量以及它们的值
lua debugger> print
<value> ping_result = {["ok"]="PONG"}
-- 执行next命令
lua debugger> next
<redis> SET msg hello -- 服务器这次执行了一个SET命令
<reply> "+OK"
* Stopped at 3, stop reason = step over
-> 3 return {ping_result, set_result}
-- 执行print命令
lua debugger> print
<value> ping_result = {["ok"]="PONG"}
<value> set_result = {["ok"]="OK"}
-- 执行next命令
lua debugger> next
1) PONG
2) OK
(Lua debugging session ended -- dataset changes rolled back)
其中PONG
以及OK
为脚本语句return{ping_result,set_result}
返回的值,而之后显示的(Lua debugging session ended--datasetchanges rolled back)
则是调试器打印的提示信息,它告诉我们Lua调试会话已经结束。此外,因为在调试完成之后,客户端将退出调试模式并重新回到普通的Redis客户端模式,所以我们在最后看到了熟悉的redis>
提示符。
调试器操作命令:
因为调试程序通常需要重复执行多次相同的调试命令,为了让枯燥的调试过程变得稍微愉快和容易一些,Redis为每个调试命令都设置了一个缩写,即执行命令的快捷方式,这些缩写就是命令开头的首个字母,用户只需要输入这些缩写,就可以执行相应的调试命令。
在一般情况下,我们将以单步执行的方式对脚本进行调试,也就是说,使用next
命令执行一个代码行,观察一下执行的结果,在确认没有问题之后,继续使用next
命令执行下一个代码行,以此类推,直到整个脚本都被执行完毕为止。
但是在有需要的情况下,也可以通过break
命令给脚本增加断点,然后使用continue
命令执行代码,直到遇见下一个断点为止。
/script/breakpoint.lua
redis.call("echo", "line 1")
redis.call("echo", "line 2")
redis.call("echo", "line 3")
redis.call("echo", "line 4")
redis.call("echo", "line 5")
$ ./redis-cli --ldb --eval ../script/breakpoint.lua
Lua debugging session started, please use:
quit -- End the session.
restart -- Restart the script in debug mode again.
help -- Show Lua script debugging commands.
* Stopped at 1, stop reason = step over
-> 1 redis.call("echo", "line 1")
lua debugger> break 3 5 -- 分别在脚本的第3行和第5行添加断点
2 redis.call("echo", "line 2")
#3 redis.call("echo", "line 3")
4 redis.call("echo", "line 4")
4 redis.call("echo", "line 4")
#5 redis.call("echo", "line 5")
lua debugger> continue
* Stopped at 3, stop reason = break point
->#3 redis.call("echo", "line 3")
符号->用于标识当前行,而符号#则用于标识添加了断点的行。
如果我们现在执行命令continue
,那么调试器将执行脚本的第1行和第2行,然后在脚本的第1个断点(第3个代码行)前面暂停
break
命令除了可以用于添加断点之外,还可用于显示已有断点以及移除断点
lua debugger> break -- 显示已有断点
2 breakpoints set:
->#3 redis.call("echo", "line 3")
#5 redis.call("echo", "line 5")
lua debugger> break -3 -- 移除第3个代码行的断点
Breakpoint removed.
lua debugger> break
1 breakpoints set:
#5 redis.call("echo", "line 5")
除了可以使用break
命令在调试脚本时手动添加断点之外,Redis还允许用户在脚本中通过调用redis.breakpoint()
函数来添加动态断点,当调试器执行至redis.breakpoint()
调用所在的行时,调试器就会暂停执行过程并等待用户的指示。
/script/dynamic_breakpoint.lua
local i = 1
local target = tonumber(ARGV[1])
while true do
if i > target then
redis.breakpoint()
return "bye bye"
end
i = i+1
end
$ ./redis-cli --ldb --eval ../script/dynamic_breakpoint.lua , 50
* Stopped at 1, stop reason = step over
-> 1 local i = 1
lua debugger> continue
* Stopped at 6, stop reason = redis.breakpoint() called
-> 6 return "bye bye"
lua debugger> print
<value> i = 51
<value> target = 50
由print
命令的执行结果可知,在第一次执行continue
命令之后,调试器将在i的值为51时添加断点。
需要注意的是,redis.breakpoint()
调用只会在调试模式下产生效果,处于普通模式下的Lua解释器将自动忽略该调用。比如,如果我们直接使用EVAL
命令去执行dynamic_breakpoint.lua
脚本,那么脚本将不会产生任何断点,而是会直接返回脚本的执行结果
使用Lua环境内置的redis.debug()
函数,这个函数能够直接把给定的值输出到调试客户端,使得用户可以方便地得知给定变量或者表达式的值。
/script/fibonacci.lua
local n = tonumber(ARGV[1])
-- F(0) = 0 , F(1) = 1
local i = 0
local j = 1
-- F(n) = F(n-1)+F(n-2)
while n ~= 0 do
i, j = j, i+j
n = n-1
redis.debug(i)
end
return i
$ ./redis-cli --ldb --eval ../script/fibonacci.lua , 10
* Stopped at 1, stop reason = step over
-> 1 local n = tonumber(ARGV[1])
lua debugger> continue
<debug> line 11: 1
<debug> line 11: 1
<debug> line 11: 2
<debug> line 11: 3
<debug> line 11: 5
<debug> line 11: 8
<debug> line 11: 13
<debug> line 11: 21
<debug> line 11: 34
<debug> line 11: 55
(integer) 55
(Lua debugging session ended -- dataset changes rolled back)
Lua调试器提供了eval
和redis
这两个调试命令,用户可以使用前者来执行指定的Lua代码,并使用后者来执行指定的Redis命令,也可以通过这两个调试命令来快速地验证一些想法以及结果,这会给程序的调试带来很多好处。
比如,如果我们在调试某个脚本时,需要知道某个字符串对应的SHA1
校验和,那么只需要使用eval
命令调用Lua环境内置的redis.sha1hex()
函数即可:
lua debugger> eval redis.sha1hex('hello world')
<retval> "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"
又比如,如果我们在调试某个会对数据库进行操作的脚本时,想要知道某个键的当前值,那么只需要使用redis
命令执行相应的数据获取命令即可:
lua debugger> redis GET msg
<redis> GET msg
<reply> "hello world"
trace
调试命令可以打印出脚本的调用链信息,这些信息在研究脚本的调用路径时会非常有帮助。
/script/trace.lua
local f1 = function()
local f2 = function()
local f3 = function()
redis.breakpoint()
end
f3()
end
f2()
end
f1()
$ redis-cli --ldb --eval trace.lua
* Stopped at 9, stop reason = step over
-> 9 end
lua debugger> continue
* Stopped at 5, stop reason = redis.breakpoint() called
-> 5 end
lua debugger> list
1 local f1 = function()
2 local f2 = function()
3 local f3 = function()
4 redis.breakpoint()
-> 5 end
6 f3()
7 end
8 f2()
9 end
10
lua debugger> trace
In f3:
-> 5 end -- 表示调试器停在了脚本第5行,该行位于函数f3()当中
From f2:
6 f3() -- 则说明了函数f3()位于脚本的第6行,由函数f2()调用
From f1:
8 f2() -- 则说明了函数f2()位于脚本的第8行,由函数f1()调用
From top level: -- 则说明了函数f1()位于脚本的第11行,由解释器顶层(top level)调用。
11 f1()
restart
是一个非常重要的调试器操作命令,它可以让调试客户端重新载入被调试的脚本,并开启一个新的调试会话。
一般来说,用户在调试脚本的时候,通常需要重复执行以下几个步骤,直至排除所有问题为止:
1) 调试脚本。
2) 根据调试结果修改脚本。
3) 使用restart
命令重新载入修改后的脚本,然后继续调试。
/script/typo.lua
redis.call('PING')
redis.call('PING')
redis.call('P1NG') -- 错别字
redis.call('PING')
redis.call('PONG') -- 错别字
$ redis-cli --ldb --eval typo.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call('PING')
lua debugger> continue
(error) ERR Error running script (call to f_cb3ff5da49083d9b7765f3c62b6bfce3b07cbdcb): @user_scr
ipt:3: @user_script: 3: Unknown Redis command called from Lua script
(Lua debugging session ended -- dataset changes rolled back)
redis>
根据调试结果可知,脚本第3行的命令调用出现了错误。于是我们修改文件,将调用中的P1NG
修改为PING
,然后在客户端中输入restart
,重新开始调试:
redis> restart
* Stopped at 1, stop reason = step over
-> 1 redis.call('PING')
lua debugger>
之后,调试器继续执行脚本,并在脚本的第5行停了下来,修改文件,将调用中的PONG
修改为PING
,然后再次在客户端中输入restart
并重新开始调试
经过修改之后,脚本终于可以顺利地执行了:
lua debugger> continue
(nil)
(Lua debugging session ended -- dataset changes rolled back)
redis>
Redis的Lua调试器支持两种不同的调试模式,一种是异步调试,另一种则是同步调试。当用户以ldb
选项启动调试会话时,Redis服务器将以异步方式调试脚本:
redis-cli --ldb --eval script.lua
运行在异步调试模式下的Redis服务器会为每个调试会话分别创建新的子进程,并将其用作调试进程:
当用户以ldb-sync-mode
选项启动调试会话时,Redis服务器将以同步方式调试脚本:
redis-cli --ldb-sync-mode --eval script.lua
运行在同步调试模式下的Redis服务器将直接使用服务器进程作为调试进程:
$ redis-cli --ldb --eval typo.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call('PING')
lua debugger> redis SET msg 'hello world'
<redis> SET msg hello world
<reply> "+OK"
lua debugger> quit
那么在调试完毕之后,msg键将不会在数据库中出现:
$ redis-cli
redis> KEYS *
(empty list or set)
与此相反,如果我们在相同的Redis服务器上进行同步调试,并执行相同的设置操作:
$ redis-cli --ldb-sync-mode --eval typo.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call('PING')
lua debugger> redis SET msg 'hello world'
<redis> SET msg hello world
<reply> "+OK"
lua debugger> quit
那么在调试完毕之后,msg键将继续保留在服务器的数据库中:
$ redis-cli
redis> KEYS *
1) "msg"
在调试Lua脚本时,用户有3种方法可以退出调试会话:
Ctrl+C
键时,调试器将在执行完整个脚本之后终止调试会话。abort
命令时,调试器将不再执行任何代码,直接终止调试会话。我们需要特别注意方法2和方法3之间的区别,因为对于一些脚本来说,使用这两种退出方法可能会产生不一样的结果。
举个例子,对于代码清单14-10所示的脚本:
/script/set_strings.lua
redis.call("SET", "msg", "hello world")
redis.call("SET", "database", "redis")
redis.call("SET", "number", 10086)
如果我们使用同步模式调试这个脚本,并在执行脚本的第一行代码之后按Ctrl+C
键退出调试,那么调试器将在执行完整个脚本之后退出调试会话:
$ redis-cli --ldb-sync-mode --eval set_strings.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call("SET", "msg", "hello world")
lua debugger> next
<redis> SET msg hello world
<reply> "+OK"
* Stopped at 2, stop reason = step over
-> 2 redis.call("SET", "database", "redis")
lua debugger>
$
通过访问数据库可以看到,脚本设置的3个键都出现在了数据库中,这说明脚本包含的3个SET
命令都被执行了:
redis> KEYS *
1) "number"
2) "database"
3) "msg"
此外,如果我们使用相同的模式调试相同的脚本,但是在执行脚本的第1行之后使用abort
命令退出调试:
$ redis-cli --ldb-sync-mode --eval set_strings.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call("SET", "msg", "hello world")
lua debugger> next
<redis> SET msg hello world
<reply> "+OK"
* Stopped at 2, stop reason = step over
-> 2 redis.call("SET", "database", "redis")
lua debugger> abort
(error) ERR Error running script (call to f_4a3b211335f38c87bc0465bb0b6b0c9780-
f4be41): @user_script:2: script aborted for user request
(Lua debugging session ended)
那么数据库将只会包含脚本第1行代码设置的msg键:
redis> KEYS *
1) "msg"
为了避免出现类似问题,我们在进行调试,特别是在同步模式下进行调试时,如果要中途停止调试,最好还是使用abort
命令退出调试会话,从而尽可能地避免意料之外的情况发生。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。