- 1Redis7.x&8.x 全新进阶系列(01):划时代升级总览——从6.x到7.x/8.x全版本变革全景
- 2Redis7+&8.X 全新进阶系列(02):Redis Functions 详解——替代Lua脚本的官方轻量化函数方案当前
参考文档:Redis 7.0 Release Notes、官方 Functions 文档、redis/src/function/ 源码模块
验证环境:Redis 7.0.15
前言
在 Redis 7.0 正式发布之前,Lua 脚本是 Redis 实现原子复合操作的唯一内置方案。大量业务依赖 Lua 实现批量读写、原子校验、复杂计算逻辑,但在长期生产实践中,Lua 的缺陷不断暴露:脚本分散无法统一管理、缺少版本控制、调试困难、权限粗粒度、脚本缓存与集群同步存在隐性风险。
Redis 7.0 引入 Redis Functions,作为官方原生脚本方案,定位并不是简单替换 Lua,而是在保留原子执行、服务端运算能力的前提下,解决 Lua 在运维、权限、生命周期管理上的痛点。
本篇我们会对比 Lua 的历史痛点、Functions 的设计思路、完整使用示例、Lua vs Functions 差异、源码核心逻辑,以及生产环境的迁移与风险。
一、Lua 脚本在 Redis 6.x 的核心痛点
Lua 脚本核心优势是单脚本原子执行,但这些原生短板是架构层面无法修复的:
- 脚本无持久化与版本管理
业务代码每次都通过EVAL/EVALSHA上传脚本,脚本仅保存在节点内存。集群扩容、节点重启后脚本丢失,业务需要客户端重新推送脚本;没有版本标记,多人维护容易出现脚本版本错乱。 - 权限管控粒度粗糙
ACL 只能禁止/允许执行EVAL整条命令,无法单独限制某一段脚本,一旦开放 EVAL,脚本内部可以调用任意Redis命令,存在高危操作风险。 - 调试与可观测性差
Lua 的print输出只能写入服务端日志,无法返回给客户端;脚本报错信息简陋,很难定位线上问题。 - 集群同步缺陷
使用EVALSHA在集群场景下,存在脚本哈希在不同节点不一致的问题;跨槽脚本本身就不被支持,但Lua没有提前校验机制。 - 复用性差
逻辑无法封装成命名函数,公共逻辑只能复制粘贴到各个业务脚本中,维护成本高。
注意:Redis 没有废弃Lua,只是推荐新业务优先使用 Functions;老Lua脚本可以继续运行,属于兼容保留。
二、Redis Functions 设计思路
Redis Functions 的核心设计目标:将脚本从“客户端临时提交的代码片段”升级为“服务端托管、命名、版本化、可权限隔离的函数”。
核心设计要点:
- 函数在服务端持久托管:使用
FUNCTION LOAD将代码上传到Redis,函数会持久化到RDB/AOF,节点重启自动加载。函数拥有唯一名称,客户端只需要调用名字即可执行。 - 库+函数的两层封装:代码以**library(库)**为单位加载,一个库可以包含多个函数;库支持版本标记。
- 独立沙箱环境:Functions 的执行沙箱与Lua隔离,可单独对函数配置ACL权限,控制函数内部能调用哪些Redis命令。
- 统一生命周期:支持加载、删除、列举、转储函数,运维侧可以统一管理所有服务端注册逻辑。
- 保留原子语义:单个函数调用依然是单线程原子执行,和Lua一样,执行期间阻塞其他命令,长耗时函数同样会造成主进程阻塞。
重要取舍:Functions 并没有解决“长脚本阻塞Redis主线程”的问题。无论是Lua还是Functions,都是在Redis单线程事件循环内执行,禁止在函数内编写重计算、长时间阻塞逻辑,这是官方明确的约束。
三、完整使用示例
环境:Redis 7.0 单机,
redis-cli客户端
3.1 FUNCTION LOAD:加载函数库
函数代码使用 #!lua 声明语言(7.0仅原生支持Lua作为函数语言),一个library包含多个函数。
#!lua name=mylib
-- 库名称:mylib
local function hello(keys, args)
-- keys: 传入的key数组
-- args: 传入的参数数组
redis.call('SET', keys[1], args[1])
return "ok, value=" .. args[1]
end
local function get_val(keys, args)
return redis.call('GET', keys[1])
end
-- 注册函数名称
redis.register_function('hello', hello)
redis.register_function('get_val', get_val)
使用 FUNCTION LOAD 加载:
FUNCTION LOAD "#!lua name=mylib\nlocal function hello(keys, args)\nredis.call('SET', keys[1], args[1])\nreturn \"ok, value=\" .. args[1]\nend\nlocal function get_val(keys, args)\nreturn redis.call('GET', keys[1])\nend\nredis.register_function('hello', hello)\nredis.register_function('get_val', get_val)"
加载成功会返回库的SHA哈希。
若库已存在,使用
FUNCTION LOAD REPLACE覆盖更新版本。
3.2 FCALL 调用函数
语法:FCALL <funcname> <numkeys> [key1 key2 ...] [arg1 arg2 ...]
# 调用 hello 函数,1个key,参数为 world
FCALL hello 1 testkey "world"
# 读取值
FCALL get_val 1 testkey
输出:
1) "ok, value=world"
3.3 查看已加载函数
FUNCTION LIST
返回库名、函数名称、源码、描述信息。
3.4 删除函数库
FUNCTION DELETE mylib
3.5 持久化验证
执行 BGSAVE,重启Redis,重新连接,直接 FCALL get_val 1 testkey,无需重新加载函数,函数从RDB自动恢复。
3.6 序列化导出/导入
# 导出全部函数
FUNCTION DUMP
# 导入函数
FUNCTION RESTORE <dump_data>
适合多环境同步函数。
四、Lua VS Redis Functions 全方位对比
| 对比维度 | Lua脚本(EVAL/EVALSHA) | Redis Functions |
|---|---|---|
| 存储位置 | 节点内存,重启丢失;依赖客户端推送 | RDB/AOF持久化,服务端托管,重启自动加载 |
| 调用方式 | 传入完整代码片段 / sha哈希 | 按函数名调用 |
| 版本管理 | 无原生版本标记 | 以library为单位,支持REPLACE更新 |
| 权限控制 | ACL只能全局开关EVAL,无法隔离脚本 | 支持ACL针对函数单独授权,限制内部命令 |
| 代码复用 | 无封装,代码片段分散 | 库内多个函数,公共逻辑可复用 |
| 集群同步 | EVALSHA存在哈希不一致风险 | 函数随RDB/AOF同步,集群节点自动加载 |
| 调试能力 | print输出到服务端日志 | 同样lua沙箱,7.x小幅增强报错信息 |
| 原子性 | 原子执行,阻塞主线程 | 原子执行,阻塞主线程 |
| 适用场景 | 一次性临时脚本、简单测试 | 业务长期稳定复用的服务端逻辑 |
破坏性变更提醒:Redis 7.2 废弃了Lua脚本中的
五、核心源码解析(Redis 7.0,源码路径 src/function/)
Commit参考:7.0 分支
616225b
5.1 核心模块文件
function.c:函数管理入口,FUNCTION LOAD/FCALL/FUNCTION LIST命令实现function_lua.c:Lua语言沙箱实现,redis.register_function注册逻辑function.h:结构体定义functionLib、function
5.2 关键数据结构
// 简化示意源码,非完整原始代码
typedef struct functionLib {
sds name; // 库名称
sds code; // 原始源码
dict *functions; // 库内函数字典 name -> function
int engine; // 执行引擎,7.0仅LUA
long long version;
} functionLib;
typedef struct function {
sds name;
functionLib *lib;
lua_func *func; // lua闭包引用
} function;
Redis 在全局 server.function_libs 字典保存所有加载的library。
5.3 执行流程(FCALL)
FCALL命令入口fcallCommand,解析函数名、keys、args;- 在全局字典查找对应
function对象; - 执行ACL权限校验:检查当前用户是否有权限执行该函数,以及函数内部调用命令的权限;
- 进入Lua沙箱,把
keys、args传入函数; - 调用
redis.call()执行Redis内置命令; - 捕获返回值/异常,包装为Redis协议返回给客户端;
- 函数执行全程阻塞主线程,执行完毕释放沙箱上下文。
5.4 持久化逻辑
RDB持久化钩子:在RDB保存阶段,遍历 server.function_libs,序列化库源码、名称、函数列表写入RDB。
AOF持久化:加载函数的 FUNCTION LOAD 命令会写入AOF日志,重放AOF自动重建函数库。
源码关键注意点:函数的Lua沙箱和EVAL的Lua沙箱是两套独立虚拟机实例,环境变量互不共享。
六、生产落地、适用场景与踩坑清单
✅ 推荐使用场景
- 业务通用原子逻辑,多客户端复用(如库存扣减、状态流转、批量字段更新);
- 需要统一管控脚本,不希望业务客户端携带大量脚本代码;
- 希望做细粒度权限隔离,禁止业务脚本执行高危命令;
- Redis集群环境,不想处理EVALSHA哈希同步问题。
❌ 不推荐场景
- 一次性临时测试脚本,频繁修改调试;
- 执行耗时较长的计算逻辑(依然阻塞主线程);
- 需要动态高频更新函数(每次FUNCTION LOAD REPLACE会重载库,有成本)。
⚠️ 生产风险与踩坑
- 函数更新是全库替换:
FUNCTION LOAD REPLACE是替换整个library,不是单个函数;更新时原子替换,正在执行的旧函数会继续执行完毕。 - 集群场景下,跨槽key依然禁止:和Lua一样,FCALL传入的keys必须落在同一个hash slot,否则直接报错,不会自动转发。
- 权限最小化原则:不要给业务账号开放
FUNCTION LOAD / FUNCTION DELETE,这两个是高危运维命令,仅运维账号可用;业务账号只授予FCALL。 - 版本回滚要提前备份:更新函数前,先
FUNCTION DUMP导出备份,出错可以FUNCTION RESTORE回滚。 - 8.x兼容说明:Redis8.x保留Functions完整能力,没有移除;向量检索等新能力可以在Functions内部调用VSIM等命令。
迁移建议(Lua迁移到Functions)
- 筛选长期稳定、复用度高的Lua脚本,优先迁移;一次性临时脚本保留EVAL;
- 将多个相关脚本合并到同一个library,统一管理;
- 增加预发环境验证:加载函数、ACL权限、集群槽位校验;
- 保留监控:监控FCALL的执行耗时、报错次数,和Lua脚本监控对齐。
七、总结
Redis Functions 并不是性能上的巨大提升,而是运维与安全层面的升级。它把脚本从客户端的临时片段,转变为服务端托管的一等公民。
如果你在生产上被Lua脚本版本混乱、重启丢失、权限失控困扰,Functions 是7.x官方给出的标准解决方案。但是一定要记住底层不变的约束:单线程原子执行,长时间运算依然会阻塞Redis主线程。
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接
文章评论