Redis Lua脚本 & Debug

Redis 编程接口之Lua脚本

Redis 使用Lua脚本和Redis Functions扩展其功能。Redis提供编程接口,允许开发者在服务器执行自定义的脚本,对于不同的版本,实现的方式略有不同

  • Redis 7 及以上版本 使用Redis Functions 管理、运行脚本
  • Redis 6.2及之前的版本 使用Lua脚本和EVAL命令对Redis 服务进行编程

首先,自Redis 2.6.0以来,EVAL命令支持运行服务器端脚本。Eval脚本提供了一种让Redis临时运行脚本的快速、直接的方法。然而,使用它们意味着脚本逻辑是应用程序的一部分(而不是Redis服务器的扩展)。运行脚本的每个应用程序实例必须具有随时可供加载的脚本源代码。这是因为脚本只由服务器缓存,并且是不稳定的。随着应用程序的增长,这种方法可能变得更难开发和维护。

其次,在Redis7.0中添加的Redis Functions本质上是数据库元素的脚本。因此,函数将脚本与应用程序逻辑分离,并支持脚本的独立开发、测试和部署。要使用函数,需要首先加载它们,然后所有连接的客户端都可以使用它们。在这种情况下,将函数加载到数据库成为一项管理部署任务(例如加载Redis模块),将脚本与应用程序分离。

只读脚本

只读脚本是只执行却并不修改Redis服务器中任何键命令的脚本。只读脚本通过添加 no-writes 标志来执行;或者执行任意只读命令(EVAL_RO,、EVALSHA_RO、 FCALL_RO)。只读脚本具有以下属性:

  • 只读脚本在副本上照样运行
  • 可以被SCRIPT KILL 命令杀死
  • Redis 内存超过限制时,不会因为OOM导致失败
  • 写入不会被阻塞,例如在协调故障切换期间发生的暂停
  • 无法执行任何可能修改数据集的命令
  • PUBLISH、SPUBLISH、PFCOUNT被认为是写入命令

执行时长

脚本的最大执行时间(默认设置为5秒),这个默认值在通常情况下是合理。一般的业务场景下,脚本的运行时长不超过一毫秒。这个限制是用来处理开发过程中意外产生的无限循环的。

可以通过CONFIG SET 命令;或者修改redis.conf中busy-reply-threshold属性来修改默认值。

当脚本达到超时阈值时,Redis不会自动终止脚本。这样做将违反Redis和脚本引擎之间的约定,后者确保脚本是原子的。

Lua脚本

初识lua

> EVAL "return 'Hello, scripting!'" 0
"Hello, scripting!"

在上面的示例中国,EVAL携带两个参数。第一个参数是由Lua源代码组成的字符串。第二个参数是lua脚本正文后面的参数序号,从第三个参数开始,表示Redis键名称。在本例中,我们使用了值0,因为我们没有为脚本提供任何参数。

脚本参数

redis> EVAL "return ARGV[1]" 0 Hello
"Hello"
redis> EVAL "return ARGV[1]" 0 Parameterization!
"Parameterization!"

Redis 服务器执行上下文通过KEYS、ARGV关键字为脚本提供参数。KEYS代表脚本中的 Redis 键信息;ARGV 关键字代表脚本中的参数信息。在上面的示例中,只有ARGV,因此整个lua脚本不需要填充键信息,因此指定数字为0;Hello和Parameterization!作为脚本的常规输入参数。

请看接下来的示例,需要输入2个键参数,因此参数后面接数字2,后续2个参数为键参数,其他参数为ARGV参数

redis> EVAL "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }" 2 key1 key2 arg1 arg2 arg3
1) "key1"
2) "key2"
3) "arg1"
4) "arg2"
5) "arg3"

Redis交互

可以通过在Lua脚本中调用Redis.call()、Redis.pcall()调用Redis命令。这两个命令的功能完全相同,都能通过动态参数执行Redis命令。区别在于:二者处于运行时错误的处理方式。

  • 调用redis.call()函数产生的错误将直接返回到执行它的客户端
  • 调用redis.pcall()函数时遇到的错误将返回到脚本的执行上下文中,以进行可能的处理
> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo bar
OK

上面的脚本接收2个参数,Key值、Value值。脚本的执行效果跟 set foo bar 直接调用命令行一致

脚本缓存

在之前的示例中,都是通过调用EVAL命令运行脚本。假设脚本逻辑复杂导致源代码很多。重复调动EVAL来执行相同的脚本,会造成网络带宽浪费。

由此,Redis为脚本提供了一种缓存机制,该方式类似数据库中的存储过程,先将复杂的逻辑通过函数的方式固定到服务器,然后通过函数名 + 参数 指定动态逻辑。

具体的实现是调用script LOAD命令并提供其源代码,将脚本加载到服务器的缓存中。服务器不执行脚本,而是只编译脚本并将其加载到服务器的缓存中。加载后,可以使用从服务器返回的SHA1摘要执行缓存脚本。

redis> SCRIPT LOAD "return 'Immabe a cached script'"
"c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f"
redis> EVALSHA c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f 0
"Immabe a cached script"

Redis脚本缓存是不稳定的。它不被视为数据库的一部分,也不被持久化。当服务器重新启动时、复制副本承担主角色时的故障切换期间,或由SCRIPT FLUSH明确清除缓存。这意味着缓存的脚本是短暂的,缓存的内容随时可能丢失。

使用脚本的应用程序应始终调用EVALSHA来执行脚本。如果脚本的SHA1摘要不在缓存中,服务器将返回错误。例如:

redis> EVALSHA ffffffffffffffffffffffffffffffffffffffff 0
(error) NOSCRIPT No matching script

在Pipeline请求中执行EVALSHA函数需要特别注意,pipeline中请求的命令按发送顺序运行,但其他客户端的命令可能会在这些命令之间交错执行。因此,NOSCRIPT错误可能从流水线请求返回,但无法处理。因此,客户端库在pipeline请求中应使用EVAL命令执行lua脚本。

脚本维护

Redis 服务器提供了几个命令对lua脚本进行维护:

  • SCRIPT FLUSH - 该命令会强制刷新(删除)Redis服务中的所有脚本信息
  • SCRIPT EXISTS - 给定一个或多个SHA1作为参数,判断脚本是否存在。返回1 即存在;0 不存在
  • SCRIPT LOAD script - 之前用到过 在Redis服务端注册缓存指定脚本
  • SCRIPT KILL - 该命令是中断长时间运行的脚本(也称为慢脚本)的唯一方法,而不是关闭服务器。一旦脚本的执行持续时间超过配置的最大执行时间阈值,则该脚本被视为缓慢。SCRIPT KILL命令只能用于在执行过程中未修改数据集的脚本(因为停止只读脚本不会违反脚本引擎保证的原子性)。
  • SCRIPT DEBUG

Lua 脚本DEBUG

测试脚本

-- 创建 script.lua 文件 内容如下
local pong = redis.call('ping')
local data = 'world'
redis.debug('hello',data)
redis.log(redis.LOG_WARNING, "foo bar")
return pong
# 验证脚本是否能正常运行
AndydeMacBook-Pro:tmp andy$ redis-cli --eval script.lua 
PONG

启动Debug

# 执行脚本,进入debug模式 如图所示
redis-cli --ldb --eval script.lua

Redis Lua脚本 & Debug_第1张图片

进入Debug模式后,可以相关帮助信息

  • quit - 退出debug模式
  • restart - 重新以debug模式执行脚本
  • help - 查看帮助文档

测试指令

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). 执行lua代码
[r]edis <cmd>        Execute a Redis command.  执行redis执行
[m]axlen [len]       Trim logged Redis replies and Lua var dumps to len.
                     Specifying zero as <len> means unlimited.
[a]abort             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 as if there was a breakpoint in the
                     next line of code.

Debug 之旅

新增断点

Redis Lua脚本 & Debug_第2张图片

顺序执行

输入n 敲回车键

Redis Lua脚本 & Debug_第3张图片

打印变量
lua debugger> print pong
<value> {["ok"]="PONG"}
Debug 日志

执行到第三步时 会在debug 控制台输出 信息 如下

lua debugger> n
<debug> line 3: "hello", "world"
* Stopped at 4, stop reason = break point
->#4   redis.log(redis.LOG_WARNING, "foo bar")
服务端日志

接着一步步执行,直至Debug完毕即可,需要注意的是,在示例中 第四行 会在Redis 服务端打印日志。

总结

使用Redis Lua脚本最大的优势是业务代码原子执行,Redis 服务能保证Lua脚本中的操作要么全部成功,要么全部失败,由此来弥补Redis 多个指令之间不能原子执行的问题。基于这个特性,通常使用Lua脚本来实现分布式锁、秒杀等业务场景。

你可能感兴趣的:(Redis,Redis,Lua脚本,Lua脚本Debug)