首页/数据/Redis 进阶速查/Lua 脚本:原子执行与 EVALSHA
数据结构与选型42 条

Lua 脚本:原子执行与 EVALSHA

EVAL 在服务端执行 Lua 实现复合原子操作;脚本先 SCRIPT LOAD 拿 SHA 再 EVALSHA,可省去每次传输脚本正文的开销。

Redis 进阶速查数据更新于 2026-10-13

Lua 脚本是什么

Redis 允许把一段 Lua 脚本交给服务端执行,脚本内的多条命令按顺序一次跑完,执行期间不会被其他命令插入,因此天然具备原子性。这让「读判断再写」这类复合操作不必靠事务或客户端加锁就能安全完成。脚本有两种调用方式:EVAL 每次把脚本正文传过去,SCRIPT LOAD 预先上传并拿到 SHA1,之后用 EVALSHA 只传摘要,省掉重复传输脚本的开销。

基本信息

执行EVAL script numkeys key1 key2 ... arg1 arg2 ...
预载SCRIPT LOAD script,返回 SHA1 摘要
调用EVALSHA <sha1> numkeys key... arg...
清理SCRIPT FLUSH 清空脚本缓存,SCRIPT EXISTS 检查是否存在
集群约束所有参与路由的键必须放进 KEYS 数组,且必须同槽

典型使用场景

  • 安全释放分布式锁:比较锁持有者后删除,避免误删他人锁
  • 限流与配额扣减:删除窗口外记录、统计数量、未超限才写入,一次完成
  • 延时队列领取任务:批量取出到期任务并同时移除,多消费者不会重复领到
  • 复合条件更新:库存判断与扣减放在一起,防止并发下超卖
  • 批量小操作合并:把多次往返压缩成一次调用,降低网络往返延迟

执行与预载

执行与预载
redis-cli EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 k1 v1
redis-cli SCRIPT LOAD "return redis.call('INCR', KEYS[1])"      # 返回 sha1
redis-cli EVALSHA <sha1> 1 counter:1                            # 只传摘要
redis-cli SCRIPT EXISTS <sha1>                                  # 脚本是否仍在缓存
redis-cli EVAL "local v=redis.call('GET',KEYS[1]); if not v then return redis.call('SET',KEYS[1],ARGV[1]) end; return v" 1 k1 v1

脚本内外的分工

项目约定原因
KEYS存放所有需要路由或访问的键名集群据此判断槽归属并做重定向
ARGV存放普通参数与阈值不参与路由,可以放任意值
禁止命令不能调用阻塞类命令脚本执行期间会独占主线程
随机性写操作前后不能用随机结果影响数据复制与 AOF 重放需要可重复
NOSCRIPT 与自动回退:实例重启、SCRIPT FLUSH 或主从切换后脚本缓存会清空,此时 EVALSHA 返回 NOSCRIPT。成熟客户端库会自动回退为 EVAL 重新加载,自研调用封装时必须显式处理这个错误码,否则上线后偶发失败很难复现。
Lua 脚本在服务端单线程执行,脚本越复杂、循环越多,阻塞时间越长。脚本里禁止出现会随数据规模增长的循环或对极大集合的遍历,需要处理海量数据时应拆成多批小脚本执行。

易错点

  • 把需要路由的键写在 ARGV 里,单机环境看不出问题,上集群后直接报错或路由到错误节点,KEYS 与 ARGV 的区分必须严格遵守
  • 脚本中包含阻塞命令或长循环,导致整个实例在脚本执行期间无响应,表现为所有客户端同时超时
  • 未处理 NOSCRIPT 错误,脚本缓存被清空后调用随机失败,排查时容易误判为网络问题
  • 把脚本当作跨槽事务:集群下脚本涉及的所有键必须落在同一槽,跨槽需求只能拆分成多次调用并在业务层设计补偿

数据来源与参考

  • Redis 官方文档:EVAL / EVALSHA / SCRIPT LOAD 命令
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌