为什么需要

Redis 单线程,但多个命令之间不原子

> GET counter        # 100
> SET counter 101

并发场景两个客户端都读到 100、各自写 101——丢一次自增。

Lua 脚本在 Redis 内部原子执行:脚本期间不会有别的命令插入。

EVAL 基础

> EVAL "return 'hello'" 0
"hello"

> EVAL "return ARGV[1]" 0 world
"world"

> EVAL "redis.call('SET', KEYS[1], ARGV[1]); return redis.call('GET', KEYS[1])" 1 mykey hello
"hello"

格式:EVAL script numkeys key1 ... keyN arg1 ... argN

  • KEYS[]:要操作的 key 列表(必须显式声明,集群模式下用来路由)
  • ARGV[]:其他参数

原子自增 + 上限

-- KEYS[1] = 计数器 key
-- ARGV[1] = 上限
local current = tonumber(redis.call("GET", KEYS[1]) or "0")
if current >= tonumber(ARGV[1]) then
    return -1
end
return redis.call("INCR", KEYS[1])

调用:

> SET counter 0
> EVAL "...上面脚本..." 1 counter 100
(integer) 1

整个判断 + 自增在 Redis 内一次性完成——不可能两个客户端都过了上限检查。

EVALSHA:缓存脚本

> SCRIPT LOAD "return redis.call('GET', KEYS[1])"
"abc123def..."  (SHA)

> EVALSHA abc123def... 1 mykey

省网络传输。生产代码模式:

  1. 启动时 SCRIPT LOAD,存 SHA
  2. 每次调用 EVALSHA
  3. 失败时(Redis 重启后缓存丢)回退 EVAL

各语言 Redis 客户端通常封装好了这一步(如 redis-pyregister_script)。

redis.call vs redis.pcall

redis.call("GET", "x")    -- 命令失败 → 整个脚本失败
redis.pcall("GET", "x")   -- 命令失败 → 返回 error 表,脚本继续

类似 Lua 自己的 error vs pcall。

类型转换

Lua → Redis Redis → Lua
number → integer string
string → string integer → number
table (array) → array array → table
true → 1 true → nil(!!)
false → nil false → nil

true / false 在 Lua 内是布尔,传回客户端会变 1 / nil。最好用整数。

限制

Lua 脚本是 Redis 的阻塞操作——所有别的客户端都在等。

  • 不要写超长脚本——超过 lua-time-limit(默认 5 秒)会被警告
  • 默认不能 kill 已经写过数据的脚本(避免数据不一致)
  • 慎用循环、大量 redis.call

函数(7.0+):FUNCTION 替代 EVAL

Redis 7.0 引入 FUNCTION——把脚本作为持久化对象注册:

#!lua name=mylib

redis.register_function('incr_capped',
    function(keys, args)
        local n = tonumber(redis.call('GET', keys[1]) or '0')
        if n >= tonumber(args[1]) then return -1 end
        return redis.call('INCR', keys[1])
    end
)
> FUNCTION LOAD "$(cat mylib.lua)"
> FCALL incr_capped 1 counter 100

优势:脚本持久化、能命名、能版本管理。新项目优先用 FUNCTION。

何时用 Lua 脚本

  • 大量数据(脚本里 GET 一万个 key)—— 改用 MGET / Pipeline
  • 复杂业务逻辑——放应用层;Redis 只存数据
  • 需要 SUBSCRIBE / 等待——脚本阻塞期间不能 pub/sub

Lua 脚本的甜区:少量 key、原子组合 2-5 个 Redis 命令的小逻辑。

例:库存扣减

-- KEYS[1] = stock:product:123
-- ARGV[1] = 扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
local need = tonumber(ARGV[1])
if stock < need then return 0 end
redis.call('DECRBY', KEYS[1], need)
return 1
# Python 调用(redis-py)
script = client.register_script(open('decr_stock.lua').read())
ok = script(keys=['stock:product:123'], args=[1])

整个"读 → 判断 → 扣减"原子完成——防止超卖。

调试

> EVAL "return redis.error_reply('boom')" 0
(error) boom

> SCRIPT EXISTS abc123       # 检查 SHA 是否已 load
> SCRIPT FLUSH               # 清空脚本缓存(开发用)

→ 下一篇 OpenResty / Nginx + Lua