应避免在lua脚本中写大循环,因其单线程特性会导致redis整体阻塞;须用原生命令批量操作、限制循环次数、禁用全局变量,并强制使用evalsha+script load预加载。

避免在Lua脚本里写大循环
Redis 执行 Lua 是单线程的,一个脚本卡住,整个 Redis 就卡住。所谓“大循环”,指遍历上百个元素、嵌套多层、或每次循环都调用 redis.call() 的场景——比如对 500 个用户 ID 逐个 HGETALL,或在脚本里做字符串正则匹配过滤。
常见错误现象:EVAL "for i=1,1000 do redis.call('GET', 'key_'..i) end" 0 耗时陡增,甚至触发 lua-time-limit 超时(默认 5 秒),返回 BUSY Redis is busy running a script。
应该怎么做:
- 把批量读/写操作交给原生命令:用
HMGET替代循环HGET,用ZRANGEBYSCORE+WITHSCORES一次拉取带分值的集合,而不是循环ZSCORE - 若必须条件筛选,优先在客户端做:只从 Redis 拉关键字段(如 ID + score),筛选逻辑放客户端;或改用
SCAN分批 + 客户端聚合 - 真要在脚本里循环,限制在几十次内,且不嵌套、不调用 Redis 命令——例如仅做数组拼接或简单数值累加
别用全局变量,local 才是安全的
Lua 脚本里的全局变量(如 counter = 1)不仅无效,还危险:它不保存到 Redis,不参与原子性,脚本退出就丢;更糟的是,多次执行可能因变量残留导致逻辑错乱(比如上一次没清空的 tmp_result 影响下一次)。
真正能跨调用、受 Redis 管理的,只有键(key)。所有数据读写必须走 redis.call() 或 redis.pcall()。
正确做法:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 所有中间值用
local声明:比如local val = redis.call('GET', KEYS[1]),复用val避免重复调用 - 不要写
my_flag = true然后后面 ifmy_flag—— 改成local my_flag = true - 需要“状态暂存”?用 Redis 键:比如
redis.call('SET', KEYS[2], 'processing'),而不是靠 Lua 变量记状态
为什么 EVALSHA + SCRIPT LOAD 是硬性要求
每次用 EVAL 发完整脚本,等于把几百字节甚至几 KB 的文本反复传输、解析、计算 SHA、加载进缓存——高频调用(如限流器每秒上千次)时,这部分开销远超脚本逻辑本身。
常见错误现象:客户端每次生成动态脚本(如拼接 "return redis.call('INCR', '"..key.."' )"),导致 SCRIPT LOAD 失效,永远走 EVAL,网络和 CPU 开销翻倍。
必须做到:
- 上线前统一
SCRIPT LOAD,拿到 SHA1 存客户端内存(如 Go 的map[string]string) - 运行时一律用
EVALSHA <sha><numkeys> ...</numkeys></sha>,失败时捕获NOSCRIPT错误,自动 fallback 到EVAL并重新LOAD - Redis 重启后缓存清空,
EVALSHA必然失败一次,这个 case 不处理就会持续报错
批量操作比循环调用快一个数量级
Redis 原生命令本身就是高度优化的 C 实现,而 Lua 解释器慢、调用开销大。比如 100 次 redis.call('HSET', KEYS[1], k, v),不如一次 redis.call('HMSET', KEYS[1], unpack(ARGV))(注意 ARGV 要按 key1,val1,key2,val2… 排列)。
容易踩的坑:
-
unpack(ARGV)要求ARGV是 table,且长度不能超限制(Redis 默认 10k 元素);传太多参数会报ERR Error running script - 用
MSET替代循环SET时,确保所有 key 都在同一个 slot(集群下需用{key}hash tag) -
INCRBY比GET+SET快得多,也更原子——别为了“可读性”拆原生命令
最常被忽略的一点:脚本性能瓶颈往往不在逻辑,而在“以为自己在优化”的地方——比如用 Lua 做 JSON 解析、时间格式化、base64 编码,或者把小 key 拆成几百个独立 key 导致内存碎片。这些操作该由客户端干,或者换数据结构(如用 HASH 聚合小字段),而不是塞进 Lua 里硬扛。










