Redis Lua脚本 #

一、Lua脚本概述 #

1.1 什么是Lua脚本 #

Redis支持使用Lua脚本执行复杂操作:

  • 原子性:整个脚本作为一个原子操作执行
  • 高效:减少网络开销,一次发送多个命令
  • 灵活:支持条件判断、循环等复杂逻辑
text
Lua脚本执行流程:

┌─────────────────────────────────────────────────────────┐
│  客户端                                                 │
│  ┌─────────────────────────────────────────────────┐   │
│  │ EVAL "return redis.call('GET', KEYS[1])" 1 key │   │
│  └─────────────────────────────────────────────────┘   │
│                        │                                │
│                        ▼                                │
│  ┌─────────────────────────────────────────────────┐   │
│  │ Redis Server                                    │   │
│  │ 1. 接收脚本                                     │   │
│  │ 2. 解析脚本                                     │   │
│  │ 3. 原子执行                                     │   │
│  │ 4. 返回结果                                     │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

1.2 Lua脚本优势 #

text
Lua脚本优势:

┌─────────────────────────────────────────────┐
│ 1. 原子性                                   │
│    - 整个脚本原子执行                       │
│    - 不会被其他命令打断                     │
│                                             │
│ 2. 减少网络开销                             │
│    - 多个命令一次发送                       │
│    - 减少RTT                                │
│                                             │
│ 3. 复用性                                   │
│    - 脚本可以缓存                           │
│    - 多次调用                               │
│                                             │
│ 4. 灵活性                                   │
│    - 支持条件判断                           │
│    - 支持循环                               │
│    - 支持复杂逻辑                           │
└─────────────────────────────────────────────┘

二、基本使用 #

2.1 EVAL命令 #

bash
# EVAL: 执行Lua脚本
# 语法:EVAL script numkeys key [key ...] arg [arg ...]

# 简单示例
EVAL "return 'Hello World'" 0
# "Hello World"

# 使用KEYS和ARGV
EVAL "return KEYS[1]" 1 key1
# "key1"

EVAL "return ARGV[1]" 0 arg1
# "arg1"

# 多个KEYS和ARGV
EVAL "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 key1 key2 arg1 arg2
# 1) "key1"
# 2) "key2"
# 3) "arg1"
# 4) "arg2"

2.2 调用Redis命令 #

bash
# redis.call(): 调用Redis命令
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
# 返回mykey的值

EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue
# OK

# redis.pcall(): 调用Redis命令(错误返回错误对象)
EVAL "return redis.pcall('GET', KEYS[1])" 1 mykey

2.3 call vs pcall #

bash
# redis.call(): 命令错误时抛出异常,停止脚本
EVAL "redis.call('INCR', 'not_a_number')" 0
# (error) ERR value is not an integer or out of range

# redis.pcall(): 命令错误时返回错误对象,继续执行
EVAL "local res = redis.pcall('INCR', 'not_a_number'); return res" 0
# (error) ERR value is not an integer or out of range
# 但脚本继续执行

三、脚本缓存 #

3.1 SCRIPT命令 #

bash
# SCRIPT LOAD: 加载脚本到缓存,返回SHA1
SCRIPT LOAD "return 'Hello'"
# "b00d3d0cbeeb2c6b8c9c5c1c5c1c5c1c5c1c5c1c"

# EVALSHA: 使用SHA1执行缓存的脚本
EVALSHA b00d3d0cbeeb2c6b8c9c5c1c5c1c5c1c5c1c5c1c 0
# "Hello"

# SCRIPT EXISTS: 检查脚本是否存在
SCRIPT EXISTS b00d3d0cbeeb2c6b8c9c5c1c5c1c5c1c5c1c5c1c
# 1) (integer) 1

# SCRIPT FLUSH: 清除所有缓存脚本
SCRIPT FLUSH
# OK

# SCRIPT KILL: 杀死正在运行的脚本
SCRIPT KILL
# OK

3.2 使用缓存脚本 #

bash
# 加载脚本
sha=$(redis-cli SCRIPT LOAD "return redis.call('GET', KEYS[1])")

# 使用SHA执行
redis-cli EVALSHA $sha 1 mykey

# 检查脚本是否存在
if ! redis-cli SCRIPT EXISTS $sha | grep -q 1; then
    # 脚本不存在,重新加载
    sha=$(redis-cli SCRIPT LOAD "...")
fi

四、Lua语法基础 #

4.1 变量 #

bash
# 局部变量
EVAL "local x = 10; return x" 0
# (integer) 10

# 全局变量(不推荐)
EVAL "x = 10; return x" 0
# (integer) 10

# 多变量赋值
EVAL "local a, b = 1, 2; return {a, b}" 0
# 1) (integer) 1
# 2) (integer) 2

4.2 数据类型 #

bash
# nil
EVAL "return nil" 0
# (nil)

# boolean
EVAL "return true" 0
# (integer) 1

EVAL "return false" 0
# (nil)  # false在Redis中返回nil

# number
EVAL "return 3.14" 0
# "3.14"

# string
EVAL "return 'hello'" 0
# "hello"

# table
EVAL "return {1, 2, 3}" 0
# 1) (integer) 1
# 2) (integer) 2
# 3) (integer) 3

4.3 条件判断 #

bash
# if-then-else
EVAL "local x = 10; if x > 5 then return 'big' else return 'small' end" 0
# "big"

# if-elseif-else
EVAL "local x = 10; if x > 15 then return 'big' elseif x > 5 then return 'medium' else return 'small' end" 0
# "medium"

4.4 循环 #

bash
# for循环
EVAL "local t = {}; for i = 1, 5 do t[i] = i end; return t" 0
# 1) (integer) 1
# 2) (integer) 2
# 3) (integer) 3
# 4) (integer) 4
# 5) (integer) 5

# while循环
EVAL "local i = 1; while i <= 3 do i = i + 1 end; return i" 0
# (integer) 4

# 迭代器
EVAL "local t = {a=1, b=2}; local r = {}; for k, v in pairs(t) do r[#r+1] = k end; return r" 0
# 1) "a"
# 2) "b"

4.5 函数 #

bash
# 定义函数
EVAL "local function add(a, b) return a + b end; return add(1, 2)" 0
# (integer) 3

# 匿名函数
EVAL "local add = function(a, b) return a + b end; return add(1, 2)" 0
# (integer) 3

五、应用场景 #

5.1 分布式锁释放 #

bash
# 安全释放分布式锁
local lock_key = KEYS[1]
local lock_value = ARGV[1]

if redis.call("GET", lock_key) == lock_value then
    return redis.call("DEL", lock_key)
else
    return 0
end

# 使用
EVAL "local lock_key = KEYS[1]; local lock_value = ARGV[1]; if redis.call('GET', lock_key) == lock_value then return redis.call('DEL', lock_key) else return 0 end" 1 lock:resource "uuid-12345"

5.2 限流 #

bash
# 滑动窗口限流
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = tonumber(ARGV[3])

redis.call("ZADD", key, current, current)
redis.call("ZREMRANGEBYSCORE", key, 0, current - window)
local count = redis.call("ZCARD", key)

if count > limit then
    return 0
else
    return 1
end

# 使用
EVAL "local key = KEYS[1]; local limit = tonumber(ARGV[1]); local window = tonumber(ARGV[2]); local current = tonumber(ARGV[3]); redis.call('ZADD', key, current, current); redis.call('ZREMRANGEBYSCORE', key, 0, current - window); local count = redis.call('ZCARD', key); if count > limit then return 0 else return 1 end" 1 rate:user:1001 100 60000 1700000000000

5.3 库存扣减 #

bash
# 原子库存扣减
local stock_key = KEYS[1]
local amount = tonumber(ARGV[1])

local stock = tonumber(redis.call("GET", stock_key) or 0)
if stock >= amount then
    redis.call("DECRBY", stock_key, amount)
    return 1
else
    return 0
end

# 使用
EVAL "local stock_key = KEYS[1]; local amount = tonumber(ARGV[1]); local stock = tonumber(redis.call('GET', stock_key) or 0); if stock >= amount then redis.call('DECRBY', stock_key, amount); return 1 else return 0 end" 1 product:1001:stock 1

5.4 批量操作 #

bash
# 批量设置带过期时间
for i = 1, #KEYS do
    redis.call("SET", KEYS[i], ARGV[i], "EX", ARGV[#ARGV])
end
return #KEYS

# 使用
EVAL "for i = 1, #KEYS do redis.call('SET', KEYS[i], ARGV[i], 'EX', ARGV[#ARGV]) end; return #KEYS" 3 key1 key2 key3 value1 value2 value3 3600

六、最佳实践 #

6.1 使用局部变量 #

bash
# 推荐:使用局部变量
EVAL "local x = 10; return x" 0

# 不推荐:使用全局变量
EVAL "x = 10; return x" 0

6.2 避免长时间运行 #

bash
# 设置脚本超时时间(默认5秒)
# redis.conf
# lua-time-limit 5000

# 长时间运行的脚本会影响性能
# 应该拆分为多个小脚本

6.3 使用SCRIPT缓存 #

bash
# 不推荐:每次发送完整脚本
EVAL "long script..." 0

# 推荐:使用SCRIPT缓存
SCRIPT LOAD "long script..."
EVALSHA sha1 0

6.4 错误处理 #

bash
# 使用pcall处理错误
local ok, result = pcall(function()
    return redis.call("GET", KEYS[1])
end)
if not ok then
    return {err = result}
end
return result

七、总结 #

Lua脚本命令:

命令 说明
EVAL 执行脚本
EVALSHA 使用SHA执行缓存脚本
SCRIPT LOAD 加载脚本到缓存
SCRIPT EXISTS 检查脚本是否存在
SCRIPT FLUSH 清除缓存脚本
SCRIPT KILL 杀死运行中的脚本

应用场景:

场景 实现方式
分布式锁 Lua + GET + DEL
限流 Lua + ZADD + ZCARD
库存扣减 Lua + GET + DECRBY
批量操作 Lua循环

下一步,让我们学习Redis持久化!

最后更新:2026-03-27