--- 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