Lua 脚本与 Redis Functions
Redis 可以在服务端执行 Lua 代码,将多个读写步骤变成一次原子执行。这能减少网络往返,并避免客户端在“读后写”之间被其他请求插入。
何时需要脚本
- Redis 没有直接提供所需的原子命令。
- 操作需要读取多个值、判断,再更新数据。
- 使用
WATCH重试会很频繁。 - 多个命令必须在服务端一次完成。
如果 INCR、SET NX EX、Sorted Set 等内置命令已经能解决问题,应优先使用内置命令。
一个简单 Lua 示例
只有当前值等于调用方提供的 token 时才删除键:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
end
return 0
这比客户端先 GET 再 DEL 更安全,因为比较和删除在 Redis 中原子执行。
可以使用 eval() 学习脚本调用:
const releaseLockScript: string = `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
end
return 0
`;
const deleted: unknown = await client.eval(releaseLockScript, {
keys: ["nloop:lock:report"],
arguments: [lockToken],
});
返回值来自 Redis 协议。项目中应根据脚本契约进行类型收窄,不要直接使用 any。
KEYS 和 ARGV
KEYS:脚本操作的 Redis 键,Cluster 使用它们进行路由和槽位检查。ARGV:普通参数,不参与键路由。
不要把键偷偷放进 ARGV,否则 Cluster 无法正确理解脚本涉及的键。
Node-Redis 脚本定义
Node-Redis 允许在创建客户端时通过 scripts 和 defineScript() 注册带类型转换的脚本。官方示例会定义:
- 脚本文本。
- 键的数量或键位置。
- 参数如何写入命令解析器。
- 返回值如何转换。
这种方式适合稳定、重复使用的脚本,但对初学者抽象较多。建议先理解 EVAL、KEYS、ARGV 和原子性,再封装成类型安全方法。
Redis Functions
Redis Functions 将函数库加载到服务端,可通过 FCALL 调用。与临时脚本相比,它更接近由 Redis 管理和部署的服务端程序。
使用 Functions 前要明确:
- Redis 服务端版本是否支持。
- 函数库由谁部署和升级。
- 应用版本与函数版本如何兼容。
- 故障恢复和新节点是否包含函数库。
脚本最佳实践
- 脚本尽量短小,Redis 主线程执行长脚本会阻塞其他请求。
- 限制循环次数和输入规模。
- 不在脚本中实现复杂业务系统。
- 为脚本写并发测试和边界测试。
- 记录脚本版本,不在线上临时粘贴不可追踪的脚本。
- Cluster 中涉及多个键时,确保它们位于同一槽位。
特别提醒:分布式锁
SET NX EX 加“比较 token 后删除”可以构成单 Redis 实例上的基础租约,但完整分布式锁还涉及:
- 任务超过 TTL。
- 进程暂停或网络分区。
- 主从切换时写入尚未复制。
- fencing token(栅栏令牌)。
涉及资金、库存等强一致业务时,不要把几行 Redis 代码当作完整锁方案。应选择经过验证的算法或在权威数据库中实现并发控制。