1. Redis函数
EVAL
执行 LUA
脚本存在的问题:
- 脚本可以被缓存,但是会存在丢失问题,例如重启服务器或者故障转移至副本时,客户端运行时需要重新加载
- 客户端应用程序实例必须维护所有脚本
- 在事务中调用缓存的脚本会增加事务失败的概率,因为可能缺少脚本
SHA1
散列值是无意义的,这使得在MONITOR
会话中调试系统变得极为困难- 使用
EVAL
时,往往会促使客户端应用程序直接使用脚本而不负责使用Lua API
中的KEYS
和ARGV
,这是一种反模式。
Redis v7.0
引入了 Redis Functions
(函数),是一种基于脚本的优化方案,提供与脚本相同的核心功能。通过持久化和复制保证可用性,不依赖于客户端,允许通过服务端加载自定义的函数库,并且会进行主从复制和持久化存储。支持多种语言,目前仅支持LUA
,后续计划支持 JavaScript
、Python
等。
2. 常用命令
2.1 FUNCTION LOAD
将一个库及函数加载到Redis
中。
语法格式:
FUNCTION LOAD [REPLACE] function-code
参数说明:
REPLACE
:加载已存在名称的库时,Redis
服务器将返回错误,使用REPLACE
可以强制进行覆盖function-code
:必填参数,即函数源代码
注意以下情况下会返回错误:
- 提供了无效的引擎名称。
- 在没有
REPLACE
修饰符的情况下,已存在该库名称。 - 在另一个库中已存在同名函数(即使指定了
REPLACE
)。 - 引擎在创建库函数时失败(例如由于编译错误)。
- 未声明任何函数。
示例:
redis> FUNCTION LOAD "#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)"
mylib
redis> FCALL myfunc 0 hello
"hello"
2.2 FUNCTION DELETE
删除一个库及其所有函数。如果该库不存在,则服务器会返回错误。
语法格式:
FUNCTION DELETE library-name
2.3 FUNCTION LIST
返回有关函数和库的信息。
语法格式:
FUNCTION LIST [LIBRARYNAME library-name-pattern] [WITHCODE]
可选参数说明:
LIBRARYNAME
:指定匹配库名称的模式WITHCODE
:包含库的源代码
响应信息:
library_name
:库的名称。engine
:库的引擎。functions
:库中函数的列表。- 每个函数具有以下字段:
name
:函数的名称。description
:函数的描述。flags
:函数标志的数组。library_code
:库的源代码(当使用WITHCODE
修饰符时)。
示例:
localhost:0>FUNCTION LIST
1) 1) "library_name"
2) "mylib"
3) "engine"
4) "LUA"
5) "functions"
6) 1) 1) "name"
2) "knockknock"
3) "description"
4) null
5) "flags"
6)
2.4 FUNCTION RESTORE
恢复已被序列化的库。
语法格式:
FUNCTION RESTORE serialized-value [FLUSH | APPEND | REPLACE]
指定处理现有库的策略:
APPEND
:将恢复的库追加到现有库中,并在冲突时中止。这是默认策略。FLUSH
:在恢复载荷之前删除所有现有库。REPLACE
:将恢复的库追加到现有库中,在名称冲突时替换现有库。请注意,此策略不会阻止函数名称的冲突,仅处理库名称冲突。
2.5 FUNCTION STATS
返回当前正在运行的函数以及可用执行引擎的信息。可以使用此命令检查长时间运行函数的调用,并决定是否使用 FUNCTION KILL
命令终止它。
语法格式:
FUNCTION STATS
返回键值对:
running_script
:关于正在运行脚本的信息。如果没有正在运行的函数,服务器将返回nil
。name
:函数的名称。command
:调用函数时使用的命令和参数。duration_ms
:函数的运行时长,以毫秒为单位。engines
:包含有关引擎的统计信息,如函数数量和库数量。
2.6 FUNCTION DUMP
返回已加载库的序列化内容,可以稍后使用 FUNCTION RESTORE
命令来恢复这些序列化的库。
语法格式:
FUNCTION DUMP
示例,使用 FUNCTION DUMP
命令转储已加载的库,然后调用 FUNCTION FLUSH
删除所有库。接着使用FUNCTION RESTORE
从序列化内容中恢复原始库:
redis> FUNCTION LOAD "#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)"
"mylib"
redis> FUNCTION DUMP
"\xf5\xc3@X@]\x1f#!lua name=mylib \n redis.registe\rr_function('my@\x0b\x02', @\x06`\x12\nkeys, args) 6\x03turn`\x0c\a[1] end)\x0c\x00\xba\x98\xc2\xa2\x13\x0e$\a"
redis> FUNCTION FLUSH
OK
redis> FUNCTION RESTORE "\xf5\xc3@X@]\x1f#!lua name=mylib \n redis.registe\rr_function('my@\x0b\x02', @\x06`\x12\nkeys, args) 6\x03turn`\x0c\a[1] end)\x0c\x00\xba\x98\xc2\xa2\x13\x0e$\a"
OK
redis> FUNCTION LIST
1) 1) "library_name"
2) "mylib"
3) "engine"
4) "LUA"
5) "functions"
6) 1) 1) "name"
2) "myfunc"
3) "description"
4) (nil)
5) "flags"
6) (empty array)
2.7 FUNCTION FLUSH
删除所有库。
语法格式:
FUNCTION FLUSH [ASYNC | SYNC]
有效的模式包括:
ASYNC
:异步地清空库。SYNC
:同步地清空库。
2.8 FUNCTION KILL
终止当前正在执行的函数。只能用于在执行过程中没有修改数据集的函数(因为终止只读函数不会违反脚本引擎的原子性保证)。
语法格式:
FUNCTION KILL
2.9 FCALL
调用函数。
语法格式:
FCALL function numkeys [key [key ...]] [arg [arg ...]]
参数说明:
function
:第一个参数是已加载函数的名称。numkeys
:key
参数的数量。[key [key ...]] [arg [arg ...]]
:和EVAL
命令一下,指定多个key
或参数。
示例:
redis> FUNCTION LOAD "#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)"
"mylib"
redis> FCALL myfunc 0 hello
"hello"
RESP2/RESP3 Reply
3. 案例演示
3.1 定义库
每个 Redis
函数都属于一个加载到 Redis
中的库,首先需要定义一个库,格式为:
1 |
|
各部分解释如下:
#!
:固定格式<engine name>
:使用的引擎,加载时会编译和评估库中的源代码,目前仅支持lua
<library name>
:库的名称
示例,定义一个 lua
引擎加载,名称为 mylib
的库:
1 |
|
每个库必须至少包含一个注册的函数才能成功加载,直接加载一个不包含函数的空库,会报错:
localhost:0>FUNCTION LOAD "#!lua name=mylib\n"
"ERR Invalid library metadata"
3.2 定义函数
定义好了库之后,可以在库中定义函数,格式为:
#!lua name=mylib
local function fun_name(keys, args)
return xx
end
各部分解释如下:
fun_name
:自定义的函数名称keys
:key
数组args
:参数数据return
:返回end
:函数结束标识
示例,获取某个字符串类型的值:
#!lua name=mylib
local function getV(keys)
local key = keys[1]
return redis.call('GET', key)
end
3.3 注册函数
定义好库和函数之后,还需要调用 redis.register_function()
来注册函数,这个 API
需要两个参数:
- 注册的函数名
- 回调函数
示例,直接使用匿名函数:
#!lua name=mylib
redis.register_function(
'knockknock',
function() return 'Who\'s there?' end
)
示例,外部定义函数:
#!lua name=mylib
local function my_hset(keys, args)
local hash = keys[1]
local time = redis.call('TIME')[1]
return redis.call('HSET', hash, '_last_modified_', time, unpack(args))
end
redis.register_function('my_hset', my_hset)
3.4 加载库和函数
定义好了库和函数后,需要加载到 Redis
服务器端,加载完成后会返回当前库名称。
使用 FUNCTION LOAD
命令进行加载:
127.0.0.1:6379> FUNCTION LOAD "#!lua name=mylib\nredis.register_function('knockknock', function() return 'Who\\'s there?' end)"
"mylib"
已存在当前库时,会报错:
(error) ERR Library 'mylib' already exists
也可以使用客户端 API
加载,Lettuce
示例:
// 创建客户端
RedisClient redisClient = RedisClient.create("redis://:123456@127.0.0.1:6379/0");
// 获取连接
try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
// 执行命令
RedisCommands<String, String> sync = connection.sync();
String msg = sync.functionLoad("#!lua name=mylib\nredis.register_function('knockknock', function() return 'Who\\'s there?' end)");
System.out.println(msg);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
redisClient.shutdown();
}
3.5 调用函数
可以使用 FCALL
调用函数,语法格式:
FCALL function numkeys [key [key ...]] [arg [arg ...]]
示例:
127.0.0.1:6379> FCALL knockknock 0
"Who's there?"
也可以使用客户端 API
调用,Lettuce
示例:
String res = sync.fcall("knockknock", ScriptOutputType.VALUE);
System.out.println(res);
4. 集群环境
在集群部署环境中,某个主节点的函数会自动向副本传播,但是还需要函数加载到其他所有集群节点上。这一过程集群不会自动处理,需要由集群管理员来完成,例如模块加载、配置设置等。可以是使用下面的命令在所有主节点上执行加载函数:
redis-cli --cluster-only-masters --cluster call host:port FUNCTION LOAD
在添加节点时,redis-cli --cluster add-node
命令会自动处理将,已加载的函数从现有节点传播到新节点。
5. 迁移、备份还原
可以使用 redis-cli --functions-rdb
命令将当前服务端的函数进行备份,生成一个 RDB
文件,使用该文件可以将函数进行迁移、备份还原。