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