前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >redis之初识lua脚本

redis之初识lua脚本

原创
作者头像
墨紫羽墨
发布2023-01-30 17:52:03
2K0
发布2023-01-30 17:52:03
举报
文章被收录于专栏:FutureTesterFutureTester

3.6、Lua脚本

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环境中内置了一些非常有用的包,通过使用这些包,用户可以直接在服务器端对数据进行处理,然后把它们存储到数据库中,这可以有效地减少不必要的网络传输。

3.6.1、EVAL:执行脚本

用户可以使用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为开始的。
代码语言:shell
复制
redis> EVAL "return 'hello world'" 0
"hello world"

这个命令将脚本"return 'hello world'"传递给了Lua环境执行,其中Lua关键字return用于将给定值返回给脚本调用者,而'hello world'则是被返回的字符串值。跟在脚本后面的是numkeys参数的值0,说明这个脚本不需要对Redis的数据库键进行处理。除此之外,这个命令也没有给定任何arg参数,说明这个脚本也不需要任何附加参数。

使用脚本执行Redis命令

Lua脚本的强大之处在于它可以让用户直接在脚本中执行Redis命令,这一点可以通过在脚本中调用redis.call()函数或者redis.pcall()函数来完成:

redis.call(command, ...), redis.pcall(command, ...)

代码语言:shell
复制
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表格:

代码语言:shell
复制
-- 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-lua.png
redis-lua.png
redis-lua-1.png
redis-lua-1.png
redis-lua-2.png
redis-lua-2.png
代码语言:shell
复制
redis> EVAL "return 3.14" 0  -- 因为带有小数部分的Lua数字将被转换为Redis整数回复
(integer) 3

redis> EVAL "return tostring(3.14)" 0  -- 先使用Lua内置的tostring()函数将它转换为字符串
"3.14"
在脚本中切换数据库
代码语言:shell
复制
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

代码语言:lua
复制
redis.call("SET", KEYS[1], ARGV[1])
return redis.call("GET", KEYS[1])

在命令行执行

代码语言:text
复制
$ ./redis-cli --eval ../script/set_and_get.lua 'msg' , 'ciao'
"ciao"
key key … 和 arg arg … 之间的“ , ”,英文逗号前后必须有空格,否则报错。
3.6.2、SCRIPT LOAD和EVALSHA:缓存并执行脚本

在定义脚本之后,程序通常会重复地执行脚本。一个简单的脚本可能只有几十到上百字节,而一个复杂的脚本可能会有数百字节甚至数千字节,如果客户端每次执行脚本都需要将相同的脚本重新发送一次,这对于宝贵的网络带宽来说无疑是一种浪费。

为了解决上述问题,Redis提供了Lua脚本缓存功能,这一功能允许用户将给定的Lua脚本缓存在服务器中,然后根据Lua脚本的SHA1校验和直接调用脚本,从而避免了需要重复发送相同脚本的麻烦。

命令SCRIPT LOAD可以将用户给定的脚本缓存在服务器中,并返回脚本对应的SHA1校验和作为结果:SCRIPT LOAD script

代码语言:shell
复制
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 ...]

代码语言:shell
复制
redis> EVALSHA "5332031c6b470dc5a0dd9b4bf2030dea6d65de91" 0
"hello world"

redis> EVALSHA "18d788194860a281b19910d462b1e96dabf3c984" 1 "msg" "Ciao!"
"Ciao!"

除了SCRIPT LOAD之外,EVAL命令在执行完脚本之后也会把被执行的脚本缓存起来,以供之后使用。

3.6.3、脚本管理
SCRIPT EXISTS:检查脚本是否已被缓存

SCRIPT EXISTS命令接受一个或多个SHA1校验和作为参数,检查这些校验和对应的脚本是否已经被缓存到了服务器中:SCRIPT EXISTS sha1 [sha1 ...]

代码语言:shell
复制
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命令将移除服务器已缓存的所有脚本,这个命令一般只会在调试时使用:SCRIPT FLUSH

代码语言:shell
复制
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
SCRIPT KILL:强制停止正在运行的脚本

因为Lua脚本在执行时会独占整个服务器,所以如果Lua脚本的运行时间过长,又或者因为编程错误而导致脚本无法退出,那么就会导致其他客户端一直无法执行命令。

配置选项lua-time-limit的值定义了Lua脚本可以不受限制运行的时长,这个选项的默认值为5000:lua-time-limit <milliseconds>

当脚本的运行时间低于lua-time-limit指定的时长时,其他客户端发送的命令请求将被阻塞;相反,当脚本的运行时间超过lua-time-limit指定的时长时,向服务器发送请求的客户端将得到一个错误回复,提示用户可以使用SCRIPT KILL或者SHUTDOWN NOSAVE命令来终止脚本或者直接关闭服务器。

-- 客户端1

代码语言:shell
复制
client-1> EVAL "repeat until false" 0  -- 无限循环

等待5s

-- 客户端2

代码语言:shell
复制
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命令之后,服务器可能会有以下两种反应:

  • 如果正在运行的Lua脚本尚未执行过任何写命令,那么服务器将终止该脚本,然后回到正常状态,继续处理客户端的命令请求。
  • 如果正在运行的Lua脚本已经执行过写命令,并且因为该脚本尚未执行完毕,所以它写入的数据可能是不完整或者错误的,为了防止这些脏数据被保存到数据库中,服务器是不会直接终止脚本并回到正常状态的。在这种情况下,用户只能使用SHUTDOWN nosave命令,在不执行持久化操作的情况下关闭服务器,然后通过手动重启服务器来让它回到正常状态。
代码语言:shell
复制
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> -- 服务器已被关闭
3.6.4、内置函数库

Redis在Lua环境中内置了一些函数库,用户可以通过这些函数库对Redis服务器进行操作,或者对给定的数据进行处理,这些函数库分别是:

  • base包
  • table包
  • string包
  • math包
  • redis包
  • bit包
  • struct包
  • cjson包
  • cmsgpack包
redis包

除了前面已经介绍过的redis.call()函数和redis.pcall()函数之外,redis包还包含了以下函数:

  • redis.log()
  • redis.sha1hex()
  • redis.error_reply()
  • redis.status_reply()
  • redis.breakpoint()
  • redis.debug()
  • redis.replicate_commands()
  • redis.set_repl()
1、redis.log()函数

redis.log()函数用于在脚本中向Redis服务器写入日志,它接受一个日志等级和一条消息作为参数:redis.log(loglevel, message)

其中loglevel的值可以是以下4个日志等级的其中一个,这些日志等级与Redis服务器本身的日志等级完全一致:

  • redis.LOG_DEBUG
  • redis.LOG_VERBOSE
  • redis.LOG_NOTICE
  • redis.LOG_WARNING 当给定的日志等级超过或等同于Redis服务器当前设置的日志等级时,Redis服务器就会把给定的消息写入日志中。
代码语言:shell
复制
redis> EVAL "redis.log(redis.LOG_WARNING, 'Something wrong!')" 0

redis-server 将写一条日志:

代码语言:text
复制
1276417:M 27 Sep 2022 14:19:08.395 # Something wrong!
2、redis.shalhex()函数

redis.sha1hex()函数可以计算出给定字符串输入的SHA1校验和:redis.sha1hex(string)

代码语言:shell
复制
redis> EVAL "return redis.sha1hex('show me your sha1')" 0
"e00ecdbe6ea77b31972c28dccad6aceba9822a12"
3、redis.error_reply()函数和redis.status_reply()函数

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字段的值则是给定的状态消息。

代码语言:shell
复制
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包

bit包提供了将数字转换为十六进制字符串的tohex()函数,以及对给定的数字执行按位反、按位或、按位并以及按位异或的bnot()bor()band()bxor()等函数:

代码语言:txt
复制
bit.tohex(x [,n])
bit.bnot(x)
bit.bor(x1 [,x2...])
bit.band(x1 [,x2...])
bit.bxor(x1 [,x2...])
代码语言:shell
复制
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包

struct包提供了能够在Lua值以及C结构之间进行转换的基本设施,这个包提供了pack()unpack()以及size()这3个函数:

代码语言:txt
复制
struct.pack (fmt, v1, v2, ...)
struct.unpack (fmt, s, [i])
struct.size (fmt)

其中struct.pack()用于将给定的一个或多个Lua值打包为一个类结构字符串(struct-like string),struct.unpack()用于从给定的类结构字符串中解包出多个Lua值,而struct.size()则用于计算按照给定格式进行打包需要耗费的字节数量。

代码语言:shell
复制
-- 打包一个浮点数、一个无符号长整数以及一个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包

cjson包能够为Lua脚本提供快速的JSON编码和解码操作,这个包中最常用的就是将Lua值编码为JSON数据的编码函数encode(),以及将JSON数据解码为Lua值的解码函数decode()

代码语言:txt
复制
cjson.encode(value)
cjson.decode(json_text)
代码语言:shell
复制
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包

cmsgpack包能够为Lua脚本提供快速的MessagePack打包和解包操作,这个包中最常用的就是打包函数pack()以及解包函数unpack(),前者可以将给定的任意多个Lua值打包为msgpack包,而后者则可以将给定的msgpack包解包为任意多个Lua值:

代码语言:txt
复制
cmsgpack.pack(arg1, arg2, ..., argn)
cmsgpack.unpack(msgpack)
代码语言:shell
复制
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"
3.6.5、脚本调试
一个简单的调试示例

/script/debug.lua

代码语言:lua
复制
local ping_result = redis.call("PING")
local set_result = redis.call("SET", KEYS[1], ARGV[1])
return {ping_result, set_result}
代码语言:text
复制
-- 要将--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>提示符。

调试器操作命令:

  • quit——退出调试会话并关闭客户端。
  • restart——重新启动调试会话。
  • help——列出可用的调试命令。
调试命令
redis-lua-调试命令1.png
redis-lua-调试命令1.png
redis-lua-调试2.png
redis-lua-调试2.png

因为调试程序通常需要重复执行多次相同的调试命令,为了让枯燥的调试过程变得稍微愉快和容易一些,Redis为每个调试命令都设置了一个缩写,即执行命令的快捷方式,这些缩写就是命令开头的首个字母,用户只需要输入这些缩写,就可以执行相应的调试命令。

断点

在一般情况下,我们将以单步执行的方式对脚本进行调试,也就是说,使用next命令执行一个代码行,观察一下执行的结果,在确认没有问题之后,继续使用next命令执行下一个代码行,以此类推,直到整个脚本都被执行完毕为止。

但是在有需要的情况下,也可以通过break命令给脚本增加断点,然后使用continue命令执行代码,直到遇见下一个断点为止。

/script/breakpoint.lua

代码语言: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")
代码语言:text
复制
$ ./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命令除了可以用于添加断点之外,还可用于显示已有断点以及移除断点

代码语言:text
复制
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

代码语言: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
代码语言:text
复制
$ ./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

代码语言: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
代码语言:text
复制
$ ./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调试器提供了evalredis这两个调试命令,用户可以使用前者来执行指定的Lua代码,并使用后者来执行指定的Redis命令,也可以通过这两个调试命令来快速地验证一些想法以及结果,这会给程序的调试带来很多好处。

比如,如果我们在调试某个脚本时,需要知道某个字符串对应的SHA1校验和,那么只需要使用eval命令调用Lua环境内置的redis.sha1hex()函数即可:

代码语言:txt
复制
lua debugger> eval redis.sha1hex('hello world')
<retval> "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"

又比如,如果我们在调试某个会对数据库进行操作的脚本时,想要知道某个键的当前值,那么只需要使用redis命令执行相应的数据获取命令即可:

代码语言:txt
复制
lua debugger> redis GET msg
<redis> GET msg
<reply> "hello world"
显示调用链

trace调试命令可以打印出脚本的调用链信息,这些信息在研究脚本的调用路径时会非常有帮助。

/script/trace.lua

代码语言:lua
复制
local f1 = function()
local f2 = function()
local f3 = function()
redis.breakpoint()
end
f3()
end
f2()
end

f1()
代码语言:text
复制
$ 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

代码语言:lua
复制
redis.call('PING')
redis.call('PING')
redis.call('P1NG') -- 错别字
redis.call('PING')
redis.call('PONG') -- 错别字
代码语言:text
复制
$ 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,重新开始调试:

代码语言:shell
复制
redis> restart
* Stopped at 1, stop reason = step over
-> 1 redis.call('PING')

lua debugger> 

之后,调试器继续执行脚本,并在脚本的第5行停了下来,修改文件,将调用中的PONG修改为PING,然后再次在客户端中输入restart并重新开始调试

经过修改之后,脚本终于可以顺利地执行了:

代码语言:lua
复制
lua debugger> continue

(nil)

(Lua debugging session ended -- dataset changes rolled back)

redis>
调用模式

Redis的Lua调试器支持两种不同的调试模式,一种是异步调试,另一种则是同步调试。当用户以ldb选项启动调试会话时,Redis服务器将以异步方式调试脚本:

redis-cli --ldb --eval script.lua

运行在异步调试模式下的Redis服务器会为每个调试会话分别创建新的子进程,并将其用作调试进程:

  • 因为Redis服务器可以创建出任意多个子进程作为调试进程,所以异步调试允许多个调试会话同时存在,换句话说,异步调试模式允许多个用户同时进行调试。
  • 因为异步调试是在子进程而不是服务器进程上进行,它不会阻塞服务器进程,所以在异步调试的过程中,其他客户端可以继续访问Redis服务器。
  • 因为异步调试期间执行的所有Lua代码以及Redis命令都是在子进程上完成的,所以在调试完成之后,调试期间产生的所有数据修改也会随着子进程的终结而消失,它们不会对Redis服务器的数据库产生任何影响。

当用户以ldb-sync-mode选项启动调试会话时,Redis服务器将以同步方式调试脚本:

redis-cli --ldb-sync-mode --eval script.lua

运行在同步调试模式下的Redis服务器将直接使用服务器进程作为调试进程:

  • 因为同步调试不会创建任何子进程,而是直接使用服务器进程作为调试进程,所以同一时间内只能有一个调试会话存在。换句话说,同步调试模式只允许单个用户进行调试。
  • 因为同步调试直接在服务器进程上进行,它需要独占整个服务器,所以在整个同步调试过程中,其他客户端对服务器的访问都会被阻塞。
  • 因为在同步调试期间,所有Lua代码以及Redis命令都是直接在服务器进程上执行的,所以调试期间产生的数据修改将保留在服务器的数据库中。
代码语言:text
复制
$ 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键将不会在数据库中出现:

代码语言:txt
复制
$ redis-cli
redis> KEYS *
(empty list or set)

与此相反,如果我们在相同的Redis服务器上进行同步调试,并执行相同的设置操作:

代码语言:text
复制
$ 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键将继续保留在服务器的数据库中:

代码语言:shell
复制
$ redis-cli 
redis> KEYS *
1) "msg"
终止调试会话

在调试Lua脚本时,用户有3种方法可以退出调试会话:

  • 当脚本执行完毕时,调试会话将自然终止,客户端也会从调试状态退回到普通状态。
  • 当用户在调试器中按下Ctrl+C键时,调试器将在执行完整个脚本之后终止调试会话。
  • 当用户在调试器中执行abort命令时,调试器将不再执行任何代码,直接终止调试会话。

我们需要特别注意方法2和方法3之间的区别,因为对于一些脚本来说,使用这两种退出方法可能会产生不一样的结果。

举个例子,对于代码清单14-10所示的脚本:

/script/set_strings.lua

代码语言:lua
复制
redis.call("SET", "msg", "hello world")
redis.call("SET", "database", "redis")
redis.call("SET", "number", 10086)

如果我们使用同步模式调试这个脚本,并在执行脚本的第一行代码之后按Ctrl+C键退出调试,那么调试器将在执行完整个脚本之后退出调试会话:

代码语言:shell
复制
$ 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命令都被执行了:

代码语言:shell
复制
redis> KEYS *
1) "number"
2) "database"
3) "msg"

此外,如果我们使用相同的模式调试相同的脚本,但是在执行脚本的第1行之后使用abort命令退出调试:

代码语言:text
复制
$ 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键:

代码语言:shell
复制
redis> KEYS *
1) "msg"

为了避免出现类似问题,我们在进行调试,特别是在同步模式下进行调试时,如果要中途停止调试,最好还是使用abort命令退出调试会话,从而尽可能地避免意料之外的情况发生。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 3.6、Lua脚本
    • 3.6.1、EVAL:执行脚本
      • 使用脚本执行Redis命令
      • 值转换
      • 在脚本中切换数据库
      • 脚本的原子性
      • 以命令行方式执行脚本
    • 3.6.2、SCRIPT LOAD和EVALSHA:缓存并执行脚本
      • 3.6.3、脚本管理
        • SCRIPT EXISTS:检查脚本是否已被缓存
        • SCRIPT FLUSH:移除所有已缓存脚本
        • SCRIPT KILL:强制停止正在运行的脚本
      • 3.6.4、内置函数库
        • redis包
        • bit包
        • struct包
        • cjson包
        • cmsgpack包
      • 3.6.5、脚本调试
        • 一个简单的调试示例
        • 调试命令
        • 断点
        • 动态断点
        • 输出调试日志
        • 执行指定的代码或命令
        • 显示调用链
        • 重载脚本
        • 调用模式
        • 终止调试会话
    相关产品与服务
    云数据库 Redis
    腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
    http://www.vxiaotou.com