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