Redis 7 新特性之 自定义Functions
Redis Functions(函数)是用于管理服务端执行代码的API。在Redis 7中出现,旨在取代之前版本的EVAL函数,是Redis 7新特性之一。
Eval 脚本的缺点
Redis 7之前的版本通过Eval执行脚本,该命令允许发送Lua脚本供服务器执行。Eval脚本的核心作用是在Redis中高效、原子地执行应用程序逻辑。通过Lua脚本可以组合不同数据类型、不同键值原子执行。
使用EVAL需要应用程序每次都发送整个脚本以供执行。由于这会导致网络和脚本编译开销,Redis以EVALSHA命令的形式提供了优化。通过首先调用SCRIPT LOAD以获取脚本的SHA1,应用程序可以在之后单独使用SHA1重复调用脚本。
按照架构设计,Redis只缓存加载的脚本。这意味着脚本缓存随时可能丢失,例如在调用script FLUSH之后、重新启动服务器之后或故障切换到副本时。如果缺少脚本,应用程序负责在运行时重新加载脚本。基本假设是脚本是应用程序的一部分,不由Redis服务器维护。
这种方法适用于许多轻量级脚本用例,但一旦应用程序变得复杂并更加依赖脚本,就会带来一些困难:
- 所有客户端应用程序实例都必须维护所有脚本的副本
- 在事务上下文中调用缓存脚本会增加由于缺少脚本而导致事务失败的可能性
- SHA1 设计作用不大 原因是调试非常困难
- EVAL促进了一种反模式,即客户端应用程序逐字渲染脚本,而不是调用KEYS和ARGV Lua API
- 脚本之间不能相互调用 重复代码优化也成为无稽之谈
Redis Functions 介绍
Redis Functions是从Lua脚本进化而来。Functions提供与Lua脚本相同的核心功能。Redis将 Functions函数作为数据库的一个组成部分进行管理,并通过数据持久性和复制确保其可用性。因为函数是数据库的一部分,因此在使用前声明,所以应用程序不需要在运行时加载它们,也不需要冒中止事务的风险。使用函数的应用程序只依赖于它们的API,而不依赖于数据库中嵌入的脚本逻辑。
Redis Functions的设计还试图模糊编程语言的界限。Lua是Redis目前唯一支持作为嵌入式执行引擎的语言解释器,其目的是简单易学。然而,选择Lua作为一种语言仍然给许多Redis用户带来了挑战。Redis Functions特性对实现的语言没有任何限定。作为函数定义的一部分的执行引擎负责运行它。理论上,引擎可以用任何语言执行函数,只要它遵守若干规则(例如终止执行函数的能力)。
与Lua脚本操作一样,函数的执行是原子的。函数的执行在其整个时间内阻止所有服务器活动,这与事务的语义类似。这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。执行函数的阻塞语义始终适用于所有连接的客户端。因为运行一个函数会阻塞Redis服务器,所以函数应该快速完成执行,所以应该避免使用长时间运行的函数。
总结:Redis Functions 类似MYSQL中的存储过程、自定义函数;事先定义Functions的逻辑,存储在服务端,客户端要做的仅仅是调用函数即可
Redis Functions 学习
接下来通过一些具体的例子和Lua片段来探索Redis函数。
每一个Redis 函数都需要被加载到Redis服务。使用FUNCTION LOAD命令将库加载到Redis数据库。该命令获取library 内容作为输入,格式为:
#!<engine name> name=<library name>
- 为执行引擎名称,目前固定为 lua
- 库名称 可自定义
尝试加载空的库内容,会导致报错
redis> FUNCTION LOAD "#!lua name=mylib\n"
(error) ERR No functions registered
错误的原因是,因为加载的库中没有函数。每个库都需要包含至少一个注册函数才能成功加载。已注册的函数被命名,并充当库的入口点。当目标执行引擎处理FUNCTION LOAD命令时,它会注册库的函数。
Lua引擎在加载时编译和评估库源代码,并期望通过调用register_function()API来注册函数。
#!lua name=mylib
redis.register_function(
'knockknock',
function() return 'Who\'s there?' end
)
代码片段演示一个简单的库,它注册了一个名为knockknock的函数,并返回一个字符串回复。redis.register_function方法接收两个参数
- 函数名称
- 执行方法
简单演示
接下来加载库内容并使用 FCALL函数执行自定义函数
redis> FUNCTION LOAD "#!lua name=mylib\nredis.register_function('knockknock', function() return 'Who\\'s there?' end)"
mylib
redis> FCALL knockknock 0
"Who's there?"
FCALL 函数提供了两个参数
- 函数注册名称
- 参数个数0,表示后面键的数量 跟EVAL用法一致
删除库函数
# 删除mylib的库函数
function delete mylib
输入参数
- 定义函数
-- 定义 mylib.lua 文件 内容如下
#!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)
- 注册函数
# 根据lua文件内容 并注册库函数
cat mylib.lua | redis-cli -x FUNCTION LOAD REPLACE
参数说明:
- redis-cli -x -x选项代表从标准输入(stdin)读取数据作为redis-cli的***一个参数。在这里表示mylib.lua 的内容作为参数
- REPLACE REPLACE修饰符 告知Redis要覆盖现有的库定义。否则,会收到一个错误,显示库已经存在
- 指定函数
redis> FCALL my_hset 1 myhash myfield "some value" another_field "another value"
(integer) 3
redis> HGETALL myhash
1) "_last_modified_"
2) "1640772721"
3) "myfield"
4) "some value"
5) "another_field"
6) "another value"
扩展函数
开发者可以向库中添加更多的函数,以满足不同的需求。如下,添加两个访问hash的方法
-- mylib.lua 文件
#!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
local function my_hgetall(keys, args)
-- 使用RESP3 协议返回数据
redis.setresp(3)
local hash = keys[1]
local res = redis.call('HGETALL', hash)
res['map']['_last_modified_'] = nil
return res
end
local function my_hlastmodified(keys, args)
local hash = keys[1]
-- 获取hash的最后修改时间
return redis.call('HGET', hash, '_last_modified_')
end
redis.register_function('my_hset', my_hset)
redis.register_function('my_hgetall', my_hgetall)
redis.register_function('my_hlastmodified', my_hlastmodified)
# 根据lua文件内容 并注册库函数
cat mylib.lua | redis-cli -x FUNCTION LOAD REPLACE
# 调用函数
redis> FCALL my_hgetall 1 myhash
1) "myfield"
2) "some value"
3) "another_field"
4) "another value"
redis> FCALL my_hlastmodified 1 myhash
"1640772721"
代码复用
Redis 函数库另一个优势是,减少重复代码,精简代码,提高复用性。现在在之前的基础上添加参数检查校验的函数
-- mylib.lua 文件
#!lua name=mylib
local function check_keys(keys)
local error = nil
local nkeys = table.getn(keys)
if nkeys == 0 then
error = 'Hash key name not provided'
elseif nkeys > 1 then
error = 'Only one key name is allowed'
end
if error ~= nil then
redis.log(redis.LOG_WARNING, error);
return redis.error_reply(error)
end
return nil
end
local function my_hset(keys, args)
local error = check_keys(keys)
if error ~= nil then
return error
end
local hash = keys[1]
local time = redis.call('TIME')[1]
return redis.call('HSET', hash, '_last_modified_', time, unpack(args))
end
local function my_hgetall(keys, args)
local error = check_keys(keys)
if error ~= nil then
return error
end
redis.setresp(3)
local hash = keys[1]
local res = redis.call('HGETALL', hash)
res['map']['_last_modified_'] = nil
return res
end
local function my_hlastmodified(keys, args)
local error = check_keys(keys)
if error ~= nil then
return error
end
local hash = keys[1]
return redis.call('HGET', keys[1], '_last_modified_')
end
redis.register_function('my_hset', my_hset)
redis.register_function('my_hgetall', my_hgetall)
redis.register_function('my_hlastmodified', my_hlastmodified)
重新注册加载库函数,并调用函数
127.0.0.1:6379> FCALL my_hset 0 myhash nope nope
(error) Hash key name not provided
127.0.0.1:6379> FCALL my_hgetall 2 myhash anotherone
(error) Only one key name is allowed
同时,Redis服务端也会输出对应的日志
13415:M 12 Dec 2022 21:53:15.581 # Hash key name not provided
13415:M 12 Dec 2022 21:53:21.197 # Only one key name is allowed
集群同步
在Redis 集群部署时 - 关于redis Cluster 集群搭建 请参考,需要考虑到Redis Functions各个节点之间的同步问题,默认情况下函数并不会加载集群中的各个节点,需要人工进行处理,
-
redis-cli --cluster add-node 新增节点会自动将节点函数同步到新节点中
-
edis-cli --cluster-only-masters --cluster call host:port FUNCTION LOAD 将函数同步到各个节点
cat mylib.lua | redis-cli -x --cluster-only-masters --cluster call localhost:6001 FUNCTION LOAD