diff --git a/hzh/REDIS/Lua脚本.md b/hzh/REDIS/Lua脚本.md
new file mode 100644
index 0000000..4668356
--- /dev/null
+++ b/hzh/REDIS/Lua脚本.md
@@ -0,0 +1,344 @@
+---
+tags: [redis, lua, scripting, atomicity]
+create time: 2026-05-31 14:30
+---
+
+# Redis Lua 脚本
+
+## 概述
+
+Redis 从 **2.6** 版本开始内嵌了 Lua 解释器,允许开发者将一段 Lua 代码直接发送到 Redis 服务器执行。它不是让你在 Redis 里写业务逻辑,而是用来把**多个命令打包成一个不可分割的原子操作**。
+
+> [!question]- 💡 思考:为什么不用 Pipeline?
+>
+> Pipeline 确实能把多个命令一起发出去——但它只是"批量发送",中间的任何一步都可能被其他客户端插入。想象你在银行取钱:先查余额,再扣款。如果这两步之间被别人插入了转账,你就可能取超。Lua 脚本相当于"查余额并扣款"一个柜台窗口一条龙办完,中间没人能插队。
+
+核心能力只有三件事:
+
+| 能力 | 说明 | 对应传统方案 |
+|------|------|-------------|
+| **原子性** | Redis 单线程串行执行,脚本运行期间无并发 | CAS / MULTI-EXEC + 业务判断 |
+| **一次 RTT** | 整段逻辑只往返一次网络 | Pipeline 多次拼接 |
+| **可复用** | 脚本按 SHA1 缓存,后续调用仅传指纹 | 每次发送完整脚本的开销 |
+
+---
+
+## 一、基本用法
+
+### 1.1 EVAL:发送并执行
+
+```
+EVAL script numkeys key [key ...] arg [arg ...]
+```
+
+三个部分:
+
+| 参数 | 作用 | 示例 |
+|------|------|------|
+| `script` | Lua 代码字符串 | `"return redis.call('INCR', KEYS[1])"` |
+| `numkeys` | 告诉 Redis 有几个 Key(便于分隔 Key 和 Argv) | `1` |
+| `KEYS[] / ARGV[]` | 在 Lua 中通过全局表访问 | `KEYS[1]`, `ARGV[1]` |
+
+实际调用:
+
+```
+EVAL "return redis.call('INCR', KEYS[1])" 1 rate:limit:user_10086
+```
+
+返回 `integer: 1`(第一次执行)、`2`(第二次)……
+
+### 1.2 EVALSHA:按 SHA1 指纹调用
+
+每次发送完整脚本有带宽和解析成本。Redis 会对脚本做 SHA1 哈希,之后只需传指纹:
+
+```
+# 第一步:加载脚本,得到 SHA1 指纹
+SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
+→ "b78d89f7f7654..."
+
+# 第二步:用指纹调用(注意是 EVALSHA,不带脚本正文)
+EVALSHA b78d89f7f7654... 1 rate:limit:user_10086
+→ integer: 2
+```
+
+生产环境的标准流程:**启动时预加载 → 运行时只传 SHA1**。Go 的 `go-redis` 封装成了 `client.EvalSHA()`。
+
+> [!warning]- NOSCRIPT 错误
+>
+> 如果指纹对应的脚本不在缓存中(例如 Redis 重启后),`EVALSHA` 会返回 `NOSCRIPT`。健壮的客户端应当捕获这个错误,回退到 `EVAL` 重新加载。
+
+### 1.3 EVAL vs EVALSHA:何时用哪个?
+
+```mermaid
+flowchart LR
+ A[客户端] -->|首次使用| B{SHA1 命中缓存?}
+ B -->|否| C["SCRIPT LOAD\n发送完整脚本"]
+ C --> D["Redis 计算 SHA1,\n存入脚本缓存"]
+ D --> E["返回 SHA1 指纹"]
+ E --> F["客户端调用 EVALSHA\n传指纹即可"]
+
+ B -->|是| F
+ F --> G["Lua 执行完毕,\n返回结果"]
+
+ H[Redis 重启 / FLUSHALL] --> I["脚本缓存清空"]
+ I --> J["EVALSHA → NOSCRIPT"]
+ J --> K["客户端回退到 EVAL\n重新加载"]
+
+ style A fill:#e1f5fe
+ style G fill:#c8e6c9
+ style K fill:#fff9c4
+```
+
+**选择原则:**
+
+| 方案 | 优点 | 缺点 | 适用场景 |
+|------|------|------|---------|
+| `EVAL` | 简单粗暴,无需管理状态 | 每次发完整脚本,带宽+解析开销大 | 脚本只跑几次、调试阶段 |
+| `EVALSHA` | 指纹传输(约 40 字节),高性能 | 需处理 NOSCRIPT 回退 | 生产环境高频调用 |
+
+> [!tip]- 现代客户端已自动处理
+> > Go `go-redis` 的 `Eval()` 在收到 NOSCRIPT 时会自动重试;Java Jedis / Lettuce 同理。**除非手动拼命令,否则无需自己写回退逻辑**。
+
+---
+
+## 二、Lua 脚本中的两个世界
+
+Lua 脚本运行在 Redis 服务端,它与客户端通过两套固定接口通信:
+
+```mermaid
+flowchart TB
+ subgraph Client["客户端进程"]
+ direction LR
+ C1[业务代码
Go / Java / Python] --> L1["序列化参数"]
+ L1 --> C2["构造 KEYS[] & ARGV[]"]
+ C2 --> L2["EVAL / EVALSHA"]
+ L2 --> C3[Lua 字节码]
+ end
+
+ subgraph Redis["Redis Server(单线程)"]
+ direction TB
+ R1[解析命令
提取 KEYS / ARGV] --> R2[加载 Lua 解释器]
+ R2 --> R3[执行脚本]
+ R3 --> R4{redis.call?
命中命令?}
+ R4 -->|是| R5["操作数据
读写内存"]
+ R4 -->|否| R6[返回错误]
+ R5 --> R7["序列化返回值"]
+ R6 --> R7
+ R7 --> R8[写入回复缓冲区]
+ end
+
+ C3 --> T1["TCP 发送"]
+ T1 --> R1
+ R8 --> T2["TCP 响应"]
+ T2 --> C1
+
+ style Client fill:#e3f2fd
+ style Redis fill:#fce4ec
+```
+
+与客户端通过两套固定接口通信:
+
+```lua
+-- KEYS[]:所有「真正的 Key 名」放在这里
+-- ARGV[]:所有「业务参数」放在这里
+local myKey = KEYS[1] -- 第一个 Key
+local maxValue = tonumber(ARGV[1]) -- 第一个参数,需转数字
+
+-- 通过 redis.call() 调用 Redis 命令
+local count = redis.call('GET', myKey)
+if tonumber(count) < maxValue then
+ redis.call('INCR', myKey)
+ return 1 -- 放行
+end
+return 0 -- 拒绝
+```
+
+| 维度 | KEYS[] | ARGV[] |
+|------|--------|--------|
+| **用途** | Redis Key 名称 | 数值、字符串等业务参数 |
+| **类型** | string | string(数值需 `tonumber`) |
+| **Slot 计算** | 参与 Hash Tag 计算 | 不参与 |
+
+> [!tip]- 为什么一定要分 KEYS 和 ARGV?
+>
+> Redis Cluster 根据 Key 的 hash slot 路由请求。Lua 脚本中涉及多个 Key 时,Redis 需要确认它们都在同一个 slot 上。**只有 KEYS[] 中的成员会被检查**,所以必须把所有 Key 放进 KEYS[],纯参数放 ARGV[]。
+
+---
+
+## 三、返回值映射
+
+Lua 表达式的返回值会被映射为 Redis 协议类型,客户端拿到的是对应结构:
+
+| Lua 返回值 | Redis 协议 | Go `go-redis` 接收方式 |
+|-----------|-----------|----------------------|
+| `integer` | Bulk String(如 `"42"`) | `Int()` |
+| `"string"` | Bulk String | `String()` |
+| `nil` | Null Bulk String | `Nil` error |
+| `table{1, 2, 3}` | Array | `IntSlice()` / `StringsSlice()` |
+| `true / false` | Integer (`1 / 0`) | `Int()` |
+| `error(...)` | Error reply | error |
+
+典型的多值返回(令牌桶场景):
+
+```lua
+return {allowed, math.floor(remaining * 1000 / rate)}
+-- Go: result, _ := c.Eval(ctx, script, []string{key}, ...).IntSlice()
+```
+
+---
+
+## 四、安全边界与限制
+
+Redis 的 Lua 解释器不是通用的编程环境,它有意限制了危险操作:
+
+| 限制 | 原因 | 替代方案 |
+|------|------|---------|
+| **不允许 I/O 或系统调用** | 防止阻塞或副作用 | 只能用 `redis.call()` 操作数据 |
+| **没有 `math.random` / `os.time`** | 保证确定性回放 | 从 ARGV 传入时间戳和随机种子 |
+| **没有标准库大部分函数** | 缩小攻击面 | `string.*`, `table.*`, `math.*`(部分可用) |
+| **脚本最长 5 秒** | 防止阻塞主线程 | 超时返回 `BUSY`,Key 不会被修改 |
+| **不能有协程/异步** | Redis 单线程模型 | 同步编写即可 |
+
+> [!info]- 可用的 Lua 标准库
+>
+> Redis 内嵌的 Lua 保留了以下模块:`string`, `table`, `math`, `os`(仅 `time`/`date`/`difftime`), `tcp/udp`(禁用), `_G`。实际上,**99% 的场景只需要 `redis.call` + 基础 table/string/math** 就足够了。
+
+---
+
+## 五、实战模式速查
+
+下面是生产中最常见的几种 Lua 脚本模式,每个都配了一段最小可运行的例子。
+
+### 5.1 计数 + 条件判断
+
+场景:限流、配额检查等需要"读-判-写"原子的场景。
+
+```lua
+local key = KEYS[1]
+local maxCount = tonumber(ARGV[1])
+local window = tonumber(ARGV[2])
+
+local count = redis.call('INCR', key)
+if count == 1 then
+ redis.call('EXPIRE', key, window)
+end
+if count > maxCount then
+ return 0 -- 超限
+end
+return 1
+```
+
+### 5.2 分布式锁(SETNX + TTL / 获取 + 释放)
+
+场景:防止多实例同时修改同一条数据。
+
+Lua 脚本需要同时处理 **获取锁** 和 **释放锁** 两种情况,通过 ARGV 传入操作类型来区分:
+
+```lua
+-- KEYS[1]: 锁 key, ARGV[1]: "acquire" | "release", ARGV[2]: token (随机值), ARGV[3]: ttl (秒)
+local lockKey = KEYS[1]
+local action = ARGV[1]
+local token = ARGV[2]
+local ttl = tonumber(ARGV[3])
+
+if action == "acquire" then
+ -- SET IF NOT EXISTS: 只在没有锁时创建,设置过期时间防死锁
+ if redis.call('SET', lockKey, token, 'NX', 'EX', ttl) then
+ return 1 -- 获取成功
+ end
+ return 0 -- 锁已被持有
+
+elseif action == "release" then
+ -- 只允许自己释放(校验 token),避免误删别人的锁
+ if redis.call('GET', lockKey) == token then
+ redis.call('DEL', lockKey)
+ return 1 -- 释放成功
+ end
+ return 0 -- token 不匹配,不是自己的锁
+end
+```
+
+> [!note]- 为什么不能用 GET + DEL?
+>
+> GET 然后 DEL 不是原子的——两个命令之间其他客户端可能已经获取了这把锁。**必须在 Lua 里把检查和删除打包**。
+>
+> > [!tip]- Redlock 与简化版
+> > > 生产环境推荐使用成熟的实现(如 go-redsync),它们处理了时钟漂移、节点宕机等边缘情况。上面的脚本是理解原理的最小示例。
+
+### 5.3 Sorted Set 滑动窗口
+
+场景:精确统计最近 N 秒内的请求数(见 `[[分布式限流]]` 2.2 节)。
+
+```lua
+local key = KEYS[1]
+local windowMs = tonumber(ARGV[1]) * 1000
+local maxLen = tonumber(ARGV[2])
+local now = tonumber(ARGV[3])
+local member = ARGV[4]
+
+redis.call('ZREMRANGEBYSCORE', key, '-inf', now - windowMs)
+local count = redis.call('ZCARD', key)
+if count >= maxLen then
+ return count
+end
+redis.call('ZADD', key, now, member)
+redis.call('EXPIRE', key, math.ceil(windowMs / 1000))
+return count + 1
+```
+
+### 5.4 ⚠️ 常见陷阱
+
+| 陷阱 | 表现 | 对策 |
+|------|------|------|
+| **KEYS 命令通配** | `KEYS *pattern*` 会遍历全库,阻塞主线程 | 用 `SCAN` 替代(Lua 中通过 `redis.call('SCAN', ...)`) |
+| **大 Key 操作** | `HGETALL` / `SMEMBERS` 拉出百万级元素 | 改分批接口或换数据结构 |
+| **误以为 MULTI-EXEC 是原子包** | WATCH/MULTI-EXEC 只在 EXEC 时检测冲突,中间仍可被读 | Lua 才是真·原子执行体 |
+| **过期时间不一致** | EXPIRE 和写操作不在一个脚本里,过期后数据丢失 | 用 Lua 打包读写 + EXPIRE |
+
+---
+
+## 六、实战注意事项
+
+除了上面的模式,还有几个容易踩坑的细节:
+
+> [!danger]- 原子性 ≠ 正确性
+>
+> Lua 保证了脚本内的 Redis 命令不被穿插,但**不会帮你处理业务逻辑错误**。例如限流脚本允许了请求 A,后续你的业务代码依然可能因为网络重试造成重复执行。原子性只是防并发的一层保障。
+
+> [!tip]- 脚本体积优化
+>
+> - 生产环境中脚本建议控制在 **10KB 以内**。太大不仅浪费带宽,还会影响缓存命中率。
+> - 善用局部变量减少全局查找开销。Lua 表索引访问比全局变量快很多。
+> - 避免在脚本内部构造动态命令——每次 `redis.call` 都有哈希成本,固定结构更好。
+
+> [!info]- Redis 6+ 的 Cluster 兼容性增强
+>
+> Redis 6.2 起引入了 `@ro`(只读)、`@rw`(读写)等命令标志。EVAL 默认标记为 `@admin`,需要 ACL 的 `scripting` 权限。如果你的 Redis 开了 ACL,记得给应用账号授予对应的脚本权限。
+
+---
+
+## 七、调试技巧
+
+Lua 脚本在服务端运行,不能 `print` 断点。常用的排错方式:
+
+| 方法 | 用法 | 适用场景 |
+|------|------|---------|
+| **`redis-cli --eval`** | 本地直接传参测试脚本 | 快速验证逻辑 |
+| **`SCRIPT DEBUG`** (Redis 7+) | 开启逐行调试追踪 | 定位诡异行为 |
+| **返回中间值** | `return {step1, step2, count}` | 分段观察数据流 |
+| **`SCRIPT KILL`** | 强制终止正在运行的脚本 | 脚本卡住超时的时候 |
+
+本地测试示例:
+
+```bash
+redis-cli --eval test.lua , rate:key 100 60
+```
+
+注意 `,` 前后的参数顺序——`,` 右边先列 **Keys**,再跟 **Args**(如示例中的 `rate:key` 是 Key,`100 60` 是两个 Argv)。
+
+---
+
+## 关联笔记
+
+- [[分布式限流]] — Redis Lua 脚本最核心的应用场景
+- [[02-服务治理/06-容错模式]] — 限流在容错体系中的位置
diff --git a/hzh/REDIS/分布式限流.md b/hzh/REDIS/分布式限流.md
index 10b5f89..29ba670 100644
--- a/hzh/REDIS/分布式限流.md
+++ b/hzh/REDIS/分布式限流.md
@@ -91,6 +91,19 @@ key := fmt.Sprintf("rate:limit:%s:%d", userId, windowStart)
这段代码短小精悍,看起来已经够用了。但它有一个**隐蔽的边界问题**。
+> [!info]- 💡 Redis Key 中的 `id` 是什么?
+>
+> 文中的 `id` 不是固定指某个业务字段,而是**限流维度的路由标识(identifier)**——你想限制谁,它就等于谁的唯一标识。常见取:
+>
+> | 限流维度 | `id` 实际取值 | Key 示例 |
+> |---------|-------------|----------|
+> | **用户级**(L2) | 用户 ID / 账号 ID | `rate:limit:user_10086:1717132800` |
+> | **IP 级**(L1) | 客户端 IP | `rate:limit:192.168.1.42:1717132800` |
+> | **接口级**(L3) | 接口路径或名称 | `rate:limit:/api/register:1717132800` |
+> | **API Key 级** | 客户端凭证 | `rate:limit:app_key_xxx:1717132800` |
+>
+> 生产环境中通常是**多级组合**:例如同时用 `userId + endpoint` 作为复合 Key,做到细粒度控制。
+
### 1.2 边界突发:窗口的"双杀"效应
> [!warning]- ⚠️ 先看一个具体例子
@@ -437,5 +450,6 @@ flowchart LR
## 关联笔记
+- [[Lua脚本]] — Redis Lua 脚本基础概念与使用模式
- [[02-服务治理/06-容错模式]] — 限流是容错体系的第三层防线
- [[02-服务治理/01-API网关]] — API 网关层的限流插件集成