Redis Lua脚本需从执行时长、调用频次、脚本结构三方面设防:超时由lua-time-limit控制,默认5秒,超时后进入busy状态;EVALSHA比EVAL更安全,因复用缓存SHA值并支持预热;限流须在Redis端原子实现;禁用KEYS、HGETALL等高危操作,避免大循环与字符串拼接。

Redis的Lua脚本一旦写得不加约束,会直接拖垮整个实例——不是因为逻辑错,而是它在单线程里霸占CPU、阻塞所有其他命令。你不需要禁用Lua,但必须从执行时长、调用频次和脚本结构三方面设防。
怎么监控Lua脚本是否超时运行
Redis默认对Lua脚本执行设了lua-time-limit(毫秒),超时后进入“busy”状态,后续请求会被拒绝或延迟。这不是报错,而是静默卡住,容易被忽略。
- 检查当前配置:
CONFIG GET lua-time-limit,默认是5000(5秒) - 观察慢日志:
SLOWLOG GET 10,如果看到EVAL或EVALSHA耗时接近或超过该值,说明脚本已触边 - 关键指标:通过
INFO commandstats看cmdstat_eval和cmdstat_evalsha的usec_per_call均值,持续高于2000ms就要介入 - 注意:超时后Redis不会自动杀脚本,需手动执行
SCRIPT KILL(仅对只读脚本有效);若已写入数据,只能SHUTDOWN NOSAVE重启
为什么EVALSHA比EVAL更安全
EVALSHA复用已缓存脚本的SHA1摘要,省去传输和解析开销,还能配合客户端做脚本预热和版本控制,天然降低突发流量冲击。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 首次用
SCRIPT LOAD上传脚本,返回SHA值(如"2e8b4a9d7f...") - 后续全部走
EVALSHA 2e8b4a9d7f... 0 key1 key2,而非重复传几百行Lua - 若返回
NOSCRIPT错误,说明脚本未加载,应立刻fallback到SCRIPT LOAD + EVALSHA流程,而不是退回到EVAL - 多个服务共用同一Redis时,不同应用上传同名逻辑但不同版本的脚本会导致SHA冲突,建议在脚本开头加注释标识版本,如
-- v2.1.0 stock_deduct
如何限制Lua脚本的调用频率
限流不能只靠客户端,必须在Redis端用Lua原子实现,否则分布式环境下计数器会失准。
- 典型漏桶限流脚本中,
redis.call("INCR", key)+redis.call("EXPIRE", key, window)必须包裹在同一脚本里 - 避免在脚本里用
redis.call("TIME")做时间判断——它返回的是Unix秒级时间,精度不够,推荐用redis.call("TIME")[1]取秒,再结合KEYS[1]构造带时间戳的key,如"rate:ip:192.168.1.1:2026040412" - 慎用
for循环遍历大集合:redis.call("SMEMBERS", key)返回结果过大时,Lua内存暴涨且无法GC,应改用SSCAN分批处理 - 每个脚本内
redis.call调用次数建议≤3次;超过5次时,执行耗时呈指数增长,实测平均增加40%以上延迟
哪些操作在Lua里特别危险
有些看似无害的操作,在Lua上下文中会放大性能风险,尤其在高QPS场景下。
-
redis.call("KEYS", "*pattern*"):全库扫描,O(N)复杂度,禁止在生产脚本中出现 -
redis.call("HGETALL", key):哈希字段超1000个时,序列化+网络传输开销陡增,改用HSCAN - 字符串拼接大量内容,如
local s = "" for i=1,10000 do s = s .. tostring(i) end:Lua字符串不可变,每次拼接都新建对象,极易触发内存抖动 - 使用
math.random()或os.time():非纯函数,影响脚本可复制性,主从同步可能出错;改用Redis提供的redis.call("INCR", "seq")等确定性方式
真正难的不是写出能跑的Lua脚本,而是在原子性、性能、可维护性之间做精确取舍——比如为保原子性把5个命令塞进一个脚本,结果因第4个redis.call查了个大Hash,让整个Redis卡住2秒。这种细节,监控看不到,日志不报错,只有压测时的P99延迟曲线会突然翘尾。


















