vault backup: 2026-06-08 23:08:57

This commit is contained in:
hhs
2026-06-08 23:08:57 +08:00
parent d67831afe0
commit 51de196713
136 changed files with 26069 additions and 344 deletions
+154
View File
@@ -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