Redis从很早就支持通过EVAL命令执行Lua脚本,但这种脚本用完即丢,每次执行都要把完整脚本传一遍,管理起来非常混乱。Redis 7引入的Functions机制改变了这个局面:你可以把一组Lua函数打包成一个库,注册到服务端持久保存,客户端只需要调用函数名即可。本文围绕Redis Functions的核心概念、命令用法和生产实践展开,带你完整掌握这套服务端函数库编程体系。

一、Redis Functions的核心概念与基本用法
Redis Functions的本质是把Lua脚本组织成“库”的形式。一个库可以包含多个函数,库通过FUNCTION LOAD命令一次性加载到Redis中,之后所有客户端都可以通过FCALL命令调用库中的函数。与EVAL不同,Functions的内容会被持久化到RDB和AOF文件中,主从复制时也会自动传播到从节点,重启后无需重新加载。
下面是一个最简单的函数库定义,演示了如何注册一个返回问候语的函数:
#!lua name=mylib
-- name=mylib 表示这个库的名字叫 mylib
local function greet(keys, args)
-- keys 是调用方传入的键名列表
-- args 是调用方传入的普通参数列表
local name = args[1] or 'stranger'
return 'hello, ' .. name
end
redis.register_function('greet', greet)加载这个库只需要一条命令:FUNCTION LOAD "#!lua name=mylib ...",注意整段Lua代码要作为字符串参数传入。加载成功后,调用方式是:
FCALL greet 0 redis # 返回 "hello, redis"
FCALL的语法是FCALL 函数名 numkeys key1 key2 ... arg1 arg2 ...,中间的数字表示后面有几个键名参数。这一点和EVAL的参数传递方式完全一致,熟悉EVAL的开发者可以无缝迁移。如果要查看库的信息,可以用FUNCTION LIST命令列出所有已加载的库;删除库用FUNCTION DELETE mylib;如果想临时测试不落盘的函数,可以用FUNCTION LOAD REPLACE覆盖更新,或者使用FCALL_RO以只读模式调用函数。
二、函数库设计规范与多函数协作
在实际项目中,一个库往往包含多个函数。规范的做法是先定义local函数,再统一注册,这样函数之间可以互相调用,形成模块化的代码结构。同时建议在库头部声明是否启用复合命令(no-writes属性),让Redis能更好地做读写分离路由。
下面是一个库存管理函数库的完整示例,包含扣减库存和查询库存两个函数,并演示了函数间的内部调用:
#!lua name=stocklib
-- no-writes 标记整个库只读时可以加在首行,本库有写操作所以不加
local function get_stock(keys, args)
local stock = redis.call('GET', keys[1])
return tonumber(stock) or 0
end
local function deduct(keys, args)
local key = keys[1]
local amount = tonumber(args[1]) or 1
local stock = get_stock({key}, {})
if stock < amount then
return redis.error_reply('INSUFFICIENT_STOCK')
end
redis.call('DECRBY', key, amount)
return stock - amount
end
local function restore(keys, args)
local key = keys[1]
local amount = tonumber(args[1]) or 1
redis.call('INCRBY', key, amount)
return tonumber(redis.call('GET', key))
end
redis.register_function('get_stock', get_stock)
redis.register_function('deduct', deduct)
redis.register_function('restore', restore)调用示例:FCALL deduct 1 product:1001 2表示从product:1001这个键扣减2个库存。可以看到,deduct函数内部直接调用了get_stock这个local函数,这种设计比把所有逻辑塞进一个大函数要清晰得多。
在代码规范上还有几点需要注意。第一,Lua中的nil和false在返回给客户端时都会变成false,如果要区分“值为空”和“值为false”,建议返回字符串或使用cjson序列化。第二,返回redis.error_reply可以在脚本中主动抛出错误,客户端会收到正常的错误响应而不是脚本报错。第三,如果函数内部需要区分是否处于复制传播模式,可以用redis.setresp和redis.replicate_commands相关的现代特性,不过Redis 7之后脚本默认就是效果复制(effect replication),大多数场景不需要额外处理。
三、Functions与EVAL脚本的对比及生产落地建议
两者最关键的差异在于生命周期管理。EVAL脚本是“无状态”的,脚本内容不持久化,客户端每次都要传完整代码,版本迭代时容易出现不同客户端执行不同版本脚本的混乱局面。Functions则把代码作为服务端数据的一部分管理起来,配合FUNCTION DUMP和FUNCTION RESTORE命令还可以在集群间迁移函数库,实现灰度发布和回滚。
具体对比如下表:
| 对比维度 | EVAL脚本 | Functions |
|---|---|---|
| 持久化 | 不持久化,重启后靠客户端重新提交 | 写入RDB/AOF,重启自动恢复 |
| 主从复制 | 传播脚本内容或执行效果 | 库定义整体传播,从节点直接可用 |
| 版本管理 | 无内置机制,靠脚本SHA1缓存 | 支持REPLACE更新、DUMP导出 |
| 权限控制 | 跟随普通命令权限 | 可禁用FUNCTION命令整体控制 |
在生产落地时有几条经验值得参考。首先,函数库的命名要规范,建议按业务域命名,比如orderlib、ratelib,避免多个团队互相覆盖。其次,更新函数库时使用FUNCTION LOAD REPLACE,它会原子性地替换整个库,但要注意新旧函数签名兼容,避免滚动更新期间出现调用失败。再次,函数执行具有原子性,整个函数执行期间Redis不会执行其他命令,所以函数中不要出现耗时的循环计算,单函数建议控制在毫秒级完成,否则会阻塞整个实例。最后,如果集群模式下使用Functions,所有键参数必须落在同一个哈希槽内,可以用hash tag(例如{order}:1001)保证相关键的亲和性。
总结来说,Redis Functions给服务端编程带来了工程化的管理能力,它不是要取代客户端逻辑,而是把那些需要原子性、需要跨命令一致性的关键逻辑下沉到数据所在的节点。掌握FUNCTION LOAD、FCALL这套命令体系后,配合良好的库设计规范,你就能在缓存、库存、限流、分布式锁等场景中写出既高效又易于维护的服务端逻辑。
Redis FunctionsFUNCTION LOADLua脚本修改时间:2026-09-11 03:12:33