Redis Functions(函数)是用于管理服务端执行代码的API。在Redis 7中出现,旨在取代之前版本的EVAL函数,是Redis 7新特性之一。
Redis 7之前的版本通过Eval执行脚本,该命令允许发送Lua脚本供服务器执行。Eval脚本的核心作用是在Redis中高效、原子地执行应用程序逻辑。通过Lua脚本可以组合不同数据类型、不同键值原子执行。
使用EVAL需要应用程序每次都发送整个脚本以供执行。由于这会导致网络和脚本编译开销,Redis以EVALSHA命令的形式提供了优化。通过首先调用SCRIPT LOAD以获取脚本的SHA1,应用程序可以在之后单独使用SHA1重复调用脚本。
按照架构设计,Redis只缓存加载的脚本。这意味着脚本缓存随时可能丢失,例如在调用script FLUSH之后、重新启动服务器之后或故障切换到副本时。如果缺少脚本,应用程序负责在运行时重新加载脚本。基本假设是脚本是应用程序的一部分,不由Redis服务器维护。
这种方法适用于许多轻量级脚本用例,但一旦应用程序变得复杂并更加依赖脚本,就会带来一些困难:
Redis Functions是从Lua脚本进化而来。Functions提供与Lua脚本相同的核心功能。Redis将 Functions函数作为数据库的一个组成部分进行管理,并通过数据持久性和复制确保其可用性。因为函数是数据库的一部分,因此在使用前声明,所以应用程序不需要在运行时加载它们,也不需要冒中止事务的风险。使用函数的应用程序只依赖于它们的API,而不依赖于数据库中嵌入的脚本逻辑。
Redis Functions的设计还试图模糊编程语言的界限。Lua是Redis目前唯一支持作为嵌入式执行引擎的语言解释器,其目的是简单易学。然而,选择Lua作为一种语言仍然给许多Redis用户带来了挑战。Redis Functions特性对实现的语言没有任何限定。作为函数定义的一部分的执行引擎负责运行它。理论上,引擎可以用任何语言执行函数,只要它遵守若干规则(例如终止执行函数的能力)。
与Lua脚本操作一样,函数的执行是原子的。函数的执行在其整个时间内阻止所有服务器活动,这与事务的语义类似。这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。执行函数的阻塞语义始终适用于所有连接的客户端。因为运行一个函数会阻塞Redis服务器,所以函数应该快速完成执行,所以应该避免使用长时间运行的函数。
总结:Redis Functions 类似MYSQL中的存储过程、自定义函数;事先定义Functions的逻辑,存储在服务端,客户端要做的仅仅是调用函数即可
接下来通过一些具体的例子和Lua片段来探索Redis函数。
每一个Redis 函数都需要被加载到Redis服务。使用FUNCTION LOAD命令将库加载到Redis数据库。该命令获取library 内容作为输入,格式为:
#!<engine name> name=<library name>
尝试加载空的库内容,会导致报错
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 函数提供了两个参数
# 删除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> 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各个节点之间的同步问题,默认情况下函数并不会加载集群中的各个节点,需要人工进行处理,