Redis 如何调试Lua 脚本
概述
从Redis 3.2
开始,内置了 Lua debugger
(简称LDB
),使用Lua debugger
可以很方便的对我们编写的Lua
脚本进行调试
快速开始
可以使用下面的步骤创建一个新的debug
会话:
-
在本地创建一个 Lua
脚本 -
使用 redis-cli
,通过--ldb
参数进入到debug
模式,使用--eval
参数指定需要debug
的Lua
脚本
比如我本地创建了一个/tmp/script.lua
脚本文件,脚本内容如下:
local foo = redis.call('ping')
return foo
接下来可以使用redis-cli
对/tmp/script.lua
脚本进行调试:
$ redis-cli --ldb --eval /tmp/script.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 local foo = redis.call('ping')
lua debugger>
当我们执行完redis-cli --ldb --eval /tmp/script.lua
后,从输出信息可以看出,我们开启了一个debugging
会话,并且代码停在了第一行的位置,可以使用step
命令让脚本执行到下一行
lua debugger> step
<redis> ping
<reply> "+PONG"
* Stopped at 2, stop reason = step over
-> 2 return foo
输出信息中<redis> ping
表示脚本执行了ping
命令,<reply> "+PONG"
则是redis-server
的返回信息。执行完setp
命令后,代码不会继续执行,而是停在了第二行,再次执行step
命令,执行return foo
代码,此时脚本代码已经执行完,debugging
会话结束,并输出脚本返回结果
lua debugger> step
PONG
(Lua debugging session ended -- dataset changes rolled back)
完整的流程如下:
LDB 工作模式
默认情况下,当某个客户端向服务端发起debugging
会话的时候,并不会阻塞服务端,即服务端仍能正常的处理请求,而且也能同时处理多个debugging
会话,因为会话是并行的,并不会互相干扰
另外一点是,当debugging
会话结束的时候,Lua
脚本中对redis
数据的所有修改都会回滚,这样做的好处是当多个会话同时debug
的时候不会互相干扰,并且可以在不用清理/还原数据的情况下,使用restart
重新开启debug
会话时,使每次的执行效果都相同
首先执行下面命令初始化数据:
$ rpush list_a a b c d
(integer) 4
然后编写script.lua
脚本:
ocal src = KEYS[1]
local dst = KEYS[2]
local count = redis.call('llen',src)
while count > 0 do
local item = redis.call('rpop',src)
redis.call('lpush',dst,item)
count = count - 1
end
return redis.call('llen',dst)
该脚本的作用是将src
列表中的所有元素移动到dst
中,接下对该脚本进行调试
$ redis-cli --ldb --eval /tmp/script.lua list_a list_b
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 src = KEYS[1]
lua debugger> continue
(integer) 4
(Lua debugging session ended -- dataset changes rolled back)
在这里我们使用continue
命令(后面会介绍该命令的使用)让脚本直接执行完毕,然后在redis
中看看list_a
跟list_b
的值
$ lrange list_a 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
$ lrange list_b 0 -1
(empty array)
可以看到,redis
中的数据并没有发生改变,因为默认工作模式下,当debugging
会话关闭的时候,会将会话中的所有修改回滚
我们还可以通过--ldb-sync-mode
参数,指定LDB
为同步模式,在该工作模式下调试Lua
脚本会阻塞服务端,debug
的过程中服务端不会处理其他请求,并且不会对数据做回滚操作
使用同步模式执行之前的脚本
$ redis-cli --ldb-sync-mode --eval /tmp/script.lua list_a list_b
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 src = KEYS[1]
此时我们我们在另外一个客户端执行任何命令都会被阻塞,直到脚本执行完毕,接下来我们使用continue
命令让脚本执行完。然后查看list_a
跟list_b
的数据
$ lrange list_a 0 -1
(empty array)
$ lrange list_b 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
可以看到,list_a
中的元素已经移动至list_b
中了,说明在同步模式下不会对脚本操作进行回滚
断点
添加断点
在Lua
脚本中添加和删除端点都非常简单,可以在LDB
中,通过break <line>
添加端点,比如我们想在脚本的第五行添加断点,只需要在LDB
执行break 5
即可
$ redis-cli --ldb --eval /tmp/script.lua list_a list_b
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 src = KEYS[1]
lua debugger> break 5
4
#5 while count > 0 do
6 local item = redis.call('rpop',src)
添加完断点后,可以使用continue
命令,让脚本执行到断点处
lua debugger> continue
* Stopped at 5, stop reason = break point
->#5 while count > 0 do
break
后面可以跟多个行号,一次性添加多个断点,行号之间用空格隔开
查看断点
使用break
命令可以查看脚本中设置的所有断点
lua debugger> break
1 breakpoints set:
->#5 while count > 0 do
通过LDB
返回的信息,可以看到脚本中共设置了一个断点,且行号为 5
删除端点
断点可以使用break -<line>
进行删除,其中<line>
为要删除的断点的行号,如删除第五行的断点可以使用break -5
lua debugger> b -5
Breakpoint removed.
同样的,可以使用
break -1 -2
的方式批量删除多个断点
如果想一次性删除脚本中的所有断点,可以使用break 0
lua debugger> b 0
All breakpoints removed.
动态添加断点
除了使用break
命令设置断点外,还可以通过在脚本中调用redis.breakpoint()
函数的方式设置断点,当脚本执行到redis.breakpoint()
方法时,会在redis.breakpoint()
的下一行模拟一个断点,跟使用break
设置断点一样的效果,比如想在/tmp/script.lua
的 while 循环里面设置断点,且当count==3
时才执行进行debug
,可以将脚本修改为
local src = KEYS[1]
local dst = KEYS[2]
local count = redis.call('llen',src)
while count > 0 do
if (count == 3) then redis.breakpoint() end
local item = redis.call('rpop',src)
redis.call('lpush',dst,item)
count = count - 1
end
return redis.call('llen',dst)
使用LDB
对脚本进行 debug
$ redis-cli --ldb --eval /tmp/script.lua list_a list_b
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 src = KEYS[1]
lua debugger> continue
* Stopped at 7, stop reason = redis.breakpoint() called
-> 7 local item = redis.call('rpop',src)
可以看到,当执行contiue
时,脚本停在了第 7 行,在这里,我们可以使用print
命令打印count
的值
lua debugger> print count
<value> 3
发现count
的值为 3,证明redis.breakpoint()
命令生效了,且通过该方法,可以是我们动态的设置断点,当满足一定条件的情况下才进入断点
控制台输出
在平常开发中,我们习惯使用控制台输出的方式来打印一些提示信息(如变量值等),比如Java
中,我们可以使用System.out.print()
函数将信息输出到控制台。在Redis Lua
中,也提供了向控制台输出信息的方法redis.debug()
。redis.debug()
可以接收多个参数,参数之间用逗号隔开
比如下面脚本
local a = {1,2,3}
local b = false
redis.debug(a,b)
使用LDB
执行该脚本
$ redis-cli --ldb --eval /tmp/script.lua
Stopped at 1, stop reason = step over
-> 1 local a = {1,2,3}
lua debugger> continue
<debug> line 3: {1; 2; 3}, false
可以看到,当执行到redis.debug(a,b)
的时候,控制台输出了变量a
跟变量b
的值
常用命令介绍
在LDB
中,输入help
命令,可以查看LDB
支持的所有命令
lua debugger> help
Redis Lua debugger help:
[h]elp Show this help.
[s]tep Run current line and stop again.
[n]ext Alias for step.
[c]continue Run till next breakpoint.
[l]list List source code around current line.
[l]list [line] List source code around [line].
line = 0 means: current position.
[l]list [line] [ctx] In this form [ctx] specifies how many lines
to show before/after [line].
[w]hole List all source code. Alias for 'list 1 1000000'.
[p]rint Show all the local variables.
[p]rint <var> Show the value of the specified variable.
Can also show global vars KEYS and ARGV.
[b]reak Show all breakpoints.
[b]reak <line> Add a breakpoint to the specified line.
[b]reak -<line> Remove breakpoint from the specified line.
[b]reak 0 Remove all breakpoints.
[t]race Show a backtrace.
[e]eval <code> Execute some Lua code (in a different callframe).
[r]edis <cmd> Execute a Redis command.
[m]axlen [len] Trim logged Redis replies and Lua var dumps to len.
Specifying zero as <len> means unlimited.
[a]bort Stop the execution of the script. In sync
mode dataset changes will be retained.
Debugger functions you can call from Lua scripts:
redis.debug() Produce logs in the debugger console.
redis.breakpoint() Stop execution like if there was a breakpoint in the
next line of code.
这里命令的说明写的很清楚,其中有一些命令在上面也有用过,下面对其他的指令进行介绍
step | next
step
命令在快速开始有演示过,next
命令是step
的别名,使用step
或next
命令,可以执行当前行的代码,并且停在下一行
list [line]
查看line
行周围的脚本代码,特别地,当line=0
或不指定时,表示查看当前位置的行附近的代码
lua debugger> list
-> 1 local a = {1,2,3}
2 local b = false
3 redis.debug(a,b)
eval <code>
执行Lua
脚本
lua debugger> eval redis.call('ping')
<retval> {["ok"]="PONG"}
redis <cmd>
执行redis
命令
lua debugger> redis ping
<redis> ping
<reply> "+PONG"
abort
结束脚本执行,在同步模式下,执行过的脚本对数据的修改会被保留下来,即不会回滚
print
命令可以打印出脚本中的变量的值
lua debugger> list
1 local a = {1,2,3}
2 local b = false
-> 3 redis.debug(a,b)
lua debugger> print a
<value> {1; 2; 3}
当print
后面不指定变量名的时候,会打印所有变量及其值
lua debugger> list
1 local a = {1,2,3}
2 local b = false
-> 3 redis.debug(a,b)
lua debugger> print
<value> a = {1; 2; 3}
<value> b = false
命令缩写
LDB
在设计命令的时候,每个不同的命令的首字母都不一样,因此可以使用首字母缩写的方式代替命令全拼,如print
命令,可以使用其首字母p
代替