EVALSHA避免内存泄漏的核心是复用已加载脚本的SHA1而非重复EVAL。需在客户端预热加载并本地缓存SHA1,调用时优先EVALSHA,失败则fallback至SCRIPT LOAD重试;集群下须确保SCRIPT LOAD与EVALSHA路由至同一slot,且脚本中禁用全局变量、数组下标从1开始、禁用被禁Lua函数。

直接用 EVAL 每次传脚本内容,缓存策略就废了一半——Redis 会把每个不同内容的脚本都缓存进内存,used_memory_lua 持续上涨,重启后还残留(除非手动 SCRIPT FLUSH),这不是按需加载,是“按需泄漏”。真正的按需加载,核心是复用已加载脚本的 SHA1,且只在必要时预热。
如何避免重复加载导致内存暴涨
现象:业务上线后 INFO memory 显示 used_memory_lua 占比越来越高,即使没新增功能也停不下来。
- 根本原因不是脚本多,而是每次调用都走
EVAL,Redis 把带换行/空格/注释差异的脚本全当新脚本缓存——哪怕只是改了注释或缩进 -
EVALSHA不会自动 fallback 到EVAL;如果 SHA1 不存在,它直接报错(error) NOSCRIPT No matching script. Please use EVAL. - 集群模式下,
SCRIPT LOAD只作用于当前节点,必须确保 key 路由到同一分片,否则EVALSHA在其他节点找不到脚本
按需加载的正确流程:load → cache → evalsha
所谓“按需”,是指脚本只在首次使用前加载一次,后续全靠 SHA1 调用,而不是每次请求都重传。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 客户端启动时或第一次调用前,用
SCRIPT LOAD发送脚本,拿到返回的 SHA1(如"8f4a2e9b5c1d7a3f...") - 把这个 SHA1 存在本地缓存(比如内存 Map 或 Spring 的
RedisScriptBean),**不要存在 Redis 里**——Redis 不持久化脚本 SHA1 - 后续调用统一走
EVALSHA <sha> <key_num> [key...] [arg...];若返回NOSCRIPT,说明该节点没加载过,再触发一次SCRIPT LOAD+ 重试 - Spring Boot 中推荐用
DefaultRedisScript封装,配合RedisTemplate.execute(),它内部会自动处理EVALSHAfallback
Lua 脚本里必须注意的三个硬约束
脚本写得再好,踩中这三条中的任意一条,EVALSHA 就会失效或行为异常。
- 不能用全局变量:
local counter = ...必须声明为local,否则 Redis 直接拒绝执行(报错(error) ERR Error running script (call to f_...): @user_script: line ...: Script attempted to create global variable) - 数组下标从 1 开始:
KEYS[1]、ARGV[1]是合法的,KEYS[0]或table.insert(KEYS, ...)会出错 - 禁止调用外部系统函数:
os.time()、io.open()、math.random()(除非用redis.sha1hex()等 Redis 提供的替代)——这些在 Redis Lua 5.1 环境里被禁用
缓存策略落地时最易忽略的点
很多人写了脚本、配了 EVALSHA,结果线上还是内存涨——问题往往不在脚本本身,而在加载时机和错误兜底。
- 集群环境下,
SCRIPT LOAD必须带上 hash tag(如{user:123}),确保脚本加载和后续EVALSHA请求落在同一个 slot;否则跨 slot 调用会失败 - 服务部署多个实例时,每个实例都要独立完成首次
SCRIPT LOAD,不能只靠一个实例加载就以为全集群生效 -
SCRIPT FLUSH是全局操作,生产环境慎用;更安全的做法是监控used_memory_lua,超过阈值(如 10MB)时,只 flush 未被引用的旧脚本(需自己维护脚本引用计数)

















