一、背景与动机
传统的 Redis 脚本机制依赖于客户端加载 EVAL 脚本,存在以下局限:
- 网络与编译开销
每次调用都要传输脚本源码或重新加载 SHA1。 - 缓存失效风险
重启、主从切换、SCRIPT FLUSH 后脚本缓存丢失,事务易失败。 - 调试与运维困难
SHA1 不可读、难以在 MONITOR 或日志中追踪。 - 代码复用受限
脚本之间无法互相调用,只能由客户端拼接多份代码。
Redis Functions 应运而生,将脚本纳入 Redis 数据模型,以「声明—持久化—调用」的方式,提供了更高效、更可靠、更易运维的服务器端脚本化能力。
二、Redis Functions 概览
-
第一类工件
函数(Function)属于数据库自身的组成部分,随数据持久化(AOF/RDB)与复制而同步。 -
库(Library)管理
多个函数组织在同一库中,整体加载或替换,不支持单个函数增量更新。 -
Shebang 元数据
通过#!<engine> name=<library>
指定执行引擎与库名称。 -
调用接口
FUNCTION LOAD
/FUNCTION DELETE
/FUNCTION LIST
管理库FCALL
(可写)或FCALL_RO
(只读)执行函数
三、加载与注册函数库
-
编写库源码
以 Lua 为例,库文件开头必须包含 Shebang 行:#!lua name=mylib
-
注册函数
通过redis.register_function(name, callback, [flags])
将每个函数注册到库中:redis.register_function('knockknock',function() return "Who's there?" end )
-
加载到 Redis
cat mylib.lua | redis-cli -x FUNCTION LOAD REPLACE
返回值为库名(如
mylib
),表示加载成功。
四、调用函数:FCALL 与 FCALL_RO
- FCALL [keys…] [args…]
在任何节点执行(可写)。 - FCALL_RO [keys…] [args…]
只读副本上执行,只允许标记为no-writes
的函数。
示例调用:
redis> FCALL knockknock 0
"Who's there?"
五、键名与参数
- KEYS:指定所有将要访问的 Redis 键名,必须在调用时列出,保证集群模式下数据访问的正确路由。
- ARGV:所有非键名输入,供函数内部业务逻辑使用。
在函数回调中,Lua 参数形式为:
function(keys, args)-- keys 是一个 Lua 数组,包含前 numkeys 个参数-- args 是包含后续所有普通输入的数组
end
六、示例:可复用的 Hash 操作库
下面以一个「自动记录最后修改时间」的 Hash 操作库为例,演示如何在同一库中组织多个函数并实现代码复用。
#!lua name=mylib-- 辅助:校验 keys 数量
local function check_keys(keys)if #keys ~= 1 thenreturn redis.error_reply("只允许传入一个键名")endreturn nil
end-- my_hset:设置字段并记录时间戳
local function my_hset(keys, args)local err = check_keys(keys)if err then return err endlocal hash = keys[1]local ts = redis.call("TIME")[1]return redis.call("HSET", hash, "_last_modified_", ts, unpack(args))
end-- my_hgetall:读取所有字段(排除 _last_modified_)
local function my_hgetall(keys, args)local err = check_keys(keys)if err then return err endredis.setresp(3) -- 切换到 RESP3,返回字典格式local hash = keys[1]local res = redis.call("HGETALL", hash)res.map["_last_modified_"] = nilreturn res
end-- my_hlastmodified:读取修改时间戳
local function my_hlastmodified(keys, args)local err = check_keys(keys)if err then return err endreturn redis.call("HGET", keys[1], "_last_modified_")
end-- 注册函数,并为只读函数添加 no-writes 标志
redis.register_function("my_hset", my_hset)
redis.register_function{function_name = "my_hgetall",callback = my_hgetall,flags = {"no-writes"}
}
redis.register_function{function_name = "my_hlastmodified",callback = my_hlastmodified,flags = {"no-writes"}
}
加载并调用:
cat mylib.lua | redis-cli -x FUNCTION LOAD REPLACE
redis> FCALL my_hset 1 myhash field1 "value1"
(integer) 2redis> FCALL_RO my_hgetall 1 myhash
1) "field1"
2) "value1"redis> FCALL_RO my_hlastmodified 1 myhash
"1640772721"
七、集群与持久化
- 复制与持久化
函数与数据一同写入 AOF/RDB,并复制到从节点,保证函数持久可用。 - 集群加载
需要在所有主节点执行FUNCTION LOAD
,可借助redis-cli --cluster-only-masters
批量操作。 - RDB 钩子
使用redis-cli --functions-rdb
可导出函数的 RDB 文件,在启动时预加载到新的实例。
八、函数标志与权限控制
- 默认行为
Redis 假定所有函数可能读写数据,故禁止在只读场景(FCALL_RO 或只读副本)下执行。 - no-writes 标志
在redis.register_function
时添加flags={'no-writes'}
,表明函数仅执行读操作,允许在只读场景下调用。 - 更多 Flags
请参见官方文档 Script flags,设置合适权限确保安全与隔离。
九、最佳实践与注意事项
- 避免长时间阻塞
函数执行时会阻塞主线程,应保持逻辑简短,避免慢查询或大批量数据扫描。 - 键名列举全
集群模式下访问所有键必须列出于numkeys
参数,防止跨槽错误。 - 整体替换
函数库更新时会替换全量代码,确保所有函数均在同一版本下协同工作。 - 日志与监控
可在函数内部调用redis.log()
输出警告或错误,便于生产环境诊断。 - 版本管理
建议将函数库源码纳入版本控制,与应用一并发布,保持部署可追溯。
十、总结
Redis Functions 通过将服务器端脚本化提升为第一类工件,彻底解决了 EVAL 脚本在分发、缓存、调试和复用上的痛点。它不仅让脚本管理更可靠,也让 Redis 能像模块一样对外提供统一 API,助力构建更复杂、更高效的业务逻辑。结合本文示例和最佳实践,相信你能快速上手 Redis 7+ 的函数特性,并在生产环境中获得显著的运维与性能收益。