【Redis】Scan 命令使用教程:高效遍历海量数据
Redis 中 Scan 命令使用教程:高效遍历海量数据
在 Redis 中,当需要遍历所有键或指定模式的键时,传统的 KEYS
命令会因阻塞主线程、无法分页等问题,在海量数据场景下表现糟糕。而 Scan
命令凭借 “非阻塞”“分批遍历” 的特性,成为解决大规模数据遍历的最优方案。本文将从基础介绍、实现原理、实操方式到实践总结,全面讲解 Scan
命令的使用。
一、介绍
Scan
是 Redis 2.8 版本引入的迭代式遍历命令,主要用于遍历 Redis 中的键集合或集合类型(如 Hash、Set、Sorted Set)的元素,核心目标是解决传统遍历命令的性能问题。
1. 为什么需要 Scan?
传统的 KEYS
命令存在明显缺陷:
-
阻塞主线程:
KEYS
会一次性遍历所有符合条件的键,若数据量达百万级,会占用大量 CPU 时间,导致 Redis 无法响应其他请求; -
无分页能力:
KEYS
只能一次性返回所有结果,无法分批处理,容易造成客户端内存溢出; -
不支持复杂筛选:仅能通过简单的通配符(如
*
、?
)匹配,灵活性低。
而 Scan
命令恰好弥补这些不足,具备以下核心特性:
-
非阻塞:分批遍历数据,每次只处理少量元素,避免长时间占用主线程;
-
游标迭代:通过 “游标(cursor)” 记录遍历位置,支持断点续传;
-
安全遍历:遍历过程中数据的新增、删除、修改不会导致漏遍历或重复遍历(存在极小概率重复,但可通过业务层去重解决);
-
多类型支持:除了遍历所有键(
SCAN
),还支持遍历 Hash 字段(HSCAN
)、Set 元素(SSCAN
)、Sorted Set 元素(ZSCAN
)。
2. 典型应用场景
-
海量键统计:如统计 Redis 中所有以
user:
为前缀的键数量; -
数据清理:分批删除过期或无用的键(如删除所有
temp:
前缀的临时键); -
集合元素遍历:遍历大型 Hash 中的所有字段值,避免一次性加载导致内存溢出;
-
定期数据校验:分批检查键的过期时间或数据完整性。
二、使用原理
Scan
命令的核心是基于 “游标” 和 “哈希表遍历” 实现的,理解其底层原理能帮助更好地使用命令。
1. Redis 键空间的存储结构
Redis 的键空间(keyspace)底层基于哈希表存储,每个键通过哈希函数映射到哈希表的某个 “桶(bucket)” 中。Scan
命令本质是遍历哈希表的桶,并通过游标记录当前遍历到的桶位置。
2. 游标迭代机制
Scan
的遍历过程类似 “翻书”,游标就是 “页码”,具体流程如下:
-
初始游标:首次调用
Scan
时,游标值设为0
,表示从哈希表的起始位置开始遍历; -
分批遍历:Redis 会根据游标位置,返回当前批次的元素(默认 10 个),并返回新的游标值;
-
结束条件:当返回的游标值为
0
时,表示已遍历完所有元素; -
断点续传:若遍历中断(如客户端重启),下次可使用上次返回的非 0 游标继续遍历,无需从头开始。
3. 避免漏遍历与重复遍历的设计
由于 Redis 哈希表在扩容(rehash)时会重新分配桶的位置,Scan
通过以下机制保证遍历的准确性:
-
渐进式 rehash 兼容:遍历过程中若触发哈希表扩容,
Scan
会同时遍历旧哈希表和新哈希表,确保所有键都能被访问到; -
允许重复遍历:为了简化实现,
Scan
不保证元素只出现一次(尤其是在 rehash 过程中),但重复概率极低,业务层可通过去重(如用 Set 暂存结果)解决。
4. 计数参数(count)的作用
Scan
命令中的 count
参数用于指定 “每次遍历的桶数量”,而非 “返回的元素数量”:
-
默认
count=10
,表示每次遍历 10 个桶,返回这些桶中的所有符合条件的元素; -
count
并非严格限制,Redis 会根据桶中元素数量动态调整返回结果(如某个桶中没有符合条件的键,可能返回少于count
个元素); -
海量数据场景下,可适当增大
count
(如count=1000
),减少遍历次数,提升效率。
三、使用方式
Scan
命令家族包括 SCAN
(遍历键空间)、HSCAN
(遍历 Hash)、SSCAN
(遍历 Set)、ZSCAN
(遍历 Sorted Set),核心用法类似,以下以最常用的 SCAN
为例讲解,其他命令用法可类比。
1. 基础语法
(1)SCAN 命令(遍历所有键)
语法:SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]cursor:游标值(首次为 0,后续用上次返回的游标)MATCH pattern:通配符匹配,筛选符合条件的键(可选)COUNT count:每次遍历的桶数量(可选,默认 10)TYPE type:按键类型筛选(如 string、hash、set,可选,Redis 6.0+ 支持)
(2)其他命令语法(类比)
-
HSCAN key cursor [MATCH pattern] [COUNT count]
:遍历 Hash 键key
的字段和值; -
SSCAN key cursor [MATCH pattern] [COUNT count]
:遍历 Set 键key
的元素; -
ZSCAN key cursor [MATCH pattern] [COUNT count]
:遍历 Sorted Set 键key
的元素和分数。
2. 实操示例
(1)遍历所有键(无筛选)
# 首次遍历:游标 0,默认 count=10127.0.0.1:6379> SCAN 01) "17" # 下次遍历的游标2) 1) "user:1001"2) "product:2003"3) "order:5001"# 本次返回 3 个键(少于 count=10,因部分桶无符合条件的键)# 第二次遍历:使用上次返回的游标 17127.0.0.1:6379> SCAN 171) "0" # 游标为 0,遍历结束2) 1) "user:1002"2) "temp:3001"
(2)按前缀筛选键(MATCH)
# 遍历所有以 "user:" 为前缀的键,count=20127.0.0.1:6379> SCAN 0 MATCH user:* COUNT 201) "23"2) 1) "user:1001"2) "user:1002"3) "user:1003"# 继续遍历,直到游标返回 0127.0.0.1:6379> SCAN 23 MATCH user:* COUNT 201) "0"2) 1) "user:1004"
(3)按类型筛选键(TYPE,Redis 6.0+)
# 遍历所有 string 类型的键127.0.0.1:6379> SCAN 0 TYPE string1) "12"2) 1) "user:1001" # 假设该键是 string 类型2) "temp:3001"
(4)遍历 Hash 键(HSCAN)
# 先创建一个 Hash 键127.0.0.1:6379> HMSET user:1001 name "Alice" age "25" city "Beijing"OK# 遍历该 Hash 的字段和值127.0.0.1:6379> HSCAN user:1001 01) "0" # 游标为 0,Hash 元素少,一次遍历完2) 1) "name"2) "Alice"3) "age"4) "25"5) "city"6) "Beijing"
3. 代码示例(Golang)
以遍历所有 user:
前缀的键为例,使用 go-redis
客户端实现:
package mainimport ("context""fmt""github.com/go-redis/redis/v8"
)func main() {// 初始化 Redis 客户端client := redis.NewClient(&redis.Options{Addr: "localhost:6379",Password: "",DB: 0,})defer client.Close()ctx := context.Background()// Scan 遍历所有 user: 前缀的键var cursor uint64 = 0 // 初始游标count := int64(20) // 每次遍历的桶数量pattern := "user:*" // 匹配模式fmt.Println("开始遍历 user: 前缀的键:")for {// 执行 Scan 命令result, err := client.Scan(ctx, cursor, pattern, count).Result()if err != nil {fmt.Printf("Scan 执行失败:%v\n", err)return}// 获取本次结果和下次游标cursor = result.Cursorkeys := result.Keys// 处理本次获取的键for _, key := range keys {fmt.Printf("找到键:%s\n", key)}// 游标为 0,遍历结束if cursor == 0 {break}}fmt.Println("遍历完成")
}
4. 注意事项
-
游标必须正确传递:每次遍历需使用上次返回的游标,否则会导致重复遍历或漏遍历;
-
避免过度依赖 MATCH:
MATCH
是在遍历结果中筛选,而非提前过滤,若符合条件的键极少,会导致多次空遍历,建议结合业务场景优化匹配模式; -
count 参数按需调整:数据量小时用默认
count=10
即可,海量数据时可增大count
(如 1000~10000),但不宜过大(避免单次操作耗时过长); -
业务层去重:因
Scan
可能返回重复元素,需在业务层通过 Set 或哈希表去重; -
不建议在主库高频使用:虽然
Scan
非阻塞,但频繁遍历仍会占用 CPU 资源,建议在从库执行(需确保主从数据同步及时)。
四、总结
Scan
命令是 Redis 中处理海量数据遍历的核心工具,其核心优势可总结为:
-
非阻塞设计:分批遍历避免阻塞主线程,保障 Redis 服务可用性;
-
游标迭代:支持断点续传,适合长时间、大规模的遍历任务;
-
灵活筛选:通过
MATCH
和TYPE
实现精准筛选,满足多样化需求; -
多类型支持:覆盖键空间和所有集合类型,适用场景广泛。
在实际使用中,需注意以下关键要点:
-
正确传递游标,确保遍历的连续性;
-
根据数据量调整
count
参数,平衡遍历效率和资源占用; -
结合业务场景优化筛选逻辑,减少无效遍历;
-
对遍历结果进行去重,避免重复处理。
总之,Scan
命令彻底解决了传统 KEYS
命令的性能痛点,是 Redis 运维和开发中处理海量数据的 “必备工具”,掌握其用法能显著提升大规模 Redis 集群的管理效率。