redis 7.0中fcall比eval更稳,因function load注册的函数自动持久化并同步到集群所有节点,避免script load脚本丢失问题,且支持可观测性、细粒度acl权限和安全lua隔离。

Redis 7.0中FCALL比EVAL更稳,因为函数会落盘和同步
直接用 EVAL 或 EVALSHA 在 Redis 7.0+ 集群里跑复杂计算,大概率过两天就报 NOSCRIPT No matching script。这不是配置问题,是设计如此:Redis 7.0 明确规定 SCRIPT LOAD 的脚本不写入 AOF/RDB,也不复制到从节点。主节点重启、主从切换、集群分片迁移后,脚本就丢了。
而 FUNCTION LOAD 注册的函数会自动持久化并同步——你 reload 一次,整个集群都认得这个函数名。调用时用 FCALL mylib.myfunc 1 key1 100,不用管 SHA1、不用预加载、不担心路由错位。
- 函数库名(如
lib:stats)+ 函数名(如sum_top_n)构成全局唯一标识 -
FUNCTION LIST可查所有已注册函数,FUNCTION STATS能看到错误率和执行耗时,可观测性拉满 - ACL 权限可精确到函数级别,比如
+function|call ~stats.*,比给整个+eval权限安全得多
Lua语法要收紧:不能用redis.call大写,也不能碰os/io
把老 EVAL 脚本直接塞进 FUNCTION LOAD,90% 会失败,典型报错是 attempt to call a nil value (global 'redis') 或 Lua redis() command arguments must be strings or integers。Functions 对 Lua 环境做了硬性隔离。
必须满足三条铁律:
- 第一行且仅第一行,必须是
#!lua name=lib:xxx头,空行都不行;version=1可选但建议加上 - 命令名全小写:
redis.call("incr", key)合法,redis.CALL("INCR", key)直接报错 - 禁用全局变量和非安全模块:删掉
os.time()、math.randomseed()、io.open()、loadstring()—— 这些在 Functions 里根本不存在
合法示例开头:
#!lua name=lib:analytics
redis.register_function("sum_recent", function(keys, args)
local sum = 0
for _, k in ipairs(keys) do
local v = redis.call("get", k)
if v then sum = sum + tonumber(v) or 0 end
end
return sum
end)
复杂计算别塞进一个Function:拆开做,否则卡住整个Redis
Functions 和 EVAL 一样,运行时独占 Redis 单线程。一个函数里循环十万次、调用 KEYS *、或试图 redis.call("http.get", ...)(虽然根本没这命令),结果就是所有客户端命令排队,延迟飙升,监控里 latency spike 打满屏。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
真正该做的,是把“计算”和“协调”分开:
- 用
ZADD/HSET等原生命令快速写入原始数据,不带逻辑 - 把聚合、排序、过滤等重计算,交给后台任务(比如用 Redis Streams + worker)异步处理
- Functions 只干原子性强、路径短、毫秒级完成的事:比如
decr_if_gt、incr_with_expire、zremrangebyscore_and_count
例如限流场景,不要在 Function 里实现滑动窗口全部逻辑;只用它做「当前时间戳写入 ZSET + 清理过期项」两步,统计交给外部服务。
incr+expire原子性这种事,Function比Lua脚本更直观
老方案用 Lua 实现 INCR + EXPIRE 原子性,得手写判断 ttl == -1 再补过期时间,稍有疏漏就会造出永不过期 key,导致内存泄漏和计数器卡死。
Functions 写起来更直白,也更容易验证逻辑是否覆盖边界:
#!lua name=lib:rate
redis.register_function("incr_with_ttl", function(keys, args)
local key = keys[1]
local ttl_sec = tonumber(args[1])
local val = redis.call("incr", key)
-- 如果是新key(ttl为-1),才设过期;已存在则跳过
if redis.call("ttl", key) == -1 then
redis.call("expire", key, ttl_sec)
end
return val
end)
注意:这里仍需避免在高并发下反复触发 expire(虽无害但冗余),实际部署前建议加个 if tonumber(val) == 1 then ... 判断是否首次写入。
真正容易被忽略的是函数升级成本:改一行就得整库重载(FUNCTION FLUSH + 重新 FUNCTION LOAD),没有热更新。生产环境务必配好灰度发布流程,别在高峰期 reload 函数库。










