Lua脚本会彻底卡死Redis实例,因其原子独占主线程,阻塞键过期、AOF写入、心跳检测等所有操作;必须避免循环调用redis.call、SCAN后聚合、脚本内排序分页等高耗时行为,改由客户端处理。

因为会阻塞整个 Redis 实例,所有其他命令(包括键过期、AOF 写入、心跳检测)全部排队等待,不是慢一点,是彻底卡死。
Redis 单线程模型下 Lua 脚本是原子独占的
脚本一旦开始执行,就锁住主线程,直到它结束或超时。期间 SCRIPT KILL 和 SHUTDOWN NOSAVE 是仅有的两个能响应的命令,其余一律返回 (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.。
常见错误现象:
- 用
for i = 1, 100000 do redis.call("GET", "key:"..i) end模拟耗时,PING都会失败 - 脚本里调用
os.time()或string.match()复杂正则,直接报错或拖慢到超时 - 集群环境下跨 slot 的
EVAL直接被拒绝,错误是CROSSSLOT Keys in request don't hash to the same slot
redis.call() 在循环中调用开销远超预期
每次 redis.call() 都走完整命令路径:解析参数、ACL 权限检查、键路由(集群下)、实际执行。哪怕只是反复读同一个 key,也等于重复做这些事。
正确做法:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 用
local val = redis.call("GET", KEYS[1])缓存结果,后续全用val - 批量读改用
redis.call("MGET", unpack(KEYS)),而不是 for 循环里反复redis.call("GET") - 避免在循环里调用
redis.call("HGET", KEYS[1], ARGV[i]),优先用redis.call("HMGET", KEYS[1], unpack(ARGV)) -
unpack(ARGV)元素超过 1000 个时可能栈溢出,客户端应分批传参
哪些操作根本不能放 Lua 里做
以下行为等于在 Redis 主线程里埋雷:
- 用
SCAN遍历后,在脚本里做条件累加(如统计 VIP 用户积分总和) - 对多个 Hash 做
HGETALL合并、拼成 table、再table.sort() - 在脚本里实现分页逻辑,尤其数据量 > 500 条时用
table.slice() -
redis.call("ZRANGEBYSCORE")拿 ID 列表,再循环HGET补详情 —— 这是典型的 N+1 查询,网络开销已失控
这些该交给客户端语言:Python 的 sorted()、Go 的 sort.Slice()、Java 的 Stream,它们不抢 Redis 主线程,还能并行、限流、打点监控。
FUNCTIONS 也没绕过单线程瓶颈
Redis 7.0 引入的 FUNCTION LOAD 虽然解决了脚本持久化和主从同步问题,但执行时仍独占主线程。慢函数一样会卡住整个实例。
关键区别只在可靠性:
-
EVALSHA脚本重启后丢失,FCALL函数随 AOF/RDB 持久化 -
FUNCTIONS禁用全局变量和非安全模块(如io、loadstring),更严格 - 迁移老脚本必须改头:第一行加
#!lua name=xxx,且只能用小写命令名(redis.call("incr"),不能写REDIS.CALL("INCR"))
真正容易被忽略的是:无论用 EVAL 还是 FCALL,只要脚本里做了长循环、复杂排序、多 key 聚合,性能断崖式下跌就是必然结果——这不是配置能调出来的,是模型决定的硬约束。

















