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
进入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 之旅
新增断点
顺序执行
输入n 敲回车键
打印变量
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脚本来实现分布式锁、秒杀等业务场景。