eval比多次命令调用快,因脚本在单次rtt内原子执行,避免网络往返、客户端计算、序列化损耗及竞态;evalsha通过缓存sha进一步减少传输与解析开销。

为什么EVAL比多次命令调用快得多
因为Redis执行Lua脚本时,整个脚本在单次网络往返中完成——客户端发一次请求,服务端跑完全部逻辑再回一次响应。而等价的多个独立命令(比如GET + 条件判断 + SET)需要至少两次RTT,中间还夹着客户端CPU计算和网络排队。
常见错误现象:用redis.get(key)拿到值后在Node.js里做if判断,再决定是否redis.set(key, newVal)——这已经暴露了竞态窗口,且延迟翻倍。
- 网络开销占比高:在跨机房或容器间通信中,一次RTT可能达5–20ms;5次命令就是100ms+,而一个Lua脚本仍是1次RTT
- 服务端无序列化/反序列化损耗:Lua脚本内调用
redis.call("INCRBY", KEYS[1], ARGV[1])直接操作内存对象,不经过RESP编解码 - 避免客户端线程阻塞:Node.js/V8或Python GIL下,同步逻辑会拖慢事件循环;Lua在Redis单线程内执行,不抢占客户端资源
EVALSHA如何进一步压缩延迟
EVALSHA不是“更快的EVAL”,而是跳过脚本传输和SHA重计算的缓存机制——前提是脚本已通过SCRIPT LOAD预加载或首次EVAL触发过缓存。
使用场景:微服务集群中多个实例反复执行同一限流脚本,若每次都发完整脚本(几百字节),在QPS 5k+时会明显增加带宽压力和解析耗时。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 首次执行必须用
EVAL,Redis自动缓存SHA1并返回摘要;后续改用EVALSHA sha1 1 rate_limit_key 10 - 客户端需自己维护SHA缓存(如Map
),失败时降级回 EVAL并刷新缓存 - 注意
SCRIPT FLUSH会清空所有缓存SHA,K8s滚动更新或配置变更时可能触发意外降级
服务端计算真正省掉的是什么
不是“少写了两行代码”,而是绕开了三类开销:网络协议栈处理、客户端语言运行时调度、跨进程/跨机器状态同步。
例如分布式库存扣减:客户端读取stock后判断是否>0,再发DECR——这中间库存可能被其他服务改写;而Lua脚本里if tonumber(redis.call("GET", KEYS[1])) > 0 then redis.call("DECR", KEYS[1]) end全程在Redis内存中完成,没有中间态暴露。
- Redis单线程模型保证脚本内所有
redis.call原子执行,无需MULTI/EXEC也无法被中断 - 脚本内不能访问外部系统(如HTTP、DB),所以“服务端计算”仅指Redis内部数据闭环运算
- 超时风险真实存在:
lua-time-limit默认5000ms,复杂循环或大key遍历易触发BUSY错误,必须配合SCRIPT KILL预案
容易被忽略的性能陷阱
脚本越短越好,但“短”不等于“功能少”,而在于避免非Redis操作和隐式开销。
典型问题:在Lua里用for i=1, #KEYS do redis.call("HGETALL", KEYS[i]) end遍历100个key——这实际触发100次内部命令调用,比客户端分批pipeline更慢。
- 优先用Redis原生命令组合:比如
HGETALL比循环HGET快,LRANGE key 0 -1比while循环LPOP安全 - 禁止在脚本里做JSON解析、正则匹配、base64编解码——这些CPU操作会卡住Redis主线程
-
redis.pcall比redis.call多一层错误捕获开销,仅在必须容错时使用(如部分key可能不存在)










