【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 的遍历过程类似 "翻书",游标就是 "页码",具体流程如下:

  1. 初始游标 :首次调用 Scan 时,游标值设为 0,表示从哈希表的起始位置开始遍历;

  2. 分批遍历:Redis 会根据游标位置,返回当前批次的元素(默认 10 个),并返回新的游标值;

  3. 结束条件 :当返回的游标值为 0 时,表示已遍历完所有元素;

  4. 断点续传:若遍历中断(如客户端重启),下次可使用上次返回的非 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=10

127.0.0.1:6379> SCAN 0

1) "17"  # 下次遍历的游标

2) 1) "user:1001"

    2) "product:2003"

    3) "order:5001"

 # 本次返回 3 个键(少于 count=10,因部分桶无符合条件的键)

# 第二次遍历:使用上次返回的游标 17

127.0.0.1:6379> SCAN 17

1) "0"  # 游标为 0,遍历结束

2) 1) "user:1002"

   2) "temp:3001"
(2)按前缀筛选键(MATCH)
复制代码
# 遍历所有以 "user:" 为前缀的键,count=20

127.0.0.1:6379> SCAN 0 MATCH user:* COUNT 20

1) "23"

2) 1) "user:1001"

    2) "user:1002"

    3) "user:1003"

# 继续遍历,直到游标返回 0

127.0.0.1:6379> SCAN 23 MATCH user:* COUNT 20

1) "0"

2) 1) "user:1004"
(3)按类型筛选键(TYPE,Redis 6.0+)
复制代码
# 遍历所有 string 类型的键

127.0.0.1:6379> SCAN 0 TYPE string

1) "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 0

1) "0"  # 游标为 0,Hash 元素少,一次遍历完

2) 1) "name"

   2) "Alice"

  3) "age"

  4) "25"

  5) "city"

  6) "Beijing"

3. 代码示例(Golang)

以遍历所有 user: 前缀的键为例,使用 go-redis 客户端实现:

c 复制代码
package main

import (
	"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.Cursor
		keys := result.Keys

		// 处理本次获取的键
		for _, key := range keys {
			fmt.Printf("找到键:%s\n", key)
		}

		// 游标为 0,遍历结束
		if cursor == 0 {
			break
		}
	}

	fmt.Println("遍历完成")
}

4. 注意事项

  • 游标必须正确传递:每次遍历需使用上次返回的游标,否则会导致重复遍历或漏遍历;

  • 避免过度依赖 MATCHMATCH 是在遍历结果中筛选,而非提前过滤,若符合条件的键极少,会导致多次空遍历,建议结合业务场景优化匹配模式;

  • count 参数按需调整 :数据量小时用默认 count=10 即可,海量数据时可增大 count(如 1000~10000),但不宜过大(避免单次操作耗时过长);

  • 业务层去重 :因 Scan 可能返回重复元素,需在业务层通过 Set 或哈希表去重;

  • 不建议在主库高频使用 :虽然 Scan 非阻塞,但频繁遍历仍会占用 CPU 资源,建议在从库执行(需确保主从数据同步及时)。

四、总结

Scan 命令是 Redis 中处理海量数据遍历的核心工具,其核心优势可总结为:

  1. 非阻塞设计:分批遍历避免阻塞主线程,保障 Redis 服务可用性;

  2. 游标迭代:支持断点续传,适合长时间、大规模的遍历任务;

  3. 灵活筛选 :通过 MATCHTYPE 实现精准筛选,满足多样化需求;

  4. 多类型支持:覆盖键空间和所有集合类型,适用场景广泛。

在实际使用中,需注意以下关键要点:

  • 正确传递游标,确保遍历的连续性;

  • 根据数据量调整 count 参数,平衡遍历效率和资源占用;

  • 结合业务场景优化筛选逻辑,减少无效遍历;

  • 对遍历结果进行去重,避免重复处理。

总之,Scan 命令彻底解决了传统 KEYS 命令的性能痛点,是 Redis 运维和开发中处理海量数据的 "必备工具",掌握其用法能显著提升大规模 Redis 集群的管理效率。

相关推荐
我的offer在哪里18 小时前
Redis
数据库·redis·缓存
点灯小铭18 小时前
基于单片机的多模式自动洗衣机设计与实现
数据库·单片机·嵌入式硬件·毕业设计·课程设计
潜心编码18 小时前
基于python的仓库管理系统
数据库
herinspace18 小时前
如何设置电脑分辨率和显示缩放
服务器·数据库·智能手机·电脑
biubiubiu070618 小时前
Ubuntu中定时任务测试
数据库·postgresql
程序新视界19 小时前
在MySQL中,一条SQL语句的执行全流程是怎样的?
数据库·后端·mysql
todoitbo20 小时前
我用 TRAE 做了一个不一样的 MySQL MCP
数据库·mysql·adb·ai工具·mcp·trae·mysql-mcp
CodeJourney.20 小时前
Python开发可视化音乐播放器教程(附代码)
数据库·人工智能·python
呆呆小金人20 小时前
SQL入门:正则表达式-高效文本匹配全攻略
大数据·数据库·数据仓库·sql·数据库开发·etl·etl工程师