vault backup: 2026-06-08 23:08:57
This commit is contained in:
@@ -138,6 +138,160 @@ EVAL "return redis.call('GET', KEYS[1])" 1 mykey
|
||||
# ↑ script ↑ commands ↑ key count ↑ keys...
|
||||
```
|
||||
|
||||
### Lua 语法速查
|
||||
|
||||
Redis 内嵌的是 **Lua 5.1**,脚本运行在沙箱中(标准库受限)。如果你从没写过 Lua,这一节帮你快速建立直觉,之后看所有 Lua 脚本都不再陌生。
|
||||
|
||||
#### 变量与类型
|
||||
|
||||
Lua 是**动态类型**语言,用 `local` 声明局部变量(推荐始终使用 `local`,不加则为全局变量,容易污染脚本环境):
|
||||
|
||||
```lua
|
||||
local name = "alice" -- string
|
||||
local age = 30 -- number(Lua 5.1 只有 double 浮点数,没有 int)
|
||||
local ok = true -- boolean
|
||||
local nothing = nil -- nil ≈ 其他语言的 null / undefined
|
||||
```
|
||||
|
||||
> [!QUESTION] 为什么 Redis 里到处都是 `tonumber()`?
|
||||
> `redis.call()` 返回的永远是 **string** 或 **table**——即使 `INCR` 返回的是数字,Lua 收到的也是 `"42"`(字符串)。所以需要用 `tonumber("42")` 转为数字后才能做 `>=`、`<` 等比较运算,否则是字典序比较,结果会出错。
|
||||
|
||||
| 转换函数 | 作用 | 示例 |
|
||||
|---------|------|------|
|
||||
| `tonumber(x)` | 转为 number,失败返回 `nil` | `tonumber("42")` → `42` |
|
||||
| `tostring(x)` | 转为 string | `tostring(42)` → `"42"` |
|
||||
|
||||
#### 条件判断
|
||||
|
||||
```lua
|
||||
if 条件 then
|
||||
-- ...
|
||||
elseif 其他条件 then
|
||||
-- ...
|
||||
else
|
||||
-- ...
|
||||
end
|
||||
```
|
||||
|
||||
> [!WARNING] Lua 里没有 `switch/case`,也没有三元运算符 `? :`,全靠 `if...elseif...end` 硬写。
|
||||
|
||||
逻辑运算符和你熟悉的略有不同:
|
||||
|
||||
| 运算符 | 含义 | 说明 |
|
||||
|-------|------|------|
|
||||
| `==` | 等于 | 注意是双等号 |
|
||||
| `~=` | 不等于 | 而不是 `!=` |
|
||||
| `and` | 逻辑与 | |
|
||||
| `or` | 逻辑或 | |
|
||||
| `not` | 逻辑非 | |
|
||||
|
||||
#### 循环
|
||||
|
||||
Redis 脚本中最常见的是 `for` 循环,常用来遍历 table:
|
||||
|
||||
```lua
|
||||
-- 数值 for:从 1 到 #arr(# 是取长度运算符)
|
||||
local arr = {"a", "b", "c"}
|
||||
for i = 1, #arr do
|
||||
print(arr[i]) -- Lua 的索引从 1 开始!
|
||||
end
|
||||
|
||||
-- while
|
||||
local n = 5
|
||||
while n > 0 do
|
||||
n = n - 1
|
||||
end
|
||||
```
|
||||
|
||||
> [!NOTE] 索引从 1 开始
|
||||
> 这是 Lua 初学者踩的第一个坑:`arr[0]` 不会报错,但返回 `nil`。`KEYS` 和 `ARGV` 也是从 1 开始索引:`KEYS[1]`、`ARGV[1]`。
|
||||
|
||||
#### 函数
|
||||
|
||||
```lua
|
||||
local function add(a, b)
|
||||
return a + b
|
||||
end
|
||||
```
|
||||
|
||||
Lua 函数可以**返回多个值**(这是 Lua 的特色之一):
|
||||
|
||||
```lua
|
||||
local function divmod(a, b)
|
||||
return math.floor(a / b), a % b -- 返回商和余数
|
||||
end
|
||||
local q, r = divmod(17, 5)
|
||||
```
|
||||
|
||||
#### Table:Lua 唯一的数据结构
|
||||
|
||||
Table 同时承担了**数组**和**字典**两个角色:
|
||||
|
||||
```lua
|
||||
-- 作为数组(索引从 1 开始)
|
||||
local fruits = {"apple", "banana", "cherry"}
|
||||
print(# fruits) -- 3(# 是长度运算符)
|
||||
|
||||
-- 作为字典(键值对)
|
||||
local user = {name = "alice", age = 30}
|
||||
print(user.name) -- "alice"(等价于 user["name"])
|
||||
```
|
||||
|
||||
在 Redis 脚本里,`redis.call()` 返回多条结果时,会以 **Lua table(数组形式)** 返回,需要遍历读取。
|
||||
|
||||
#### 与 Redis 交互:redis.call / redis.pcall
|
||||
|
||||
这是 Lua 脚本调用 Redis 命令的两种方式:
|
||||
|
||||
| 方式 | 行为 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| `redis.call()` | 命令失败时**抛出错误**,脚本终止 | 需要严格保证命令成功的场景(默认推荐) |
|
||||
| `redis.pcall()` | 命令失败时**返回错误对象**,脚本继续 | 需要自行处理错误的场景 |
|
||||
|
||||
```lua
|
||||
-- redis.call:失败会中断整个脚本
|
||||
local val = redis.call('GET', KEYS[1]) -- 如果 key 不存在,返回 nil(不是错误)
|
||||
|
||||
-- redis.pcall:失败只返回 err,脚本继续
|
||||
local ok, err = redis.pcall('HGETALL', KEYS[1])
|
||||
if err then
|
||||
-- 可以在这里做降级处理
|
||||
end
|
||||
```
|
||||
|
||||
#### KEYS 与 ARGV
|
||||
|
||||
`EVAL` 命令通过位置参数把数据传进 Lua 脚本:
|
||||
|
||||
```bash
|
||||
EVAL <script> <numkeys> <key1> <key2> ... <arg1> <arg2> ...
|
||||
```
|
||||
|
||||
脚本内通过两个全局 table 访问:
|
||||
|
||||
| 全局 table | 内容 | 示例 |
|
||||
|-----------|------|------|
|
||||
| `KEYS` | 所有 key 名称,索引从 1 开始 | `KEYS[1]`、`KEYS[2]` |
|
||||
| `ARGV` | numkeys 之后的所有参数 | `ARGV[1]`、`ARGV[2]` |
|
||||
|
||||
```lua
|
||||
-- 一个完整的最小示例
|
||||
-- EVAL "..." 2 user:1 user:2 alice 30
|
||||
-- KEYS[1]="user:1" KEYS[2]="user:2"
|
||||
-- ARGV[1]="alice" ARGV[2]="30"
|
||||
|
||||
redis.call('SET', KEYS[1], ARGV[1])
|
||||
redis.call('SET', KEYS[2], ARGV[2])
|
||||
return redis.call('GET', KEYS[1])
|
||||
```
|
||||
|
||||
> [!QUESTION] KEYS 和 ARGV 为什么要分开?
|
||||
> 严格区分 key 名和普通参数,是为了让 Redis Cluster 能**自动路由**到正确的节点——Redis 根据 `KEYS` 计算 slot,如果把 key 名混在 ARGV 里,Cluster 模式下脚本会被发送到错误的节点,直接报错。
|
||||
|
||||
---
|
||||
|
||||
掌握了上面这些语法要素,下面的经典场景就不会再有「看不懂语法」的困扰了。
|
||||
|
||||
### 经典场景 1:分布式锁
|
||||
|
||||
```lua
|
||||
|
||||
Reference in New Issue
Block a user