前言
在实际的项目中引入了 redisson 这样优秀的开源框架,在研究其源码的时候,发现其使用了大量的 lua 脚本,用于处理一些比较复杂的功能。秉承着不能只知其一的原则,了解一些 redis 嵌入的 lua 引擎所支持的特性。
因为之前学习过 lua 和 redis 源码,此处只做一些 api 方面的记录和在编写和 redis 交互的lua 脚本是需要注意的地方,防止踩坑。
Redis包含一个嵌入式Lua 5.1解释器。解释器运行用户定义的临时脚本和函数。脚本在沙盒上下文中运行,并且只能访问特定的Lua包。
沙盒上下文的意思就是 脚本应该只对存储在Redis中的数据和作为执行参数提供的数据进行操作,不应该访问Redis服务器的底层主机系统(如:文件系统、网络和执行非API支持的系统调用的任何其他尝试)。 沙盒化的上下文也阻止了全局变量和函数的声明,在声明时都需要加 local 作为访问修饰符。 如下:
local my_local_variable = 'some value'
local function my_local_function()
-- Do something else, but equally amazing
end
在沙盒上下文中不支持导入使用Lua模块。只能使用 redis 自带的库。因为禁用了Lua的require函数,以此来阻止加载模块。
全局变量
- redis
- KEYS
- ARGV
上述变量从 2.6.0 版本就开始提供,所以只要不是远古时期的项目,都应该可用。
redis api
- redis.call(command [,arg…])
该函数调用给定的Redis命令并返回其响应。它的输入是命令和参数,一旦被调用,它在Redis中执行命令并返回响应。
例如,我们可以从脚本中调用ECHO命令并返回它的响应,如下所示:
return redis.call('ECHO', 'Echo, echo... eco... o...')
如果触发运行时异常,则会自动将原始异常作为错误返回给用户。因此,尝试执行以下临时脚本将失败并生成运行时异常,因为ECHO只接受一个参数:
redis> EVAL "return redis.call('ECHO', 'Echo,', 'echo... ', 'eco... ', 'o...')" 0
(error) ERR Wrong number of args calling Redis command from script script: b0345693f4b77517a711221050e76d24ae60b7f7, on @user_script:1.
- redis.pcall(command [,arg…])
local reply = redis.pcall('ECHO', unpack(ARGV))
if reply['err'] ~= nil then
-- Handle the error sometime, but for now just log it
redis.log(redis.LOG_WARNING, reply['err'])
reply['err'] = 'ERR Something is wrong, but no worries, everything is under control'
end
return reply
此函数允许处理由Redis服务器引发的运行时错误,上述代码就是演示从临时脚本的上下文中拦截和处理运行时异常。
- redis.error_reply(x)
这是一个返回错误响应的辅助函数。接受单个字符串参数并返回一个Lua表,其中err字段设置为该字符串。
local text = 'ERR My very special error'
local reply1 = { err = text }
local reply2 = redis.error_reply(text)
-- 在这里 return reply1/reply2 效果是一样的
- redis.status_reply(x)
这里是返回正确回复的,使用方式和 第3点相同。
local text = 'Frosty'
local status1 = { ok = text }
local status2 = redis.status_reply(text)
-- 在这里 return reply1/reply2 效果是一样的
- redis.sha1hex(x)
这个函数返回其单个字符串参数的SHA1十六进制摘要。
-- 获取 空字符串的 sha1 摘要
redis> EVAL "return redis.sha1hex('')" 0
"da39a3ee5e6b4b0d3255bfef95601890afd80709"
- redis.log(level, message)
该函数主要是可以输出 redis server 日志。
level 选项如下:
- redis.LOG_DEBUG
- redis.LOG_VERBOSE
- redis.LOG_NOTICE
- redis.LOG_WARNING
- redis.setresp(x)
version 6.0.0
该函数允许执行脚本在 redis.call 和 redis.pcall 返回的响应之间切换 Redis Serialization Protocol(RESP)版本。它需要一个数字参数作为协议的版本。默认的协议版本是2,但可以切换到版本3。
redis.setresp(3)
- redis.register_function
version: 7.0.0
此函数仅在function LOAD命令的上下文中可用。当被调用时,它将一个函数注册到加载的库中。可以使用位置参数或命名参数调用该函数。
Named arguments: redis.register_function{function_name=name, callback=callback, flags={flag1, flag2, …}, description=description}
The named arguments variant accepts the following arguments:
function_name: the function’s name.
callback: the function’s callback.
flags: an array of strings, each a function flag (optional).
description: function’s description (optional).
redis> FUNCTION LOAD "#!lua name=mylib\n redis.register_function{function_name='noop', callback=function() end, flags={ 'no-writes' }, description='Does nothing'}"
更多详情请看:register_function
- redis.REDIS_VERSION
version: 7.0.0
以Lua字符串的形式返回当前Redis服务器版本。回复的格式是 MM.mm.PP 。
- MM:主版本。
- mm: 小版本。
- PP:补丁级别。
- select index
可以在脚本中世界使用 select 命令已达到切换库的能力。
数据类型转换
Redis的响应和Lua的数据类型之间是一对一的映射,Lua的数据类型和Redis协议的数据类型之间是一对一的映射。从Redis协议响应(即 redis.call 和 redis.pcall )到Lua数据类型的类型转换取决于脚本使用的Redis序列化协议版本。脚本执行期间的默认协议版本是RESP2。脚本可以通过调用 redis.setresp 函数来切换应答的协议版本。
RESP2 转换 lua 类型
- RESP2 integer reply -> Lua number
- RESP2 bulk string reply -> Lua string
- RESP2 array reply -> Lua table (may have other Redis data types nested)
- RESP2 status reply -> Lua table with a single ok field containing the status string
- RESP2 error reply -> Lua table with a single err field containing the error - string
- RESP2 null bulk reply and RESP2 null multi-bulk reply -> Lua false boolean type
lua 类型转换 RESP2
- Lua number -> RESP2 integer reply (the number is converted into an integer)
- Lua string -> RESP2 bulk string reply
- Lua table (indexed, non-associative array) -> RESP2 array reply (truncated at the first Lua nil value encountered in the table, if any)
- Lua table with a single ok field -> RESP2 status reply
- Lua table with a single err field -> RESP2 error reply
- Lua boolean false -> RESP2 null bulk reply
- Lua Boolean true -> RESP2 integer reply with value of 1. 这是一个额外的规则,RESP2 转换成 lua 的时候并没有。
特殊情况
-- 正确的使用方式
redis> EVAL "return 10" 0
(integer) 10
redis> EVAL "return { 1, 2, { 3, 'Hello World!' } }" 0
1) (integer) 1
2) (integer) 2
3) 1) (integer) 3
1) "Hello World!"
redis> EVAL "return redis.call('get','foo')" 0
"bar"
-- 1. Lua只有一个数字类型,Lua数字。整数和浮点数之间没有区别。因此,直接将数字类型返回会丢失数字的小数部分(如果有的话)。如果你想返回一个Lua浮点数,它应该作为一个字符串返回 如下 3.3333 已经被丢弃。
-- 2. 由于Lua的表语义,没有简单的方法可以在Lua数组中使用nil。因此,当Redis将Lua数组转换为 RESP2 时,当遇到Lua nil值时转换停止 如下: nil 后面的 bar 被丢弃。
-- 3. 当一个Lua表是一个包含键和它们各自值的关联数组时,转换后的Redis响应将丢弃,如下: somekey = 'somevalue'。
redis> EVAL "return { 1, 2, 3.3333, somekey = 'somevalue', 'foo', nil , 'bar' }" 0
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) "foo"
注意: 在实际场景中并没有遇到使用 RESP3 的场景,有想法的可以继续了解,此处不在说明。 resp3-to-lua-type-conversion
运行时库
标准的lua库 Lua 官方文档:
- The String Manipulation (string) library
- The Table Manipulation (table) library
- The Mathematical Functions (math) library
- The Operating System Facilities (os) library
此外,脚本还可以加载并访问以下外部库 外部库文档说明:
- The struct library
- The cjson library
- The cmsgpack library
- The bitop library
总结
Redis保证脚本的原子执行。在执行脚本时,所有服务器活动在整个运行时期间都被阻塞。这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。我们使用 lua 脚本的时可以根据这个特性以达到数据一致性的目的,但是脚本尽可能不要复杂,毕竟会影响性能,在网络开销和脚本执行之间平衡。毕竟在网络中,不可能百分之百的保证环境是不变的,只能在复杂的情况中选取对自己有利的。