本文是大数据系列第 43 篇,介绍 Redis Lua 脚本的使用方法,包括 EVAL 命令、错误处理函数以及典型原子操作实战案例。
为什么用 Lua 脚本
Redis 本身是单线程的,但当业务需要”读取—计算—写入”这类多步操作时,客户端与 Redis 之间的多次网络往返存在竞态条件。Lua 脚本在 Redis 服务端原子执行,解决了:
- 原子性:脚本执行期间不会被其他命令插入
- 减少网络往返:多条命令打包为一次请求
- 复用性:通过
SCRIPT LOAD缓存脚本,后续用 SHA1 调用
Lua 简介
Lua 是 1993 年诞生的轻量级脚本语言,动态类型、自动内存管理、支持一等函数。Redis 内嵌 Lua 5.1 解释器,脚本运行在 Redis 进程内部,可直接访问 Redis 数据。
EVAL 命令语法
EVAL script numkeys key [key ...] arg [arg ...]
| 参数 | 说明 |
|---|---|
script | Lua 5.1 脚本代码 |
numkeys | 后续 key 参数的数量 |
key [key ...] | 在脚本中通过 KEYS[1]、KEYS[2]… 访问(1-indexed) |
arg [arg ...] | 在脚本中通过 ARGV[1]、ARGV[2]… 访问(1-indexed) |
示例:
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
# 返回:["key1","key2","first","second"]
redis.call vs redis.pcall
在 Lua 脚本中执行 Redis 命令有两种方式:
| 特性 | redis.call | redis.pcall |
|---|---|---|
| 出错时行为 | 中断脚本,向客户端返回错误 | 捕获错误,返回错误对象,脚本继续执行 |
| 适用场景 | 强一致性,任何错误都应中止 | 容错操作,部分失败可接受 |
-- redis.call:出错直接抛出
redis.call('SET', KEYS[1], ARGV[1])
-- redis.pcall:出错返回错误表,脚本继续
local ok, err = pcall(redis.call, 'SET', KEYS[1], ARGV[1])
if err then
return redis.error_reply(err)
end
实战案例
案例 1:原子计数器
local key = KEYS[1]
local increment = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or 0)
local new_value = current + increment
redis.call('SET', key, new_value)
return new_value
EVAL "<上方脚本>" 1 counter:hits 5
案例 2:CAS(Compare-And-Swap)
原子地”如果当前值等于期望值,则更新为新值”:
local key = KEYS[1]
local expected = ARGV[1]
local new_val = ARGV[2]
local current = redis.call('GET', key)
if current == expected then
redis.call('SET', key, new_val)
return 1
else
return 0
end
返回 1 表示更新成功,0 表示 CAS 失败。
案例 3:List 批量插入
local key = KEYS[1]
-- unpack 将 table 展开为多个参数
redis.call('LPUSH', key, unpack(ARGV))
return redis.call('LLEN', key)
EVAL "<上方脚本>" 1 mylist a b c d e
案例 4:Hash 批量写入
local key = KEYS[1]
for i = 1, #ARGV, 2 do
redis.call('HSET', key, ARGV[i], ARGV[i+1])
end
return redis.call('HLEN', key)
SCRIPT 命令:缓存与管理
# 预加载脚本,返回 SHA1 摘要
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# 使用 SHA1 执行缓存脚本(避免每次传输脚本内容)
EVALSHA <sha1> 1 mykey
# 检查脚本是否存在于缓存
SCRIPT EXISTS <sha1>
# 清空所有缓存脚本
SCRIPT FLUSH
推荐实践:生产环境将常用脚本通过 SCRIPT LOAD 预热,业务代码只发送 SHA1,减少网络传输量。
注意事项
- 不要在脚本中使用全局变量,始终用
local声明局部变量,避免污染全局命名空间 - 脚本执行是阻塞的,耗时过长会导致 Redis 无法响应其他请求,建议设置合理超时
- Lua 中 Redis 错误默认会终止脚本(
redis.call),需要容错时改用redis.pcall - 禁止使用随机命令(如
RANDOMKEY)或依赖时间的命令,否则主从复制时结果不一致