diff --git a/hzh/REDIS/EVALSHA预加载.md b/hzh/REDIS/EVALSHA预加载.md
new file mode 100644
index 0000000..eeef76b
--- /dev/null
+++ b/hzh/REDIS/EVALSHA预加载.md
@@ -0,0 +1,177 @@
+---
+tags: [redis, evalsha, script-load, performance, optimization]
+create time: 2026-06-01 10:30
+---
+
+# EVALSHA 预加载与性能优化
+
+## 概述
+
+在 Redis 生产环境中,频繁使用 `EVAL` 传输完整脚本会带来**可量化的性能损耗**。`SCRIPT LOAD` + `EVALSHA` 的组合是解决这个问题的标准方案。本节深入讲解它的工作原理、性能收益和最佳实践。
+
+对应薄弱点 Q14(EVALSHA 预加载的核心好处)。
+
+## 一、工作原理
+
+### 1.1 流程分解
+
+```mermaid
+sequenceDiagram
+ participant App as 应用实例
+ participant Redis as Redis Server
+
+ Note over App,Redis: ── 阶段1:启动时一次性预加载 ──
+ App->>Redis: SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
+ Redis->>Redis: SHA1(脚本内容) = b78d89f7...
+ Redis->>App: "b78d89f7..." (SHA1 指纹)
+
+ Note over App,Redis: ── 阶段2:运行时高频调用 ──
+ loop 每次请求
+ App->>Redis: EVALSHA b78d89f7... 1 mykey
+ Redis->>Redis: 查找缓存中的脚本 → 命中
+ Redis->>Redis: 执行脚本
+ Redis-->>App: 返回值
+ end
+
+ Note over App,Redis: ── 异常路径:NOSCRIPT ──
+ alt Redis 重启 / FLUSHALL
+ App->>Redis: EVALSHA b78d89f7... 1 mykey
+ Redis-->>App: NOSCRIPT 错误
+ App->>Redis: SCRIPT LOAD "..." (重新加载)
+ App->>Redis: EVALSHA <新SHA> ...
+ end
+```
+
+### 1.2 两种方式的对比
+
+| 维度 | EVAL | EVALSHA |
+|------|------|---------|
+| **传输内容** | 完整脚本字符串(通常 500B ~ 5KB) | SHA1 指纹(固定 40 字节) |
+| **Redis 处理** | 计算 SHA1 + 存入缓存 + 执行 | 直接查缓存 + 执行 |
+| **网络开销** | 大(每次都发脚本) | 小(只发 40 字节) |
+| **适用场景** | 调试、低频脚本 | 生产环境、高频调用 |
+
+## 二、性能收益量化
+
+以典型的令牌桶 Lua 脚本为例(约 1.2KB):
+
+```
+场景:每秒 10,000 次限流请求
+
+EVAL 模式:
+ - 每次传输:~1.2 KB
+ - 每秒总带宽:10,000 × 1.2 KB = 12 MB/s
+
+EVALSHA 模式:
+ - 每次传输:40 字节(SHA1)
+ - 每秒总带宽:10,000 × 40 B = 400 KB/s
+
+节省:12 MB/s - 0.4 MB/s ≈ 11.6 MB/s(节省 96.7%)
+```
+
+对于高频限流场景,这个差异非常可观。即使脚本只有几百字节,乘以百万级 QPS 后也是显著的网络 IO。
+
+## 三、Go-Redis 中的实现
+
+### 3.1 显式预加载(推荐生产使用)
+
+```go
+// 程序启动时统一加载
+func initScripts(ctx context.Context, rdb *redis.Client) error {
+ scripts := map[string]string{
+ "token_bucket": tokenBucketLua,
+ "sliding_window": slidingWindowLua,
+ "fixed_window": fixedWindowLua,
+ }
+
+ shas := make(map[string]string)
+ for name, lua := range scripts {
+ sha, err := rdb.ScriptLoad(ctx, lua).Result()
+ if err != nil {
+ return fmt.Errorf("load %s: %w", name, err)
+ }
+ shas[name] = sha
+ log.Printf("loaded [%s]: SHA=%s", name, sha)
+ }
+
+ // 保存到全局或依赖注入容器
+ return nil
+}
+```
+
+### 3.2 NOSCRIPT 容错处理
+
+```go
+// 捕获 NOSCRIPT 错误,自动回退到 EVAL
+func safeEval(ctx context.Context, rdb *redis.Client, sha, lua string, keys []string, args ...interface{}) (interface{}, error) {
+ result, err := rdb.EvalSHA(ctx, sha, keys, args...).Result()
+ if err == nil {
+ return result, nil
+ }
+
+ // 不是 NOSCRIPT 错误,直接返回
+ if !isNoScript(err) {
+ return result, err
+ }
+
+ // NOSCRIPT:重新加载并再次尝试
+ newSHA, err := rdb.ScriptLoad(ctx, lua).Result()
+ if err != nil {
+ return nil, err
+ }
+
+ result, err = rdb.EvalSHA(ctx, newSHA, keys, args...).Result()
+ return result, err
+}
+
+func isNoScript(err error) bool {
+ if err == nil {
+ return false
+ }
+ errStr := err.Error()
+ return strings.Contains(errStr, "NOSCRIPT") ||
+ strings.Contains(errStr, "no such script")
+}
+```
+
+## 四、什么时候不需要预加载?
+
+| 场景 | 建议 |
+|------|------|
+| 每个请求都调用的限流脚本 | ✅ 必须预加载 |
+| 每天只跑几次的管理脚本 | ⭕ 用 EVAL 即可,省得维护状态 |
+| 开发/测试环境 | ⭕ Eval 的自动重试已够用 |
+| Redis 集群多节点 | ✅ 必须在每个节点上都加载(通过 Sentinel 或 Cluster 客户端自动处理) |
+
+> [!warning]- 分布式部署注意
+>
+> 如果你的 Go 服务连接到 Redis Cluster,`ScriptLoad` 会自动在所有主节点上加载脚本。但如果你用了连接池连接多个独立 Redis 实例(非 Cluster 模式),每个实例都需要单独 ScriptLoad——或者使用统一的代理层(如 Twemproxy、Codis)。
+
+## 五、常见误区
+
+| 误区 | 真相 |
+|------|------|
+| EVALSHA 执行更快 | ❌ EVALSHA 和 EVAL 的**执行速度几乎一样**,区别只在传输带宽 |
+| 只需要加载一次永远有效 | ❌ Redis 重启、FLUSHALL、内存淘汰都会清空脚本缓存 |
+| EVALSHA 可以绕过沙箱限制 | ❌ EVALSHA 的沙箱规则和 EVAL 完全相同 |
+| 语法更简洁 | ❌ 需要额外传 SHA1,反而多了步骤 |
+
+## 六、监控指标
+
+在生产环境中,建议监控以下 EVALSHA 相关指标:
+
+```bash
+# Redis 端:脚本缓存统计
+SCRIPT STATS # Redis 7+,查看各脚本调用次数和缓存命中率
+
+# Go 端:
+- EvalSHA 失败率(NOSCRIPT 占比)
+- EvalSHA 平均延迟 vs Eval 平均延迟
+- Script Load 次数(过高可能说明缓存丢失频繁)
+```
+
+## 关联笔记
+
+- [[Go-Redis Lua 调用指南]] — Go-Redis 中 Lua 调用的完整封装方式
+- [[Lua脚本]] — Redis Lua 基础概念
+- [[分布式限流]] — EVALSHA 在限流架构中的实际位置
diff --git a/hzh/REDIS/Go-Redis Lua 调用指南.md b/hzh/REDIS/Go-Redis Lua 调用指南.md
new file mode 100644
index 0000000..3e2eed1
--- /dev/null
+++ b/hzh/REDIS/Go-Redis Lua 调用指南.md
@@ -0,0 +1,233 @@
+---
+tags: [redis, lua, go-redis, evalsha, script-load]
+create time: 2026-06-01 10:30
+---
+
+# Go-Redis Lua 脚本调用完整指南
+
+## 概述
+
+在生产环境中用 Redis + Lua 做限流,最核心的技能不是写 Lua 脚本本身——而是**如何在 Go 代码中优雅地管理、加载和执行这些脚本**。本节讲解 Go-Redis 库的调用方式、预加载流程和常见陷阱。
+
+对应你的薄弱点:Q12(Lua 在 Go-Redis 中的调用)、Q14(EVALSHA 预加载)。
+
+## 一、三种调用方式对比
+
+### 1.1 Eval:发送脚本正文
+
+```go
+const script = `
+local key = KEYS[1]
+return redis.call('INCR', key)
+`
+
+// 每次请求都传输完整的脚本字符串
+result, err := client.Eval(ctx, script, []string{"mykey"}, 100).Int()
+```
+
+**问题:** 每次都要把脚本文本通过网络传给 Redis,带宽浪费严重。
+
+### 1.2 EvalSHA:只传 SHA1 指纹
+
+```go
+// 脚本 SHA1: b78d89f7f7654...
+result, err := client.EvalSHA(ctx, "b78d89f7f7654...", []string{"mykey"}, 100).Int()
+```
+
+**优势:** 只传 40 字节指纹,大幅节省带宽和网络延迟。
+
+### 1.3 EVAL vs EVALSHA 选择流程
+
+```mermaid
+flowchart LR
+ A[客户端启动] --> B["SCRIPT LOAD
发送脚本正文"]
+ B --> C["Redis 计算 SHA1
缓存到脚本库"]
+ C --> D["返回 SHA1 指纹"]
+ D --> E["运行时调用 EvalSHA
传 SHA1 即可"]
+ E --> F{NOSCRIPT?}
+ F -- "否" --> G["执行成功"]
+ F -- "是" --> H["Redis 缓存已丢失
重新 SCRIPT LOAD"]
+ H --> E
+
+ style A fill:#e1f5fe
+ style C fill:#fff3e0
+ style G fill:#c8e6c9
+ style H fill:#ffebee
+```
+
+## 二、Go-Redis 最佳实践
+
+### 2.1 预加载 + 容错回退
+
+```go
+package ratelimit
+
+import (
+ "context"
+ "fmt"
+ "github.com/redis/go-redis/v9"
+)
+
+var (
+ // 生产环境推荐:程序启动时统一预加载
+ tokenBucketSHA string
+
+ tokenBucketLua = `
+local tokensKey = KEYS[1]
+local capacity = tonumber(ARGV[1])
+local rate = tonumber(ARGV[2])
+local requested = tonumber(ARGV[3])
+local now = tonumber(ARGV[4])
+
+local bucket = redis.call('HMGET', tokensKey, 'tokens', 'last_refill')
+local tokens = tonumber(bucket[1])
+local lastFill = tonumber(bucket[2])
+
+if tokens == nil then
+ tokens = capacity
+ lastFill = now
+end
+
+local elapsed = (now - lastFill) / 1000.0
+tokens = math.min(capacity, tokens + elapsed * rate)
+
+local allowed = 0
+local remaining = tokens
+if tokens >= requested then
+ tokens = tokens - requested
+ allowed = 1
+ remaining = tokens
+end
+
+redis.call('HMSET', tokensKey, 'tokens', remaining, 'last_refill', now)
+redis.call('EXPIRE', tokensKey, math.ceil(capacity / rate) * 2)
+
+return {allowed, math.floor(remaining * 1000 / rate)}
+`
+)
+
+// InitSHA 在应用启动时调用,注册所有 Lua 脚本
+func InitSHA(ctx context.Context, rdb *redis.Client) error {
+ var err error
+
+ // 预加载令牌桶脚本
+ tokenBucketSHA, err = rdb.ScriptLoad(ctx, tokenBucketLua).Result()
+ if err != nil {
+ return fmt.Errorf("script load failed: %w", err)
+ }
+
+ log.Printf("token bucket script loaded, SHA: %s", tokenBucketSHA)
+ return nil
+}
+
+// TokenBucketAllow 使用 EvalSHA 调用
+func TokenBucketAllow(ctx context.Context, rdb *redis.Client, id string, capacity float64, rate float64) (bool, int64, error) {
+ key := fmt.Sprintf("rate:tokenbucket:{%s}", id)
+ now := time.Now().UnixMilli()
+
+ result, err := rdb.EvalSHA(ctx, tokenBucketSHA, []string{key}, capacity, rate, 1, now).IntSlice()
+ if err != nil {
+ // NOSCRIPT 错误:缓存丢失,尝试用 EVAL 回退
+ if err == redis.Nil || isNoScriptError(err) {
+ // 重新加载
+ newSHA, loadErr := rdb.ScriptLoad(ctx, tokenBucketLua).Result()
+ if loadErr != nil {
+ return false, 0, loadErr
+ }
+ tokenBucketSHA = newSHA
+
+ result, err = rdb.EvalSHA(ctx, newSHA, []string{key}, capacity, rate, 1, now).IntSlice()
+ if err != nil {
+ return false, 0, err
+ }
+ } else {
+ return false, 0, err
+ }
+ }
+
+ allowed := result[0] == 1
+ waitMs := result[1]
+ return allowed, waitMs, nil
+}
+
+func isNoScriptError(err error) bool {
+ return err != nil && strings.Contains(err.Error(), "NOSCRIPT")
+}
+```
+
+### 2.2 更简单的写法:用 Eval 自动处理 NOSCRIPT
+
+如果你不想自己处理回退逻辑,可以直接用 `Eval()`——它会在收到 NOSCRIPT 时自动重试:
+
+```go
+// 这样写最简单,Go-Redis 内部已经处理了 NOSCRIPT 回退
+func TokenBucketAllowSimple(ctx context.Context, rdb *redis.Client, id string, capacity, rate float64) (bool, int64, error) {
+ key := fmt.Sprintf("rate:tokenbucket:{%s}", id)
+ now := time.Now().UnixMilli()
+
+ result, err := rdb.Eval(ctx, tokenBucketLua, []string{key}, capacity, rate, 1, now).IntSlice()
+ if err != nil {
+ return false, 0, err
+ }
+ return result[0] == 1, result[1], nil
+}
+```
+
+> [!tip]- 哪种方式更好?
+>
+> | 方式 | 优点 | 缺点 |
+> |------|------|------|
+> | **ScriptLoad + EvalSHA** | 明确管理脚本生命周期,启动时报错可快速定位 | 代码较繁琐,需要手写回退逻辑 |
+> | **Eval(自动重试)** | 简洁,库内部处理 NOSCRIPT | 首次或缓存丢失时多一次网络往返 |
+>
+> **推荐:** 生产环境用方案 A(显式 ScriptLoad),因为你需要确保启动时就注册成功;开发/测试环境用方案 B。
+
+## 三、关键要点总结
+
+### Q12:为什么用 Lua?
+
+Redis 的单线程模型保证 Lua 脚本**原子化执行**——读 → 算 → 写不会被并发打断。这是 Pipeline + MULTI/EXEC 做不到的:
+
+| 特性 | Lua | Pipeline | MULTI/EXEC |
+|------|-----|----------|------------|
+| 原子性 | ✅ 完全原子 | ❌ 仅批量发送 | ⚠️ EXEC 时才检测冲突 |
+| 条件判断 | ✅ 可写业务逻辑 | ❌ 只能发命令 | ❌ 同上 |
+| 返回值控制 | ✅ 自定义 | ❌ 固定格式 | ❌ 同上 |
+| 网络 RTT | 1 次 | 1 次(但无逻辑) | 1 次 |
+
+### Q14:EVALSHA 的核心收益
+
+```
+脚本大小 ≈ 1KB(典型限流脚本)
+SHA1 指纹 = 40 字节
+
+每次调用节省 ≈ 1KB - 40B ≈ 960 字节
+每秒 10000 次调用 → 节省 ≈ 9.6 MB/s
+```
+
+对于高频调用的限流场景,这个优化非常可观。
+
+## 四、常见问题排查
+
+| 症状 | 原因 | 解决 |
+|------|------|------|
+| `NOSCRIPT` 报错 | Redis 重启后缓存清空 | 添加回退逻辑或使用 Eval 自动重试 |
+| `BUSYERR` 超时 | 脚本执行超过 5 秒 | 优化脚本逻辑,避免大 Key 操作 |
+| `CROSSKEYS` 错误 | Lua 中多个 Key 不在同一 slot | 使用 Hash Tag `{}` 包裹路由键 |
+| 返回值解析失败 | Lua 返回类型与 IntSlice()/StringSlice() 不匹配 | 检查 Redis 协议映射表 |
+
+## 五、Redis 协议类型映射速查
+
+| Lua 返回值 | Redis 协议 | Go-Redis 接收方法 |
+|-----------|-----------|------------------|
+| `integer` | Bulk String `"42"` | `.Int()` |
+| `"string"` | Bulk String | `.String()` |
+| `nil` | Null Bulk String | `redis.Nil` |
+| `table{1, 2}` | Array | `.IntSlice()` / `.StringsSlice()` |
+| `true/false` | Integer `1/0` | `.Int()` |
+
+## 关联笔记
+
+- [[分布式限流]] — Lua 脚本在各限流算法中的实际应用
+- [[EVALSHA预加载]] — 更深入的性能分析和监控指标
+- [[Lua脚本]] — Redis Lua 基础概念
diff --git a/hzh/REDIS/Jitter抖动与重试策略.md b/hzh/REDIS/Jitter抖动与重试策略.md
new file mode 100644
index 0000000..628fe8d
--- /dev/null
+++ b/hzh/REDIS/Jitter抖动与重试策略.md
@@ -0,0 +1,180 @@
+---
+tags: [http, retry, jitter, backoff, distributed-system]
+create time: 2026-06-01 10:30
+---
+
+# Jitter 抖动与重试策略
+
+## 概述
+
+当客户端收到 429 响应后,简单地重试是不够的——如果所有客户端在同一个时刻同时重试,会造成"惊群效应"(thundering herd),让刚恢复的服务再次被打垮。**Jitter(随机抖动)** 是让分布式系统优雅退避的关键技术。
+
+对应薄弱点 F4:指数退避 + Jitter 的组合使用。
+
+## 一、为什么需要 Jitter?
+
+### 问题场景
+
+假设你的 API 限流阈值是 100 QPS,有 100 个客户端共享这个配额:
+
+```
+t = 0s: 所有客户端发现超限 → 等待 5 秒 (Retry-After: 5)
+t = 5s: 100 个客户端同时重试 → 瞬间又达到 100+ QPS → 再次被限流
+t = 10s: 再次全部超时...
+```
+
+这就是**惊群效应**——大量客户端在同一时刻涌入,效果相当于人为制造了一次流量脉冲。
+
+### 解决方案
+
+```mermaid
+flowchart TD
+ subgraph "无 Jitter(糟糕)"
+ C1["Client 1"] -->|wait 5s| S1["t=5s
⚡ 100 clients fire at once"]
+ C2["Client 2"] -->|wait 5s| S1
+ C3["Client N"] -->|wait 5s| S1
+ S1 --> FAIL["🔴 Server overwhelmed again"]
+ end
+
+ subgraph "有 Jitter(正常)"
+ D1["Client 1"] -->|wait 5s + jitter(+0.3s)| D4["t=5.3s
😊 gradual arrival"]
+ D2["Client 2"] -->|wait 5s + jitter(-0.7s)| D5["t=4.3s"]
+ D3["Client N"] -->|wait 5s + jitter(+1.2s)| D6["t=6.2s"]
+ D4 --> OK["🟢 Server handles gracefully"]
+ D5 --> OK
+ D6 --> OK
+ end
+
+ style FAIL fill:#ffebee
+ style OK fill:#e8f5e9
+```
+
+## 二、指数退避 + Jitter 公式
+
+### 2.1 基础公式
+
+```go
+// 第 attempt 次重试的等待时间(简单加减 Jitter 实现)
+// wait = base × 2^attempt ± random(0, base),上限不超过 Retry-After
+wait := min(base * math.Pow(2, float64(attempt)) + (-jitter ~ +jitter), retryAfter)
+```
+
+分解说明:
+
+| 部分 | 作用 | 示例值 |
+|------|------|--------|
+| `base` | 基础等待时间 | 1 秒 |
+| `2^attempt` | 指数增长因子 | attempt=0→1x, 1→2x, 2→4x, 3→8x |
+| `randomJitter` | 随机抖动,防止同步 | ±[0, base] 秒 |
+| `retryAfter` | 服务端上限,不违反约定 | Retry-After 头部的值 |
+
+### 2.2 具体数字示例
+
+假设 `base = 1s`,`Retry-After = 10s`:
+
+| 重试次数 | 指数退避 | +Jitter(±1s) | 实际等待 | capped to Retry-After |
+|---------|---------|-------------|---------|---------------------|
+| #1 | 1 × 2⁰ = 1s | 1s ± 1s | [0, 2s] | min([0,2], 10) = [0, 2] |
+| #2 | 1 × 2¹ = 2s | 2s ± 1s | [1, 3s] | min([1,3], 10) = [1, 3] |
+| #3 | 1 × 2² = 4s | 4s ± 1s | [3, 5s] | min([3,5], 10) = [3, 5] |
+| #4 | 1 × 2³ = 8s | 8s ± 1s | [7, 9s] | min([7,9], 10) = [7, 9] |
+| #5 | 1 × 2⁴ = 16s | — | — | min(16, 10) = **10s** ← 不再增长 |
+
+> [!tip]- 为什么要取 min(..., Retry-After)?
+>
+> `Retry-After` 是服务器给出的硬性上限,客户端不应超过它。但如果指数退避还没到 Retry-After 的时间,也应该遵循服务器的建议等待。
+
+## 三、Go 实现
+
+```go
+import (
+ "math/rand"
+ "time"
+)
+
+// calculateRetryDelay 计算带 Jitter 的指数退避等待时间
+func calculateRetryDelay(attempt int, retryAfterSec int64) time.Duration {
+ // 1. 指数退避
+ base := time.Second
+ wait := base * time.Duration(math.Pow(2, float64(attempt)))
+
+ // 2. 添加 Jitter:±[0, base] 的随机偏移
+ jitter := time.Duration(rand.Int63n(int64(base)))
+ if rand.Intn(2) == 0 {
+ jitter = -jitter // 50% 概率减,50% 加
+ }
+ wait = wait + jitter
+
+ // 3. 不超过 Retry-After
+ if retryAfterSec > 0 {
+ maxWait := time.Duration(retryAfterSec) * time.Second
+ if wait > maxWait {
+ wait = maxWait
+ }
+ }
+
+ return wait
+}
+
+// 使用示例
+func callWithRetry(ctx context.Context, maxRetries int, callFunc func() error) error {
+ for attempt := 0; attempt <= maxRetries; attempt++ {
+ err := callFunc()
+ if err == nil {
+ return nil
+ }
+
+ // 检查是否是 429
+ if isRateLimited(err) {
+ retryAfter := getRetryAfterHeader() // 从响应头读取
+ wait := calculateRetryDelay(attempt, retryAfter)
+
+ select {
+ case <-time.After(wait):
+ continue // 继续重试
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+
+ // 非限流错误,直接返回
+ return err
+ }
+ return fmt.Errorf("max retries (%d) exceeded", maxRetries)
+}
+```
+
+## 四、常见 Jitter 算法对比
+
+| 算法 | 公式 | 优点 | 缺点 |
+|------|------|------|------|
+| **Full Jitter**(AWS 推荐) | `random(0, min(cap, base × 2^attempt))` | 分布均匀,去抖效果好 | 可能等待很短导致过早重试 |
+| **Equal Jitter** | `min(cap, base × 2^attempt / 2 + random(0, base × 2^attempt / 2))` | 介于两者之间 | 略复杂 |
+| **Decorrelated Jitter**(AWS 默认) | `min(cap, random(base, prev_wait × 3))` | 快速分散,适应性强 | 参数调优门槛高 |
+| **简单加减 Jitter** | `base × 2^attempt ± random(0, base)` | 实现最简单 | 抖动范围小 |
+
+> [!tip]- 推荐方案
+>
+> 对于限流场景,**简单加减 Jitter**(上面 Go 代码的实现)已经足够。如果你的系统规模极大(数千客户端),建议用 Full Jitter(AWS 推荐)。
+
+## 五、完整流程总结
+
+```mermaid
+flowchart TD
+ Req["请求发出"] --> Resp{"收到响应?"}
+ Resp -- "200" --> Success["✅ 成功"]
+ Resp -- "429" --> Read["读取 Retry-After 头部"]
+ Read --> Calc["计算 wait = 指数退避 + Jitter"]
+ Calc --> Wait["⏳ 等待"]
+ Wait --> Retry{"还有重试次数?"}
+ Retry -- "是" --> Req
+ Retry -- "否" --> Fail["❌ 最终失败,上报告警"]
+
+ style Success fill:#c8e6c9
+ style Fail fill:#ffebee
+```
+
+## 关联笔记
+
+- [[分布式限流]] — 限流架构中的客户端重试策略
+- [[多级限流架构]] — 各层超时配置与降级策略设计
diff --git a/hzh/REDIS/Pipeline批量操作.md b/hzh/REDIS/Pipeline批量操作.md
new file mode 100644
index 0000000..f5d9810
--- /dev/null
+++ b/hzh/REDIS/Pipeline批量操作.md
@@ -0,0 +1,219 @@
+---
+tags: [redis, pipeline, batch, performance, multi-exec]
+create time: 2026-06-01 10:30
+---
+
+# Pipeline 批量操作
+
+## 概述
+
+Redis 的每次命令执行都有网络往返(RTT)开销。对于需要执行多条命令的场景,**Pipeline** 可以把多个命令打包成一个批次发送,大幅降低 RTT 次数。这是限流性能优化的核心手段之一(F14 考点)。
+
+## 一、为什么需要 Pipeline?
+
+### 无 Pipeline:逐条发送
+
+```
+应用 Redis
+ │ ───INCR key1─────────────▶ │ (RTT #1)
+ │ ◀──1────────────────────── │
+ │ │
+ │ ───EXPIRE key1 60─────────▶ │ (RTT #2)
+ │ ◀──1────────────────────── │
+ │ │
+ │ ───INCR key2─────────────▶ │ (RTT #3)
+ │ ◀──1────────────────────── │
+ │ │
+ │ ───EXPIRE key2 60─────────▶ │ (RTT #4)
+ │ ◀──1────────────────────── │
+ │ │
+总 RTT: 4 次,延迟 = 4 × network_latency
+```
+
+### 有 Pipeline:批量发送
+
+```
+应用 Redis
+ │ ───INCR key1 │
+ │ ───EXPIRE key1 60 │ (一次 TCP 发送)
+ │ ───INCR key2 │
+ │ ───EXPIRE key2 60─────────▶ │ (RTT #1)
+ │ ◀──[1, 1, 1, 1]─────────── │
+ │ │
+总 RTT: 1 次,延迟 = 1 × network_latency
+```
+
+**性能提升:** 如果网络 RTT 是 1ms,4 条命令从 4ms 降到 1ms——**节省了 75% 的网络延迟**。
+
+## 二、Pipeline vs MULTI/EXEC
+
+这是最容易混淆的概念。填空题 F14 的答案是 MULTI/EXEC,但实际使用中有重要的区别。
+
+### 对比表
+
+| 维度 | Pipeline | MULTI/EXEC |
+|------|----------|------------|
+| **原子性** | ❌ 每条命令独立执行 | ✅ EXEC 时整体执行 |
+| **取消支持** | ❌ 不能中途取消 | ✅ DISCARD 取消 |
+| **事务回滚** | ❌ 单条失败不影响其他 | ⚠️ EXEC 失败则全部不执行 |
+| **嵌套管道** | ❌ 不能在事务内用 Pipeline | ❌ 不支持嵌套 |
+| **Watch 支持** | ❌ 无 | ✅ 配合 WATCH 实现乐观锁 |
+
+### 关键区别:原子性
+
+```go
+// Pipeline:每条命令独立执行,前一条成功后面失败也照常返回
+pipe := rdb.Pipeline()
+pipe.Incr(ctx, "key1") // 成功
+pipe.Expire(ctx, "key1", 60) // 即使这步出错,Incr 结果仍会返回
+results, _ := pipe.Exec(ctx)
+
+// MULTI/EXEC:EXEC 时所有命令作为一个整体执行
+pipe2 := rdb.TxPipeline() // TxPipeline = MULTI/EXEC 包装
+pipe2.Incr(ctx, "key1")
+pipe2.Expire(ctx, "key1", 60)
+results2, _ := pipe2.Exec(ctx)
+```
+
+> [!tip]- Go-Redis 的 API 区分
+>
+> | API | 对应行为 |
+> |-----|---------|
+> | `rdb.Pipeline()` | 普通 Pipeline(非原子批量) |
+> | `rdb.TxPipeline()` | MULTI/EXEC 事务包装(原子批量) |
+>
+> **生产环境推荐用 `TxPipeline()`**,因为它保证了操作的原子性。
+
+## 三、在限流中的应用
+
+### 3.1 多级限流的 Pipeline 优化
+
+```go
+// 原始方式:三次独立的 Redis 调用
+func multiLevelLimitSlow(ctx context.Context, rdb *redis.Client, ip, userID, endpoint string) error {
+ if err := checkIPLimit(ctx, rdb, ip); err != nil {
+ return err // L1 拦截
+ }
+ if err := checkUserLimit(ctx, rdb, userID); err != nil {
+ return err // L2 拦截
+ }
+ if err := checkTokenBucket(ctx, rdb, endpoint); err != nil {
+ return err // L3 拦截
+ }
+ return nil
+}
+
+// Pipeline 优化:L2 + L3 合并为一个批次
+func multiLevelLimitFast(ctx context.Context, rdb *redis.Client, ip, userID, endpoint string) error {
+ // L1 单独调用(不可合并到同一个 Key space)
+ if err := checkIPLimit(ctx, rdb, ip); err != nil {
+ return err
+ }
+
+ // L2 + L3 用 Pipeline 打包
+ pipe := rdb.TxPipeline()
+
+ userResult := pipe.Eval(ctx, slidingWindowLua, []string{fmt.Sprintf("rate:user:{%s}", userID)}, 86400, 1000)
+ tokenResult := pipe.Eval(ctx, tokenBucketLua, []string{fmt.Sprintf("rate:tokenbucket:{%s}", endpoint)}, 100, 10, 1, time.Now().UnixMilli())
+
+ _, err := pipe.Exec(ctx)
+ if err != nil {
+ return err
+ }
+
+ // 检查结果
+ userCount, _ := userResult.Int()
+ if userCount >= 1000 {
+ return ErrRateLimited
+ }
+
+ tokenResult, _ := tokenResult.IntSlice()
+ if tokenResult[0] == 0 {
+ return ErrRateLimited
+ }
+
+ return nil
+}
+```
+
+### 3.2 ZAdd + EXPIRE 的 Pipeline 优化
+
+```go
+// 不用 Lua 时的简化写法(牺牲部分原子性换取简单)
+func SlidingWindowPipeline(ctx context.Context, rdb *redis.Client, id string, windowSec int, maxLen int64) error {
+ key := fmt.Sprintf("rate:sliding:%s", id)
+ now := time.Now().UnixMilli()
+ cutoff := now - int64(windowSec)*1000
+ member := ulid.Now().String()
+
+ pipe := rdb.TxPipeline()
+ pipe.ZRemRangeByScore(ctx, key, "-inf", strconv.FormatInt(cutoff, 10))
+ pipe.ZCard(ctx, key)
+ results, err := pipe.Exec(ctx)
+ if err != nil {
+ return err
+ }
+
+ currentCount := int(results[1].(*redis.IntCmd).Val())
+ if currentCount >= int(maxLen) {
+ return ErrRateLimited
+ }
+
+ pipe2 := rdb.TxPipeline()
+ pipe2.ZAdd(ctx, key, &redis.Z{Score: float64(now), Member: member})
+ pipe2.Expire(ctx, key, time.Duration(windowSec)*time.Second)
+ _, err = pipe2.Exec(ctx)
+ return err
+}
+```
+
+## 四、Pipeline 的注意事项
+
+### 4.1 注意事项汇总
+
+```mermaid
+flowchart TD
+ subgraph "⚠️ Pipeline 陷阱"
+ T1["不能用 Pipeline 做条件逻辑
无法根据 A 的结果决定 B"] --> T2["需要判断分支时用 Lua 脚本"]
+
+ T3["大批量发送可能超过 Redis
maxpacket 限制"] --> T4["控制单次 Pipeline 的命令数
建议 50~200 条"]
+
+ T5["流水线中的错误不会中断后续命令"] --> T6["需要在客户端检查每个命令的返回"]
+
+ T7["Cluster 模式下多 Key
必须在同一 slot"] --> T8["用 Hash Tag {} 保证同 slot"]
+ end
+
+ style T2 fill:#e3f2fd
+ style T4 fill:#fff3e0
+ style T6 fill:#fff3e0
+ style T8 fill:#e3f2fd
+```
+
+### 4.2 最佳实践
+
+| 场景 | 推荐方式 | 原因 |
+|------|---------|------|
+| 多条独立写入 | Pipeline (`TxPipeline`) | 减少 RTT,保持简单 |
+| 读 → 判断 → 写 | Lua 脚本 | 需要原子性和业务逻辑 |
+| 批量删除大 Key | UNLINK (逐个) | DEL 在大 Key 时会阻塞 |
+| 统计类聚合操作 | Lua 中遍历 | 避免多次往返的数据不一致 |
+
+## 五、性能基准参考
+
+假设网络 RTT = 1ms,本地部署(RTT ≈ 0.1ms):
+
+| 命令数 | 无 Pipeline (RTT×N) | Pipeline (1 RTT) | 加速比 |
+|--------|-------------------|-----------------|-------|
+| 10 条 | 10ms / 1ms | 1ms / 0.1ms | **10x** |
+| 100 条 | 100ms / 10ms | 1ms / 0.1ms | **100x** |
+| 1000 条 | 1000ms / 100ms | 1ms / 0.1ms | **1000x** |
+
+> [!note]- 实际应用
+>
+> 对于限流这种高频场景(每秒数千到数万请求),即使是 1ms 的额外 RTT 也可能成为瓶颈。Pipeline 的价值在于将 N 次 RTT 压缩为 1 次。
+
+## 关联笔记
+
+- [[分布式限流]] — 性能优化清单中的 Pipeline 章节
+- [[Go-Redis Lua 调用指南]] — 与 EvalSHA 配合使用的组合策略
+- [[SCAN命令]] — SCAN 遍历时可以结合 Pipeline 提高批量处理效率
diff --git a/hzh/REDIS/SCAN命令.md b/hzh/REDIS/SCAN命令.md
new file mode 100644
index 0000000..a1d94e1
--- /dev/null
+++ b/hzh/REDIS/SCAN命令.md
@@ -0,0 +1,240 @@
+---
+tags: [redis, scan, keys, traversal, performance]
+create time: 2026-06-01 10:30
+---
+
+# Redis SCAN 遍历命令
+
+## 概述
+
+在生产环境中,你经常需要找到所有符合某个模式的 Key——比如找出所有 `rate:limit:*` 做清理或统计。直觉上会用 `KEYS *pattern*`,但**大库中使用 KEYS 会导致服务雪崩**。`SCAN` 是唯一的正确选择。
+
+这对应你的薄弱点 Q15:为什么用 SCAN 替代 KEYS。
+
+## 一、KEYS vs SCAN:核心对比
+
+### KEYS:简单但危险的阻塞操作
+
+```bash
+# 一行搞定,但会阻塞整个 Redis
+KEYS rate:limit:*
+→ user1
+→ user2
+→ user3
+...
+```
+
+**问题:** KEYS 是一次性全量扫描——它会在单个操作中遍历所有 Key,然后一次性全部返回。在这期间:
+
+```mermaid
+flowchart LR
+ A["KEYS *pattern* 开始"] --> B["阻塞 Redis 主线程
遍历所有数据库"]
+ B --> C["如果库中有 1000 万个 Key?
可能耗时数秒到数十秒"]
+ C --> D["所有其他客户端被阻塞
超时/连接堆积"]
+ D --> E["生产事故:服务不可用"]
+
+ style A fill:#fff3e0
+ style D fill:#ffebee
+ style E fill:#b71c1c,color:#fff
+```
+
+> [!warning]- KEYS 的三个致命问题
+>
+> 1. **阻塞主线程**:Redis 单线程,KEYS 执行期间所有其他请求都无法处理
+> 2. **内存峰值**:结果一次性返回,匹配大量 Key 时可能 OOM
+> 3. **无法增量迭代**:一次返回所有结果,中途无法停止
+
+### SCAN:分步游标,永不阻塞
+
+```bash
+# 第一次调用,cursor = 0
+SCAN 0 MATCH rate:limit:* COUNT 100
+→ 1) "12345" ← 下一个 cursor
+→ 2) 1) "user1"
+ 2) "user2"
+ ...
+
+# 下次用返回的 cursor 继续
+SCAN 12345 MATCH rate:limit:* COUNT 100
+→ 1) "67890"
+→ 2) 1) "user3"
+ ...
+
+# 当 cursor 返回 "0" 时,遍历完成
+```
+
+**优势:**
+- 每次只返回少量结果(由 COUNT 控制)
+- 不会阻塞 Redis 主线程
+- 可以安全地在生产环境使用
+
+## 二、SCAN 参数详解
+
+```bash
+SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
+```
+
+| 参数 | 含义 | 示例 |
+|------|------|------|
+| `cursor` | 游标,0 表示开始,返回 0 表示结束 | `0`, `12345` |
+| `MATCH` | 模式过滤(支持 `* ? []` 通配符) | `rate:limit:*` |
+| `COUNT` | 每次迭代的元素数量(默认 10) | `COUNT 100` |
+| `TYPE` | 按数据类型过滤(Redis 4.0+) | `TYPE string` |
+
+### COUNT 参数的误区
+
+`COUNT` **不是精确返回数量**,而是告诉 Redis "这一轮大概应该返回多少条"。实际返回数量可能多也可能少。
+
+```
+COUNT 越大 → 每次返回越多,总迭代次数越少,但单次延迟略高
+COUNT 越小 → 每次返回越少,总迭代次数越多,但单次延迟极低
+```
+
+**建议值:** 根据数据量和延迟要求调整,通常 100 ~ 500 是 sweet spot。
+
+## 三、Go-Redis 中的使用
+
+### 3.1 基础用法
+
+```go
+func scanAllKeys(ctx context.Context, rdb *redis.Client, pattern string) ([]string, error) {
+ var allKeys []string
+
+ // Cursor 为 0 时会自动从第一轮开始
+ cursor := uint64(0)
+ for {
+ // KeysByPattern 内部处理了游标迭代
+ keys, nextCursor, err := rdb.Scan(ctx, cursor, pattern, 100).Result()
+ if err != nil {
+ return nil, err
+ }
+
+ allKeys = append(allKeys, keys...)
+
+ if nextCursor == 0 {
+ break // 遍历完成
+ }
+ cursor = nextCursor
+ }
+
+ return allKeys, nil
+}
+```
+
+### 3.2 ⚠️ 快捷方法(不推荐用于大数据量)
+
+```go
+// rdb.Keys() 底层调用的是 KEYS 命令——会阻塞 Redis!
+keys, err := rdb.Keys(ctx, "rate:limit:*").Result()
+```
+
+> [!note]- Go-Redis 的坑
+>
+> `rdb.Keys()` 虽然调用方便,但底层仍然是阻塞式的 `KEYS` 命令。**在大数据量场景下务必使用上面的手动 SCAN 循环**(见 3.1 节)。
+
+### 3.3 带 TYPE 过滤的高效扫描
+
+```go
+// 只找 String 类型的限流 Key
+cursor := uint64(0)
+for {
+ iter := rdb.Scan(ctx, cursor, "rate:limit:*", 200)
+ keys, _ := iter.Result()
+
+ for _, key := range keys {
+ t, _ := rdb.Type(ctx, key).Result()
+ if t.Err() != nil || t.Val() != "string" {
+ continue
+ }
+ // 处理这个限流 Key...
+ handleKey(key)
+ }
+
+ if iter.Cursor() == 0 {
+ break
+ }
+}
+```
+
+## 四、SCAN 的不一致性保证
+
+这是 SCAN 最容易让人困惑的地方——**它不保证返回的结果是某一时刻的快照**。
+
+```mermaid
+flowchart TD
+ A["SCAN 开始遍历"] --> B["过程中有新 Key 插入"]
+ B --> C{"新 Key 会被返回吗?"}
+ C -- "不一定" --> D["可能被下一轮 SCAN 返回"]
+ C -- "也可能错过" --> E["本轮不会看到"]
+
+ A --> F["过程中有 Key 被删除"]
+ F --> G{"已访问过的 Key 被删?"}
+ G -- "不会重复返回" --> H["正常,因为游标已越过"]
+ G -- "未访问到的 Key 被删?" --> I["自然跳过,不影响"]
+
+ style C fill:#fff3e0
+ style D fill:#e3f2fd
+ style E fill:#fff9c4
+```
+
+**三个一致性保证:**
+
+| 保证 | 说明 |
+|------|------|
+| ✅ 不会遗漏 | 如果一个 Key 在整个 SCAN 过程中始终存在且没被删除,它至少被返回一次 |
+| ⚠️ 可能重复 | 同一个 Key 可能在多次迭代中返回多次(去重即可) |
+| ⚠️ 可能遗漏 | 如果在 SCAN 开始前就存在的 Key 中途被删除了,它可能被跳过 |
+
+> [!tip]- 实际影响
+>
+> 对限流场景来说,SCAN 的一致性模型完全够用——你要做的通常是清理即将过期的 Key 或者统计当前有多少活跃 Key,不需要精确到某一毫秒的快照。只需要在代码中对返回的 Key 集合做 `set` 去重即可。
+
+## 五、Lua 脚本中使用 SCAN
+
+Redis Lua 中也支持 `redis.call('SCAN', ...)`,但有重要限制:
+
+```lua
+-- Lua 中可以使用 SCAN
+local cursor = 0
+repeat
+ local result = redis.call('SCAN', cursor, 'MATCH', KEYS[1], 'COUNT', 100)
+ cursor = tonumber(result[1])
+ local keys = result[2]
+ -- 处理 keys...
+until cursor == 0
+```
+
+> [!warning]- Lua + SCAN 的性能陷阱
+>
+> Lua 脚本最长只能运行 5 秒。如果你在 Lua 里用 SCAN 遍历一个包含百万级 Key 的数据库,很可能超时。解决思路:
+> - 减少 `COUNT` 参数
+> - 把 SCAN 放在 Lua 外部,用 Go 循环控制
+> - 使用 `KEYS` 替代仅在开发/小库环境中
+
+## 六、排查场景应用
+
+### 查找僵尸 Key
+
+```go
+// 找出所有 rate:limit:* 开头的 Key 并检查 TTL
+cursor := uint64(0)
+for {
+ keys, next, _ := rdb.Scan(ctx, cursor, "rate:limit:*", 200).Result()
+ for _, key := range keys {
+ ttl, _ := rdb.TTL(ctx, key).Result()
+ if ttl < 0 {
+ log.Printf("zombie key found: %s (TTL=%d)", key, ttl)
+ }
+ }
+ if next == 0 {
+ break
+ }
+ cursor = next
+}
+```
+
+## 关联笔记
+
+- [[分布式限流]] — 限流架构中 SCAN 的实际应用场景
+- [[内存持续增长排查]] — SCAN 是排查内存问题的必备工具
+- [[僵尸Key防护]] — 利用 SCAN 发现和清理僵尸 Key
diff --git a/hzh/REDIS/Sorted Set 滑动窗口.md b/hzh/REDIS/Sorted Set 滑动窗口.md
new file mode 100644
index 0000000..510caff
--- /dev/null
+++ b/hzh/REDIS/Sorted Set 滑动窗口.md
@@ -0,0 +1,195 @@
+---
+tags: [redis, sorted-set, sliding-window, lua-script]
+create time: 2026-06-01 10:30
+---
+
+# Sorted Set 实现滑动窗口详解
+
+## 概述
+
+**Sorted Set(有序集合)** 是 Redis 中最适合实现精确滑动窗口的数据结构。每条请求的时间戳作为 score,Redis 自动按分数排序,配合范围查询和清理就能实现精确的窗口统计。
+
+这个方案你标记为 Q4 薄弱点——核心在于理解它的 **运作流程、时间计算方式和空间代价**。
+
+## 一、基本思路
+
+```mermaid
+flowchart LR
+ subgraph "请求到达"
+ A["客户端构造请求"] --> B["生成唯一 member
uuid / requestId"]
+ end
+
+ subgraph "Redis 操作(Lua 原子化)"
+ B --> C["ZREMRANGEBYSCORE 清理过期"]
+ C --> D["ZCARD 计数"]
+ D --> E{"count >= max?"}
+ E -- "否" --> F["ZADD 插入新记录"]
+ E -- "是" --> G["❌ 拒绝"]
+ F --> H["EXPIRE 设 TTL"]
+ G --> I["返回 429"]
+ end
+
+ style C fill:#fff3e0
+ style D fill:#fff3e0
+ style F fill:#e8f5e9
+ style G fill:#ffebee
+```
+
+## 二、关键参数与时间计算 🔑
+
+这是你最薄弱的地方之一(对应填空题 F6)。
+
+### 2.1 Lua 脚本结构
+
+```go
+const slidingWindowLua = `
+local key = KEYS[1]
+local window = tonumber(ARGV[1]) -- 窗口大小,单位:秒
+local maxLen = tonumber(ARGV[2]) -- 最大允许的记录数
+local now = tonumber(ARGV[3]) -- 毫秒级 Unix 时间戳
+local member = ARGV[4] -- 唯一标识(UUID/RequestID)
+
+-- 步骤1:清理过期记录
+redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window * 1000)
+
+-- 步骤2:统计当前窗口内的数量
+local count = redis.call('ZCARD', key)
+
+-- 步骤3:判断并插入
+if count >= maxLen then
+ return count -- 超限,直接返回
+end
+
+-- 步骤4:放行,添加记录
+redis.call('ZADD', key, now, member)
+redis.call('EXPIRE', key, window)
+
+return count + 1
+`
+```
+
+### 2.2 清理范围详解(F6 考点)
+
+```lua
+redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window * 1000)
+ │ │
+ │ └─ 窗口起始边界(毫秒)
+ └────────── 从负无穷到这个边界
+```
+
+- **下界 `-inf`**:所有比它小的分数(更早的时间戳)都是过期的
+- **上界 `now - window * 1000`**:窗口开始的时间点
+ - `now` 是毫秒级时间戳
+ - `window` 的单位为秒
+ - 乘以 1000 是为了统一单位到毫秒
+
+**为什么用 `-inf` 而不是具体数值?**
+因为 Sorted Set 中可能存着历史遗留数据,用 `-inf` 确保一次性清理干净。如果只从某个固定值开始,可能会漏掉更早的过期记录。
+
+### 2.3 成员(member)的唯一性
+
+每个请求必须生成一个全局唯一的 member,避免 score 相同时被覆盖:
+
+```go
+import (
+ "crypto/rand"
+ "fmt"
+)
+
+func generateMember() string {
+ var b [8]byte
+ rand.Read(b[:])
+ return fmt.Sprintf("%x", b[:])
+}
+```
+
+或者用更标准的 `ulid` / `uuid`:
+
+```go
+import "github.com/oklog/ulid/v2"
+
+member := ulid.Now().String() // 单调递增,天然有序
+```
+
+> [!tip]- 为什么不用序列号?
+>
+> 分布式环境下多实例并发,简单的自增序号可能在同一毫秒产生重复。UUIDv4 / ULID / RequestID 能保证全局唯一。
+
+## 三、完整调用示例
+
+```go
+func SlidingWindowAllow(ctx context.Context, c *redis.Client, id string, windowSec int, maxRequests int) (int, error) {
+ key := fmt.Sprintf("rate:sliding:%s", id)
+ now := time.Now().UnixMilli()
+ member := generateMember()
+
+ result, err := c.Eval(ctx, slidingWindowLua, []string{key}, windowSec, maxRequests, now, member).Int()
+ if err != nil {
+ return 0, err
+ }
+ return result, nil
+}
+```
+
+## 四、空间复杂度分析(Q4 考点)
+
+这是 Q4 选择题考察的点:**在高并发场景下 Sorted Set 会成为瓶颈。**
+
+### 4.1 计算公式
+
+```
+存储空间 ≈ maxQPS × window_seconds × 单条记录大小
+```
+
+每条记录的内存开销:
+- score(double):8 字节
+- member(字符串):约 26 字节(ULID)
+- Sorted Set 节点开销:约 64 字节
+- **合计约 ~100 字节/条**
+
+### 4.2 实际数字对比
+
+| 场景 | maxQPS | window(s) | 记录数 | 内存 |
+|------|--------|-----------|--------|------|
+| 日配额(低频) | 10 | 86400 | 864,000 | ~86 MB |
+| API 接口(中频) | 100 | 60 | 6,000 | ~600 KB |
+| 高频网关 | 1000 | 60 | **60,000** | ~6 MB |
+| 超高并发 | 10000 | 60 | **600,000** | ~60 MB |
+
+> [!warning]- 什么时候不该用 Sorted Set?
+>
+> 如果你的 API 接口 QPS > 1000 且窗口 > 10s,Sorted Set 的方案就不太合适了。这时应该切换到:
+> - **令牌桶/漏桶**:O(1) 空间,适合保护后端资源
+> - **滑动窗口计数器**:O(N) 空间,N 远小于记录数
+
+### 4.3 时间复杂度
+
+| 操作 | 复杂度 | 说明 |
+|------|--------|------|
+| ZADD | O(log N) | Sorted Set 基于跳表 |
+| ZREMRANGEBYSCORE | O(log N + M) | N 是集合大小,M 是删除数 |
+| ZCARD | O(1) | Redis 内部维护计数 |
+| EXPIRE | O(1) | 设置过期时间 |
+
+高并发下每次请求都要做几次 O(log N) 操作,当 N 达到数万甚至数十万时,CPU 和延迟都会明显上升。
+
+## 五、优化方向
+
+```mermaid
+flowchart TD
+ Problem["Sorted Set 太大导致性能问题"] --> Choice{"选择优化方向"}
+
+ Choice -- "降低精度换取空间" --> SWC["切换为滑动窗口计数器
O(N) 子窗口,N << 记录数"]
+ Choice -- "保持精确但控制规模" --> REDUCE["减少窗口大小或 maxQPS
例如 60s → 10s"]
+ Choice -- "接受高频开销" --> KEEP["继续使用 Sorted Set
加监控告警"]
+
+ style Problem fill:#ffebee
+ style SWC fill:#e3f2fd
+ style KEEP fill:#fff3e0
+```
+
+## 关联笔记
+
+- [[分布式限流]] — 完整的限流算法演进路线
+- [[Lua脚本]] — Lua 脚本中的 Sorted Set 操作
+- [[滑动窗口计数器]] — 空间效率更高的替代方案
diff --git a/hzh/REDIS/僵尸Key防护.md b/hzh/REDIS/僵尸Key防护.md
new file mode 100644
index 0000000..1316928
--- /dev/null
+++ b/hzh/REDIS/僵尸Key防护.md
@@ -0,0 +1,177 @@
+---
+tags: [redis, memory, ttl, expiration, best-practices]
+create time: 2026-06-01 10:30
+---
+
+# 僵尸 Key 防护
+
+## 概述
+
+**僵尸 Key** 是指不再有新请求但仍长期占用 Redis 内存的 Key。它们通常是因为 `EXPIRE` 设置失败或遗漏导致的——虽然单个 Key 不大,但随着时间积累会消耗越来越多的内存,最终引发 OOM。
+
+对应薄弱点 F13:令牌桶中 `EXPIRE` 的保护机制及其计算方法。
+
+## 一、什么是僵尸 Key?
+
+```mermaid
+flowchart TD
+ Day1["📅 第 1 天
用户 user123 访问了 API"] --> Create["创建 Key
rate:limit:user123:daily"]
+ Create --> EXPIRE["设置 EXPIRE
TTL = 86400s (1天)"]
+
+ Day2["📅 第 2 天"] --> Active{"user123 还访问吗?"}
+ Active -- "是" --> Renew["正常续期 ✓"]
+ Active -- "否" --> Expire["✅ Key 自动过期删除 ✓"]
+
+ Day3["📅 第 3 天"] --> NoExpire{"如果 EXPIRE 没生效?"}
+ NoExpire --> Zombie["❌ Key 仍然占用 ~500B
但已无实际用途"]
+
+ zombieCount["Zombie Count 增长"] --> Accumulate["1000 users × 500B = 500KB
10000 users → 5MB
..."]
+
+ style Expire fill:#c8e6c9
+ style Zombie fill:#ffebee
+ style Accumulate fill:#ffcdd2
+```
+
+### 典型场景
+
+| 场景 | 原因 | 结果 |
+|------|------|------|
+| EXPIRE 在 Lua 脚本外面 | 原子性被破坏,EXPIRE 可能不执行 | Key 永久存在 |
+| Lua 脚本中的 EXPIRE 报错 | 条件判断导致跳过 | Key 不过期 |
+| 代码忘记写 EXPIRE | 开发疏忽 | Key 永不消失 |
+| Redis 内存满导致无法写入 | OOM 后部分操作失败 | 新增 Key 没设置 TTL |
+
+## 二、F13 考点详解:令牌桶的 EXPIRE 保护机制
+
+这是你最薄弱的地方之一。令牌桶的 Lua 脚本中有一行关键的 EXPIRE:
+
+```lua
+-- 令牌桶 Lua 脚本(节选)
+redis.call('HMSET', tokensKey, 'tokens', remaining, 'last_refill', now)
+redis.call('EXPIRE', tokensKey, math.ceil(capacity / rate) * 2)
+return {allowed, math.floor(remaining * 1000 / rate)}
+```
+
+### 2.1 为什么是 `math.ceil(capacity / rate) * 2`?
+
+这行公式的含义是:**填满整个桶所需的时间 × 2**
+
+| 参数 | 含义 | 示例值 |
+|------|------|--------|
+| `capacity` | 桶的最大容量(最大令牌数) | 100 |
+| `rate` | 每秒补充的令牌数 | 10/s |
+| `capacity / rate` | **从空到满需要的时间** | 100 / 10 = 10 秒 |
+| `* 2` | **留一倍余量** | 20 秒 |
+| `math.ceil(...)` | **向上取整**(Redis EXPIRE 需要整数秒) | 20 |
+
+**逻辑推导:**
+
+```
+如果一个用户在 t=0 时创建了限流 Key,之后再也不来了:
+ - 最晚的可能活动时间:t = capacity/rate(桶从空逐渐加满的时间)
+ - 但如果用户在 t=0 就用完了所有令牌,然后不再来...
+ - Key 实际上从最后一次活动就开始倒计时了
+
+所以 EXPIRE 应该从"最后一次有活动的时刻"开始算。
+我们已经通过 HMSET 更新时间戳保证了这一点。
+但为什么要乘 2?因为:
+ 1. 用户可能在某个时间点刚好用完令牌
+ 2. 然后又过了一段时间才再来
+ 3. 这段时间内,桶已经在慢慢补充令牌了
+ 4. ×2 给了足够的缓冲,确保即使是最长的空闲间隔也能覆盖
+```
+
+### 2.2 数值演示
+
+| capacity | rate | capacity/rate | ×2 后的 TTL | 说明 |
+|----------|------|-------------|-----------|------|
+| 100 | 10/s | 10s | 20s | 一般 API 限流 |
+| 1000 | 100/s | 10s | 20s | 高频接口 |
+| 10000 | 100/s | 100s | 200s | 高配额用户 |
+| 100 | 1/s | 100s | 200s | 低频长周期 |
+
+> [!tip]- 这个公式的直觉理解
+>
+> "一个桶要多久才能从空变成满?"— 答:capacity/rate。
+>
+> 在这个时间内肯定能看到用户的活动迹象。如果没有活动,2 倍时间就足以让它安全过期。既不会太短(刚设完就过期),也不会太长(僵尸太久)。
+
+## 三、排查与清理
+
+### 3.1 查找僵尸 Key
+
+```bash
+# 方法1: 检查特定前缀的 Key 是否没有 TTL
+redis-cli --scan --pattern "rate:*" | while read key; do
+ ttl=$(redis-cli TTL "$key")
+ if [ "$ttl" = "-1" ]; then
+ echo "ZOMBIE: $key"
+ fi
+done
+
+# 方法2: Go 程序批量扫描(见 SCAN 文档)
+```
+
+### 3.2 手动清理
+
+```bash
+# DEL 同步删除(小 Key)
+DEL rate:limit:zombie_user_001
+
+# UNLINK 异步删除(大 Key,不阻塞)
+UNLINK rate:sliding:large_zombie_key
+```
+
+### 3.3 预防 Checklist
+
+```yaml
+prevention_checklist:
+ lua_scripts:
+ - "所有涉及数据写入的 Lua 脚本,必须包含 EXPIRE"
+ - "EXPIRE 应该在 Lua 内部(保证原子性),不能放在外面"
+ - "使用合理的 TTL 计算公式: ceil(max_lifetime / rate) × safety_factor"
+
+ code_review:
+ - "新加的 Key 必须有对应的 EXPIRE"
+ - "INCR + EXPIRE 组合必须是原子的(Lua 或管道内的 MULTI/EXEC)"
+ - "禁止用 INCR 后立即 EXPIRE 的两个独立命令(非原子,中间可能被其他操作打断)"
+
+ monitoring:
+ - "监控 DBSIZE 趋势(keys 总数)"
+ - "定期检查未设置 TTL 的 Key 数量"
+ - "告警阈值: 未过期 Key 占比 > 5% 触发告警"
+```
+
+## 四、INCR + EXPIRE 的原子性问题
+
+```go
+// ❌ 错误:非原子操作
+count := rdb.Incr(ctx, key) // Step 1: 自增
+rdb.Expire(ctx, key, windowSeconds) // Step 2: 设置过期
+// 如果 Step 2 失败(网络抖动、超时),Key 就永远不会过期!
+
+// ✅ 正确:Lua 脚本内原子化
+const luaScript = `
+local count = redis.call('INCR', KEYS[1])
+if count == 1 then
+ redis.call('EXPIRE', KEYS[1], ARGV[1])
+end
+return count
+`
+result := rdb.Eval(ctx, luaScript, []string{key}, windowSeconds).Int()
+```
+
+## 五、常见误解
+
+| 误解 | 真相 |
+|------|------|
+| "Redis 会自动删除过期的 Key" | 是的,但前提是设置了 TTL。没设置 TTL 的 Key 永远不会自动删除 |
+| "LRU 淘汰会帮我清理僵尸 Key" | LRU 基于访问频率,僵尸 Key 可能因为曾经活跃过而暂时"免死" |
+| "重启 Redis 就能清空" | 生产环境不能随便重启,且 AOF 持久化会在重启后恢复这些 Key |
+| "TTL 很短没关系,反正很快会过期" | 10000 个 Key × 1 秒 TTL = 瞬时内存峰值,仍可能造成压力 |
+
+## 关联笔记
+
+- [[分布式限流]] — 排错指南中的内存持续增长章节
+- [[内存持续增长排查]] — 系统性的内存问题排查流程
+- [[SCAN命令]] — 用 SCAN 批量扫描僵尸 Key
diff --git a/hzh/REDIS/内存持续增长排查.md b/hzh/REDIS/内存持续增长排查.md
new file mode 100644
index 0000000..45cd10f
--- /dev/null
+++ b/hzh/REDIS/内存持续增长排查.md
@@ -0,0 +1,206 @@
+---
+tags: [redis, memory, troubleshooting, monitoring]
+create time: 2026-06-01 10:30
+---
+
+# Redis 内存持续增长排查
+
+## 概述
+
+生产环境中 Redis 内存持续增长是一个常见的告警场景。它不一定意味着 Bug——但如果不及时处理,最终会导致 OOM 或频繁 Swap。本节提供一套**系统性的排查思路**,对应你的薄弱点 F10(TTL 命令检查 Key 过期状态)。
+
+## 一、排查流程图
+
+```mermaid
+flowchart TD
+ Start["⚠️ 内存持续增长告警"] --> Step1["Step 1: MEMORY USED 确认增长幅度"]
+
+ Step1 --> Q1{"增长是持续的还是突发的?"}
+ Q1 -- "持续增长" --> SLOW["缓慢增长路径"]
+ Q1 -- "突然飙升" --> SPIKE["突发峰值路径"]
+
+ subgraph SLOW["慢增长排查"]
+ S1["检查 Key 总数变化
DBSIZE / SCAN"] --> S2["找出增长最快的 Key 前缀
SCAN + TTL 检查"] --> S3["确认 EXPIRE 是否生效
TTL key → -1 表示未过期"] --> S4["僵尸 Key 清理"]
+ end
+
+ subgraph SPIKE["爆发增长排查"]
+ P1["监控 BigKey 列表
MEMORY USAGE + SORTED BY size"] --> P2["检查是否有大量写入并发
Slowlog / Monitoring"] --> P3["排查是否为临时批处理任务
定时任务/数据同步"]
+ end
+
+ S4 --> Action{"找到原因了?"}
+ P3 --> Action
+
+ Action -- "是" --> Fix["修复"]
+ Action -- "否" --> Dump["MEMORY ANALYZER
深入分析内存分布"]
+
+ style Start fill:#fff3e0
+ style Fix fill:#c8e6c9
+ style Dump fill:#e3f2fd
+```
+
+## 二、逐步排查方法
+
+### Step 1:定位增长来源
+
+```bash
+# 查看所有数据库的大小
+INFO keyspace
+→ db0:keys=12345,expires=12000,avg_ttl=3600000
+```
+
+输出解析:
+| 字段 | 含义 |
+|------|------|
+| `keys` | 该数据库中 Key 的总数 |
+| `expires` | 设置了过期时间的 Key 数量 |
+| `avg_ttl` | 这些 Key 的平均剩余生存时间(毫秒) |
+
+如果 `keys` 在持续增长而 `expires` 不变 → 有大量未设置过期的 Key
+
+### Step 2:查找大 Key
+
+```bash
+# 方式1: redis-cli --bigkeys(推荐快速使用)
+redis-cli --bigkeys
+
+# 扫描中...
+# --- STRINGS ---
+# biggest rate:sliding:user123 (6 MB)
+# count 15 (total 15 keys)
+# ...
+
+# 方式2: 手动用 SCAN + MEMORY USAGE
+SCAN 0 MATCH rate:* TYPE string | xargs -I{} redis-cli MEMORY USAGE {}
+```
+
+> [!tip]- bigkeys 工具的坑
+>
+> `redis-cli --bigkeys` 会遍历所有 Key,对大库来说仍然可能阻塞 Redis。**生产环境慎用**,建议在从库或低峰期运行。
+
+### Step 3:检查过期状态(F10 考点 🔑)
+
+这是排查内存问题的核心技能——**判断 Key 是否已经过期**。
+
+```bash
+# 检查单个 Key 的 TTL
+TTL rate:limit:user123
+→ 86400 ← 剩余 86400 秒,正常
+
+TTL rate:limit:user456
+→ -1 ← ❌ 没有过期时间!这就是僵尸 Key
+
+TTL rate:limit:user789
+→ -2 ← ❌ Key 已不存在,已被删除
+```
+
+| TTL 返回值 | 含义 | 处理方式 |
+|-----------|------|---------|
+| `> 0` | Key 存在,还有 N 秒过期 | 正常,等待自动清理 |
+| `-1` | **Key 存在但没有设置过期时间** | ❌ 检查代码中是否漏了 EXPIRE,手动 DEL |
+| `-2` | Key 已不存在 | 正常,无需操作 |
+
+### Step 4:批量检查僵尸 Key
+
+```go
+func findZombieKeys(ctx context.Context, rdb *redis.Client, pattern string) []string {
+ var zombies []string
+ cursor := uint64(0)
+
+ for {
+ keys, nextCursor, _ := rdb.Scan(ctx, cursor, pattern, 200).Result()
+ for _, key := range keys {
+ ttl, err := rdb.TTL(ctx, key).Result()
+ if err == nil && ttl == -1 {
+ zombies = append(zombies, key)
+ }
+ }
+ if nextCursor == 0 {
+ break
+ }
+ cursor = nextCursor
+ }
+ return zombies
+}
+```
+
+## 三、常见内存增长原因及对策
+
+| 原因 | 表现 | 解决方式 |
+|------|------|---------|
+| **EXPIRE 未生效** | TTL 返回 -1 | 检查 Lua 脚本或 Go 代码中的 EXPIRE 调用 |
+| **Sorted Set 规模过大** | 个别 Key 占几 MB~几十 MB | 切换到令牌桶方案(O(1) 空间) |
+| **批量写入无上限** | DBSIZE 快速增长 | 添加写入限流和 Key 数量上限 |
+| **大 Hash/List 结构** | 单 Key 占用巨大 | 拆分或使用更合适的数据结构 |
+| **副本同步延迟** | Master 已过期,Slave 还没删 | 配置 `replica-ignore-maxmemory` |
+| **Redis 缓存淘汰策略不当** | maxmemory-policy 设为 noeviction | 改为 `allkeys-lru` 或 `volatile-ttl` |
+
+## 四、预防性措施
+
+```mermaid
+flowchart TD
+ Monitor["📊 持续监控"] --> A["MEMORY USAGE per Key 告警"]
+ Monitor --> B["DBSIZE 增长速率告警"]
+ Monitor --> C["Evicted Keys 计数告警"]
+
+ Policy["⚙️ 合理的淘汰策略"] --> D["maxmemory-policy: allkeys-lru"]
+ Policy --> E["maxmemory: 设为可用内存的 70%"]
+
+ Design["🔧 设计时考虑"] --> F["所有 Key 必须有 EXPIRE"]
+ Design --> G["高频场景用 O(1) 算法(令牌桶)"]
+ Design --> H["定期 SCAN 清理残留数据"]
+
+ style Monitor fill:#e3f2fd
+ style Policy fill:#fff3e0
+ style Design fill:#e8f5e9
+```
+
+### 推荐配置
+
+```conf
+# redis.conf
+
+# 最大内存限制(建议设置为服务器总内存的 70%,留余量给 Swap)
+maxmemory 2gb
+
+# 淘汰策略:LRU(最近最少使用)
+maxmemory-policy allkeys-lru
+
+# 或者:优先删除有过期时间的 Key 中最早过期的
+# maxmemory-policy volatile-ttl
+
+# RDB/AOF 混合持久化
+rdb-save-incremental-mem YES
+aof-use-rdb-preamble YES
+```
+
+## 五、紧急止血步骤
+
+```
+当内存已经接近上限时的应急操作:
+
+1. 停止所有非必要的写入(暂停定时任务、数据同步)
+2. 找到并删除最大的几个 Key:
+ redis-cli --bigkeys
+ DEL ...
+3. 如果无法确定哪些 Key 该删,先批量减少 Key 总量:
+ SCAN 0 MATCH rate:* COUNT 1000 → DEL 随机抽取的部分 Key(谨慎评估影响)
+4. 临时提升 maxmemory(如果服务器还有物理内存可用)
+5. 事后复盘:为什么没及时发现?监控阈值要不要调低?
+```
+
+> [!warning]- DEL vs UNLINK
+>
+> 删除大 Key 时用 `UNLINK`(异步删除)替代 `DEL`(同步删除),避免阻塞主线程:
+> ```bash
+> # 同步删除(阻塞)
+> DEL rate:sliding:large_key
+>
+> # 异步删除(不阻塞)
+> UNLINK rate:sliding:large_key
+> ```
+
+## 关联笔记
+
+- [[分布式限流]] — 排错指南中的内存问题章节
+- [[僵尸Key防护]] — 针对僵尸 Key 的具体防护措施
+- [[SCAN命令]] — SCAN 是排查内存问题的必备工具
diff --git a/hzh/REDIS/分布式限流.md b/hzh/REDIS/分布式限流.md
index 29ba670..f985052 100644
--- a/hzh/REDIS/分布式限流.md
+++ b/hzh/REDIS/分布式限流.md
@@ -450,6 +450,24 @@ flowchart LR
## 关联笔记
+### 进阶阅读(按知识点拆分)
+
+- [[路由键与Hash Tag]] — Redis Cluster 中 Hash Tag 的完整用法
+- [[滑动窗口计数器]] — 贡献度计算详解与加权公式
+- [[Sorted Set 滑动窗口]] — Sorted Set 方案的时空复杂度分析
+- [[Go-Redis Lua 调用指南]] — Go-Redis 封装 + EVALSHA 预加载流程
+- [[限流HTTP响应头]] — 标准限流响应头规范
+- [[EVALSHA预加载]] — EVALSHA 性能优化与监控
+- [[SCAN命令]] — SCAN vs KEYS 对比与使用示例
+- [[Jitter抖动与重试策略]] — 指数退避 + Jitter 的 Go 实现
+- [[多级限流架构]] — L1/L2/L3 三层架构实战
+- [[滑动窗口日志方案]] — Lua 脚本中的清理逻辑与时序
+- [[内存持续增长排查]] — 系统性的内存问题排查思路
+- [[僵尸Key防护]] — EXPIRE 保护机制与 TTL 公式推导
+- [[Pipeline批量操作]] — Pipeline vs MULTI/EXEC 区别与应用
+
+### 基础参考
+
- [[Lua脚本]] — Redis Lua 脚本基础概念与使用模式
- [[02-服务治理/06-容错模式]] — 限流是容错体系的第三层防线
- [[02-服务治理/01-API网关]] — API 网关层的限流插件集成
diff --git a/hzh/REDIS/多级限流架构.md b/hzh/REDIS/多级限流架构.md
new file mode 100644
index 0000000..bacc0a6
--- /dev/null
+++ b/hzh/REDIS/多级限流架构.md
@@ -0,0 +1,246 @@
+---
+tags: [redis, rate-limiting, multi-layer, architecture]
+create time: 2026-06-01 10:30
+---
+
+# 多级限流架构实战
+
+## 概述
+
+单个限流算法通常不够用。生产环境中的限流像洋葱一样**层层叠加**——每层保护不同的目标,使用不同的算法和维度。这种设计既防御了大规模攻击,又为正常用户提供了良好的配额管理。
+
+对应薄弱点 F5:多级限流架构各层的设计选择。
+
+## 一、为什么需要多级?
+
+```mermaid
+flowchart LR
+ subgraph "单一维度的局限"
+ L1["仅 IP 限流"] --> PROBLEM1["问题:无法区分同一 IP 下的不同用户"]
+ L2["仅用户限流"] --> PROBLEM2["问题:恶意攻击者可注册大量账号绕过"]
+ L3["仅接口限流"] --> PROBLEM3["问题:不防刷单/暴力扫描"]
+ end
+
+ PROBLEM1 --> MULTI["→ 需要多级组合"]
+ PROBLEM2 --> MULTI
+ PROBLEM3 --> MULTI
+
+ style PROBLEM1 fill:#ffebee
+ style PROBLEM2 fill:#ffebee
+ style PROBLEM3 fill:#ffebee
+ style MULTI fill:#e8f5e9
+```
+
+单层限流的盲区:
+- 只看 IP → 一个用户的多个账号共享配额(或反之,一个 IP 下多个用户被误伤)
+- 只看用户 → 恶意用户可以创建无数个账号突破限制
+- 只看接口 → 对暴力破解、爬虫扫描无能为力
+
+## 二、经典三级限流架构
+
+```mermaid
+flowchart TD
+ Client["👤 客户端请求"] --> GW["🌐 API Gateway"]
+ GW --> L1
+
+ subgraph L1Layer["🔴 L1: IP 维度 - 第一道防线"]
+ direction LR
+ L1Action["防暴力扫描
防恶意攻击"]
+ L1Algorithm["短窗口固定计数
例如: 100 req/s/IP"]
+ end
+
+ L1 -- pass --> L2
+
+ subgraph L2Layer["🟡 L2: 用户维度 - 配额管理"]
+ direction LR
+ L2Action["控制每日/每月配额
区分免费/付费等级"]
+ L2Algorithm["天级滑动窗口日志
例如: 1000 req/day"]
+ end
+
+ L2 -- pass --> L3
+
+ subgraph L3Layer["🟢 L3: 接口维度 - 保护后端"]
+ direction LR
+ L3Action["保护实例不被打满
QPS/内存保护"]
+ L3Algorithm["令牌桶/漏桶
例如: 5000 req/s/endpoint"]
+ end
+
+ L3 -- pass --> Biz["💼 执行业务逻辑"]
+
+ L1 -- fail --> R429["HTTP 429 Too Many Requests"]
+ L2 -- fail --> R429
+ L3 -- fail --> R429
+
+ style L1Layer fill:#ffebee,color:#000
+ style L2Layer fill:#fff3e0,color:#000
+ style L3Layer fill:#e8f5e9,color:#000
+ style R429 fill:#b71c1c,color:#fff
+```
+
+### 层级详解
+
+| 层级 | 限流维度 | 核心目标 | 推荐算法 | 典型参数 | Key 示例 |
+|------|---------|---------|---------|---------|---------|
+| **L1** | IP / CIDR | 防刷、防攻击、防爬虫 | 短窗口固定计数 | 100 req/s/IP | `rate:ip:{IP}:short` |
+| **L2** | 用户 ID / Account | 配额管理(免费/付费) | 天级滑动窗口日志 | 1000 req/day | `rate:user:{UID}:daily` |
+| **L3** | Endpoint / Interface | 保护下游资源 | 令牌桶 / 漏桶 | 5000 req/s/path | `rate:api:{path}` |
+
+### L1:IP 层 - 最短视的窗口
+
+```
+时间窗口:1 ~ 5 秒
+算法:固定窗口计数器
+目的:快速拦截自动化攻击
+
+为什么用短窗口?
+- 恶意流量的特点是短时间内密集发起
+- 短窗口可以及时响应,不留攻击窗口
+- 不会因为窗口太长导致误伤正常用户的突发流量
+```
+
+```go
+// L1: IP 限流 - 短窗口固定计数
+const shortWindowLua = `
+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
+return count
+`
+
+func IPLimitAllow(ctx context.Context, rdb *redis.Client, ip string) (bool, error) {
+ // 5 秒窗口,最多 100 次请求
+ const windowSec = 5
+ const maxReq = 100
+
+ key := fmt.Sprintf("rate:ip:{%s}", ip)
+ result, err := rdb.Eval(ctx, shortWindowLua, []string{key}, maxReq, windowSec).Int()
+ if err != nil {
+ return false, err
+ }
+ return result <= maxReq, nil
+}
+```
+
+### L2:用户层 - 精确的日配额
+
+```
+时间窗口:24 小时(按天重置)
+算法:Sorted Set 滑动窗口(逐条精确统计)
+目的:区分免费用户和付费用户的配额
+
+为什么用 Sorted Set?
+- 日配额 QPS 不高(1000/day ≈ 0.01 QPS)
+- 需要精确统计,不能有近似误差
+- 免费/付费分级,每个用户独立配额
+```
+
+### L3:接口层 - 保护后端资源
+
+```
+时间窗口:持续监控(令牌桶模型)
+算法:令牌桶
+目的:防止某个接口被打爆,保护数据库/下游服务
+
+为什么用令牌桶?
+- O(1) 空间,不随时间增长
+- 允许合理突发(用户批量操作)
+- 输出速率可控,保护后端稳定
+```
+
+## 三、多级限流的调用链路
+
+```mermaid
+sequenceDiagram
+ participant C as 客户端
+ participant G as Go Service
+ participant R as Redis
+
+ C->>G: HTTP Request
+ G->>R: L1: IP Check (100/s/5s)
+ alt Blocked by L1
+ R-->>G: count > 100
+ G-->>C: 429 (L1 blocked)
+ else
+ G->>R: L2: User Check (1000/day)
+ alt Blocked by L2
+ R-->>G: count >= 1000
+ G-->>C: 429 (L2 blocked)
+ else
+ G->>R: L3: Token Bucket (5000/s)
+ alt Blocked by L3
+ R-->>G: no tokens
+ G-->>C: 429 (L3 blocked)
+ else
+ R-->>G: allowed
+ G->>G: Execute Business Logic
+ G-->>C: 200 OK
+ end
+ end
+ end
+```
+
+> [!note]- 性能考量
+>
+> 多级限流意味着每次请求要发 **3 次 Redis 调用**。优化方向:
+> - **本地缓存 + 远程校验**:L1 可以在本地做短期计数(如 sync.Map),定期同步到 Redis
+> - **Pipeline 合并**:L1/L2/L3 的三个 Lua 调用可以用 Pipeline 打包
+> - **短路策略**:如果 L1 就拒绝了,不需要继续查 L2/L3
+
+## 四、降级与熔断联动
+
+```mermaid
+flowchart TD
+ Normal["✅ 正常限流状态"] --> Check{"Redis 可用?"}
+
+ Check -- "是" --> RL["正常走多级限流"]
+ Check -- "否" --> Degradation{"降级策略"}
+
+ Degradation -- "短暂断开" --> Local["本地内存限流
降级模式"]
+ Degradation -- "长时间不可用" --> PermitAll["放行所有请求
宁可超限不可停机"]
+
+ Local --> Recovery{"Redis 恢复?"}
+ Recovery -- "是" --> Normal
+ Recovery -- "否" --> PermitAll
+
+ style Normal fill:#c8e6c9
+ style PermitAll fill:#fff3e0
+ style Local fill:#e3f2fd
+```
+
+| Redis 状态 | L1 IP 层 | L2 用户层 | L3 接口层 |
+|-----------|---------|----------|----------|
+| **正常** | Redis Lua | Redis Lua | Redis Lua |
+| **短暂故障** | 本地 sync.Map 缓存计数 | 跳过(默认放行) | 本地令牌桶 |
+| **长时间故障** | 全部放行 | 全部放行 | 全部放行 |
+
+> [!warning]- 降级不是无限制的
+>
+> 如果 L1(IP 层)也降级放行了,意味着任何 IP 都没有限制——这时应该配合 CDN/WAF 等外部防御层做兜底。
+
+## 五、各层的超时与重试配置
+
+```
+多层限流系统的超时设置:
+
+┌─────────┬──────────────┬──────────────────────────┐
+│ 层级 │ Redis 超时 │ 客户端超时 │
+├─────────┼──────────────┼──────────────────────────┤
+│ L1 │ 1ms │ 50ms(快速判断) │
+│ L2 │ 5ms │ 200ms(配额查询) │
+│ L3 │ 3ms │ 100ms(令牌检查) │
+│ 合计 │ 9ms │ — │
+└─────────┴──────────────┴──────────────────────────┘
+```
+
+各层超时应该远小于整体业务超时,否则限流模块本身会成为瓶颈。
+
+## 关联笔记
+
+- [[分布式限流]] — 限流的基础算法介绍
+- [[Jitter抖动与重试策略]] — 各层的客户端重试策略设计
+- [[02-服务治理/06-容错模式]] — 限流在容错体系中的位置
diff --git a/hzh/REDIS/滑动窗口日志方案.md b/hzh/REDIS/滑动窗口日志方案.md
new file mode 100644
index 0000000..d2aa9b4
--- /dev/null
+++ b/hzh/REDIS/滑动窗口日志方案.md
@@ -0,0 +1,198 @@
+---
+tags: [redis, sliding-window, sorted-set, performance, monitoring]
+create time: 2026-06-01 10:30
+---
+
+# 滑动窗口日志方案细节
+
+## 概述
+
+滑动窗口日志方案是最精确的限流方式:每条请求作为 Sorted Set 的一条记录,通过范围查询实现精确统计。这一节深入你标记为薄弱的 F6——Lua 脚本中的清理逻辑和各操作的详细时序。
+
+## 一、核心流程(F6 考点深挖)
+
+### 完整时序图
+
+```mermaid
+sequenceDiagram
+ participant App as Go 应用
+ participant R as Redis
+
+ Note over App,R: ── Lua 脚本执行(原子化)─
+
+ App->>R: EVALSHA sha1 ...