本文是大数据系列第 43 篇,介绍 Redis Lua 脚本的使用方法,包括 EVAL 命令、错误处理函数以及典型原子操作实战案例。

完整图文版(含截图):CSDN 原文 | 掘金

为什么用 Lua 脚本

Redis 本身是单线程的,但当业务需要”读取—计算—写入”这类多步操作时,客户端与 Redis 之间的多次网络往返存在竞态条件。Lua 脚本在 Redis 服务端原子执行,解决了:

  1. 原子性:脚本执行期间不会被其他命令插入
  2. 减少网络往返:多条命令打包为一次请求
  3. 复用性:通过 SCRIPT LOAD 缓存脚本,后续用 SHA1 调用

Lua 简介

Lua 是 1993 年诞生的轻量级脚本语言,动态类型、自动内存管理、支持一等函数。Redis 内嵌 Lua 5.1 解释器,脚本运行在 Redis 进程内部,可直接访问 Redis 数据。


EVAL 命令语法

EVAL script numkeys key [key ...] arg [arg ...]
参数说明
scriptLua 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.callredis.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,减少网络传输量。


注意事项

  1. 不要在脚本中使用全局变量,始终用 local 声明局部变量,避免污染全局命名空间
  2. 脚本执行是阻塞的,耗时过长会导致 Redis 无法响应其他请求,建议设置合理超时
  3. Lua 中 Redis 错误默认会终止脚本(redis.call),需要容错时改用 redis.pcall
  4. 禁止使用随机命令(如 RANDOMKEY)或依赖时间的命令,否则主从复制时结果不一致