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 脚本在服务端单线程执行,脚本越复杂、循环越多,阻塞时间越长。脚本里禁止出现会随数据规模增长的循环或对极大集合的遍历,需要处理海量数据时应拆成多批小脚本执行。