Redis单个命令无法满足原子性批量需求,因其不支持条件判断、循环或中间值计算;而Lua脚本在服务端执行,具备原子性、可读写键、做计算和条件跳转,将多次往返压缩为一次EVAL请求。

为什么 Redis 的单个命令不能满足原子性批量需求
Redis 原生的 MULTI/EXEC 虽然能打包多个命令,但本质是事务队列,不支持条件判断、循环或中间值计算。比如「先查库存,够再扣减」这种逻辑,在 MULTI 中无法实现原子性——因为 GET 返回值无法被后续 DECR 直接使用,客户端拿到结果再发命令,中间就存在竞态。
而 Lua 脚本在 Redis 服务端执行,整个脚本体天然具备原子性:要么全部执行完,要么因错误中断,不会被其他客户端命令穿插。更重要的是,它能读写键、做简单计算、条件跳转,把原本需要 3–5 次网络往返的逻辑压缩成一次 EVAL 请求。
如何用 EVAL 正确传参并避免常见报错
Lua 脚本里不能直接访问 Redis 键名字符串,必须通过 KEYS 和 ARGV 两个 table 传入——这是最容易出错的地方。硬编码键名(如 redis.call("GET", "user:123"))看似能跑,但会导致 Redis 无法预判脚本操作哪些键,从而在集群模式下直接报错 (error) ERR EvalSha is not supported in cluster mode 或 NOAUTH Authentication required(因脚本未被正确哈希路由)。
-
KEYS[1]对应第一个键名(如"stock:itemA"),必须是真实存在的 Redis key -
ARGV[1]对应第一个参数(如期望扣减数量"5"),类型始终为字符串,需用tonumber()转换 - 脚本中所有
redis.call()操作的键,都必须出现在KEYS数组里,否则集群模式下执行失败
示例:安全扣减库存
eval "local stock = redis.call('GET', KEYS[1]); if not stock or tonumber(stock) < tonumber(ARGV[1]) then return 0 end; redis.call('DECRBY', KEYS[1], ARGV[1]); return 1" 1 stock:itemA 5
EVALSHA 与缓存脚本的实用边界
重复执行相同逻辑时,用 EVALSHA 替代 EVAL 能省去每次传输脚本体的开销,但前提是脚本已通过 SCRIPT LOAD 预加载。不过要注意:Redis 不会自动管理脚本生命周期,SCRIPT FLUSH 会清空所有已加载脚本,而重启 Redis 同样丢失;生产环境若依赖 EVALSHA,必须确保脚本加载逻辑幂等(比如启动时检查 SCRIPT EXISTS,不存在则 SCRIPT LOAD)。
- 脚本内容任意字符变化(包括空格、注释)都会导致 SHA1 值不同,
EVALSHA找不到对应脚本,返回(error) NOSCRIPT No matching script. Please use EVAL. - 调试阶段建议始终用
EVAL,上线后才切到EVALSHA+ 预加载机制 - 单次调用频率不高(如每秒几次)时,
EVAL的额外带宽成本远低于维护脚本缓存的复杂度
Lua 脚本的性能和限制要心里有数
Redis 对 Lua 脚本执行有硬性约束:默认超时 5 秒(可调 lua-time-limit),且禁止调用可能阻塞的函数(如 os.execute、math.random 等)。更关键的是,脚本运行期间会阻塞当前 Redis 实例的其他命令——所以别在脚本里写死循环或大数组遍历。
- 避免在脚本中处理超过几百个元素的集合操作,改用服务端命令(如
SMEMBERS+ 客户端过滤) - 不要尝试在 Lua 里解析 JSON 字符串,Redis 6.2+ 虽支持
JSON.GET,但 Lua 本身无原生 JSON 解析能力,强行loadstring可能触发沙箱限制 - 返回值只能是 Redis 支持的类型:数字、字符串、table(会被转为数组)、
nil;返回function或userdata会直接报错
真正难的不是写对语法,而是判断「这个逻辑到底该放客户端还是塞进 Lua」——比如带复杂排序、分页、关联多 key 的聚合,通常不如拆成几个高效命令 + 客户端组装来得稳定。

















