Redis SCAN 命令详解:作用、用法与最佳实践

摘要

  • 在使用 Redis 时,经常需要遍历数据库中的键,例如查找特定前缀的 key、统计 key 数量、批量删除 key 等。
  • Redis 提供了两种思路:使用 KEYS pattern 或使用游标式遍历命令 SCAN
  • KEYS pattern 是一种非常不安全的方式,因为它会触发 Redis 服务器的阻塞,从而导致性能下降。设置一些注重安全的环境会禁用KEYS pattern等危险命令。
  • 所以Redis官方强烈推荐 SCAN 方式,其是最安全、最可控的遍历方法。
  • 本文基于redis-7.4.7
  • Redis官网:https://redis.io/

SCAN 的核心作用

  • SCAN 的主要功能是基于游标的、非阻塞的、渐进式遍历 Redis 数据库中的 key

  • 它允许应用在不阻塞服务器的情况下,每次拉取少量 key,从而安全地在生产环境执行 key 遍历操作。

  • SCAN 的设计目标包括:

    • 避免阻塞 Redis 主线程
    • 分批、渐进扫描大规模 key 集合
    • 灵活配合模式匹配(MATCH)
    • 控制每次返回 key 的数量(COUNT)
    • 在不影响线上业务的情况下处理数百万甚至数千万级别键值扫描
  • SCAN 命令是完整游标遍历族的一部分,还包括:

    • HSCAN:遍历哈希表 field/value
    • SSCAN:遍历 set 元素
    • ZSCAN:遍历有序集合
  • 为了完成后面的示例演示,这里准备一个测试数据批量生产脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/bin/bash

# 脚本名称:import_unified_data.sh
# 运行方法:sh $0 <type> [count]
# <type>: string, hash, list, set, 或 zset
# [count]: 导入的数据量 (可选,默认为 100)
# ----------------------------------------

# --- 1. 参数校验与配置 ---

SUPPORTED_TYPES="string hash list set zset"
# 定义每个集合/列表/有序集合要添加的成员数量
MEMBERS_PER_KEY=5

# 检查是否提供了类型参数
if [ $# -lt 1 ]; then
echo "❌ 错误:请指定导入类型。"
echo "支持的类型: ${SUPPORTED_TYPES}"
echo "用法: sh $0 <type> [count]"
exit 1
fi

# 获取类型参数 (第一个参数)
DATA_TYPE=$(echo "$1" | tr '[:upper:]' '[:lower:]')

# 获取数据量参数 (第二个参数),不存在则默认为 1000
if [ $# -ge 2 ]; then
DATA_COUNT=$2
else
DATA_COUNT=100
fi

# 检查类型是否有效
if [[ ! " ${SUPPORTED_TYPES} " =~ " ${DATA_TYPE} " ]]; then
echo "❌ 错误:不支持的类型 '$DATA_TYPE'。"
echo "支持的类型: ${SUPPORTED_TYPES}"
exit 1
fi

# Redis 连接配置
REDIS_HOST="127.0.0.1"
REDIS_PORT="6379"
REDIS_PASSWORD="password" # 替换为你的实际密码或留空
KEY_PREFIX="test_data"

# ----------------------------------------
# --- 2. 命令生成主逻辑 ---
# ----------------------------------------

echo "⏳ 正在生成 ${DATA_COUNT} 条 Redis [${DATA_TYPE}] 命令..."

# 构建 RESP 命令流
i=1
while [ $i -le $DATA_COUNT ]
do
case "$DATA_TYPE" in
# --- String 类型导入 (SET key value) ---
string)
KEY="${KEY_PREFIX}:string:${i}"
VALUE="value_of_${i}_$(date +%s%N)"

# SET 命令 RESP 协议: *3 (SET, key, value)
printf "*3\r\n\$3\r\nSET\r\n\$${#KEY}\r\n${KEY}\r\n\$${#VALUE}\r\n${VALUE}\r\n"
;;

# --- Hash 类型导入 (HSET key field1 value1 field2 value2) ---
hash)
HASH_KEY="${KEY_PREFIX}:hash:${i}"
FIELD1="name"
VALUE1="User_Name_${i}"
FIELD2="age"
VALUE2=$(( (i % 50) + 20 ))

# HSET 命令 RESP 协议: *6 (HSET, key, f1, v1, f2, v2)
printf "*6\r\n\$4\r\nHSET\r\n\$${#HASH_KEY}\r\n${HASH_KEY}\r\n"
printf "\$${#FIELD1}\r\n${FIELD1}\r\n\$${#VALUE1}\r\n${VALUE1}\r\n"
printf "\$${#FIELD2}\r\n${FIELD2}\r\n\$${#VALUE2}\r\n${VALUE2}\r\n"
;;

# --- List 类型导入 (LPUSH key value) ---
list)
LIST_KEY="${KEY_PREFIX}:list:${i}"
# 循环添加 5 个成员
for j in $(seq 1 $MEMBERS_PER_KEY); do
VALUE="list_element_${i}_${j}"
# LPUSH 命令 RESP 协议: *3 (LPUSH, key, value)
printf "*3\r\n\$5\r\nLPUSH\r\n\$${#LIST_KEY}\r\n${LIST_KEY}\r\n\$${#VALUE}\r\n${VALUE}\r\n"
done
;;

# --- Set 类型导入 (SADD key member) ---
set)
SET_KEY="${KEY_PREFIX}:set:${i}"
# 循环添加 5 个成员
for j in $(seq 1 $MEMBERS_PER_KEY); do
MEMBER="set_member_${i}_${j}"
# SADD 命令 RESP 协议: *3 (SADD, key, member)
printf "*3\r\n\$4\r\nSADD\r\n\$${#SET_KEY}\r\n${SET_KEY}\r\n\$${#MEMBER}\r\n${MEMBER}\r\n"
done
;;

# --- Sorted Set 类型导入 (ZADD key score member) ---
zset)
ZSET_KEY="${KEY_PREFIX}:zset:${i}"
# 循环添加 5 个成员
for j in $(seq 1 $MEMBERS_PER_KEY); do
SCORE="${i}${j}" # 生成唯一分数:例如 key 1 的分数是 11, 12...
MEMBER="zset_member_${i}_${j}"

# ZADD 命令 RESP 协议: *4 (ZADD, key, score, member)
printf "*4\r\n\$4\r\nZADD\r\n\$${#ZSET_KEY}\r\n${ZSET_KEY}\r\n\$${#SCORE}\r\n${SCORE}\r\n\$${#MEMBER}\r\n${MEMBER}\r\n"
done
;;
esac

i=$((i+1))
done | (
# ----------------------------------------
# --- 3. 管道导入到 Redis ---
# ----------------------------------------
echo "📤 正在通过 redis-cli --pipe 导入数据..."

# 构建 redis-cli 命令
REDIS_CLI_CMD="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT}"

# 如果设置了密码,则添加 -a 参数
if [ -n "${REDIS_PASSWORD}" ]; then
REDIS_CLI_CMD="${REDIS_CLI_CMD} -a ${REDIS_PASSWORD}"
fi

# 执行导入
${REDIS_CLI_CMD} --pipe

# 检查命令是否执行成功
if [ $? -eq 0 ]; then
echo "✅ 数据导入成功!共导入 ${DATA_COUNT} 条 [${DATA_TYPE}] 记录。"
else
echo "❌ 数据导入失败!请检查 Redis 服务是否运行以及配置是否正确。"
fi
)

SCAN 的基本用法

  • SCAN 的基本语法如下:

1
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
  • 参数解释:

参数 说明
cursor 游标,从 0 开始,返回值用于下一次扫描;当返回值为 0 时表示遍历结束
MATCH pattern 使用通配符匹配 key(可选)
COUNT count 建议 Redis 每次返回多少 key(可选),尽力而为,并不保证返回的 key 数量与指定的数量一致
TYPE type 匹配的 key 类型(可选)

示例:

  • 为了测试 SCAN 命令,我们首先创建一些测试数据,通过如下脚本初始化 100 条数据

1
sh import_unified_data.sh string 100
    1. 获取所有 key:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 游标初始值为0
127.0.0.1:6379> scan 0
1) "88" # 游标
2) 1) "test_data:string:16"
2) "test_data:string:85"
3) "test_data:string:21"
4) "test_data:string:83"
5) "test_data:string:97"
6) "test_data:string:77"
7) "test_data:string:29"
8) "test_data:string:89"
9) "test_data:string:91"
10) "test_data:string:28"

## 说明
# 实际上只会返回10条key(默认count为10),并不会向 keys * 那样返回所有key
## 扫描结果中:
# 第一个元素是游标,不为0时,表示还有更多的 key 可以继续扫描,只有0表示扫描结束
# 第二个元素是匹配的 key列表

# 接着上面给出的游标,继续扫描:
127.0.0.1:6379> scan 88
1) "92" # 不为0就表示还有更多的 key 可以继续扫描
2) 1) "test_data:string:63"
2) "test_data:string:2"
3) "test_data:string:4"
4) "test_data:string:20"
5) "test_data:string:57"
6) "test_data:string:78"
7) "test_data:string:71"
8) "test_data:string:35"
9) "test_data:string:86"
10) "test_data:string:6"
11) "test_data:string:47"
……………………………………
127.0.0.1:6379> scan 87
1) "0" # 扫描结束 游标为0
2) 1) "test_data:68"
2) "test_data:79"
3) "test_data:41"

    1. 控制每次获取的 key 数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
127.0.0.1:6379> scan 0 count 20
1) "92"
2) 1) "test_data:string:16"
2) "test_data:string:85"
3) "test_data:string:21"
4) "test_data:string:83"
5) "test_data:string:97"
6) "test_data:string:77"
7) "test_data:string:29"
8) "test_data:string:89"
9) "test_data:string:91"
10) "test_data:string:28"
11) "test_data:string:63"
12) "test_data:string:2"
13) "test_data:string:4"
14) "test_data:string:20"
15) "test_data:string:57"
16) "test_data:string:78"
17) "test_data:string:71"
18) "test_data:string:35"
19) "test_data:string:86"
20) "test_data:string:6"
21) "test_data:string:47"
# 注意:COUNT 是“尽力而为”,并不保证一定返回 20 条。
# 本示例就返回了21条,比 count 还多了 1 条
# 甚至有可能一条都不会返回,但是游标却并不为0的情况,此时就需要继续扫描,直到游标为0才算扫描结束。
    1. 扫描以 test_data:string 开头的 key
1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> scan 0 match test_data:string*
1) "88"
2) 1) "test_data:string:16"
2) "test_data:string:85"
3) "test_data:string:21"
4) "test_data:string:83"
5) "test_data:string:97"
6) "test_data:string:77"
7) "test_data:string:29"
8) "test_data:string:89"
9) "test_data:string:91"
10) "test_data:string:28"
    1. 扫描以 test_data:string 开头的 key,并返回 30 条
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
127.0.0.1:6379> scan 0 match test_data:string* count 30
1) "10"
2) 1) "test_data:string:16"
2) "test_data:string:85"
3) "test_data:string:21"
4) "test_data:string:83"
5) "test_data:string:97"
6) "test_data:string:77"
7) "test_data:string:29"
8) "test_data:string:89"
9) "test_data:string:91"
10) "test_data:string:28"
11) "test_data:string:63"
12) "test_data:string:2"
13) "test_data:string:4"
14) "test_data:string:20"
15) "test_data:string:57"
16) "test_data:string:78"
17) "test_data:string:71"
18) "test_data:string:35"
19) "test_data:string:86"
20) "test_data:string:6"
21) "test_data:string:47"
22) "test_data:string:48"
23) "test_data:string:74"
24) "test_data:string:67"
25) "test_data:string:26"
26) "test_data:string:60"
27) "test_data:string:36"
28) "test_data:string:49"
29) "test_data:string:3"
30) "test_data:string:44"
31) "test_data:string:68"

# 这里我们虽然指定了 count 30,但是实际返回的 key 数量却有 31 个
# 也有可能比 count 少,甚至为 0 个
    1. 扫描以 h_key 开头的 key,并返回 30 条
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 先插入一条记录
127.0.0.1:6379> set h_key:1 1
OK
127.0.0.1:6379> scan 0 match h_key* count 30
1) "124"
2) (empty array)

# 这里我们虽然指定了 count 30,但是实际返回的 key 数量为 0 个
# 虽然我们在redis中设置了h_key:1,但是h_key:1 并没有被返回
# 并且此时返回的游标也不为0,说明我们还需要继续扫描

# 完整的扫描过程如下:
127.0.0.1:6379> scan 0 match h_key* count 30
1) "124"
2) (empty array) # empty array 也不能说明一定没有
127.0.0.1:6379> scan 124 match h_key* count 30
1) "9"
2) (empty array)
127.0.0.1:6379> scan 9 match h_key* count 30
1) "43"
2) 1) "h_key:1"
127.0.0.1:6379> scan 43 match h_key* count 30
1) "0" # 只有游标为0时才算扫描结束
2) (empty array)

# 实际上redis官方推荐:当返回的游标不为0,但是key数量为0时,下一次扫描可以增加count的数量,比如扩大2倍,这样可以有效减少扫描次数
127.0.0.1:6379> scan 0 match h_key* count 30
1) "124"
2) (empty array)
127.0.0.1:6379> scan 124 match h_key* count 60
1) "75"
2) 1) "h_key:1"
127.0.0.1:6379> scan 75 match h_key* count 60
1) "0"
2) (empty array)
    1. 扫描指定类型的key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
127.0.0.1:6379> scan 0 type string
1) "88"
2) 1) "test_data:string:16"
2) "test_data:string:85"
3) "test_data:string:21"
4) "test_data:string:83"
5) "test_data:string:97"
6) "test_data:string:77"
7) "test_data:string:29"
8) "test_data:string:89"
9) "test_data:string:91"
10) "test_data:string:28"

# 支持的类型有:
# string list set zset hash stream
# 注意一个小问题,某些 Redis 类型,如 GeoHashes、HyperLogLogs、Bitmaps 和 Bitfield,内部可能用其他 Redis 类型实现,如字符串或 zset,因此无法通过 type 区分 。
# 例如,ZSET 和 GEOHASH 都是 zset 类型。
127.0.0.1:6379> GEOADD geokey 0 0 value
(integer) 1
127.0.0.1:6379> type geokey
zset
127.0.0.1:6379> ZADD zkey 1000 value
(integer) 1
127.0.0.1:6379> SCAN 0 TYPE zset count 1000
1) "0"
2) 1) "zkey"
2) "geokey"

HSCAN 的基本使用方法

  • HSCAN 的基本语法如下:

1
HSCAN key cursor [MATCH pattern] [COUNT count] [NOVALUES]
  • 参数解释:

参数 说明
key 键名
cursor 游标,从 0 开始,返回值用于下一次扫描;当返回值为 0 时表示遍历结束
MATCH pattern 使用通配符匹配 key(可选)
COUNT count 建议 Redis 每次返回多少 key(可选),尽力而为,并不保证返回的 key 数量与指定的数量一致
NOVALUES redis8+增加的属性,只返回key,不返回 value(可选)
  • 为了测试 HSCAN,我们先插入一些数据:

1
sh import_unified_data.sh hash 100

使用示例

    1. 获取所有 field 和 value:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> scan 0 type hash
1) "48"
2) 1) "test_data:hash:42"
2) "test_data:hash:71"
3) "test_data:hash:54"
4) "test_data:hash:87"
5) "test_data:hash:96"
6) "test_data:hash:49"
# 获取 test_data:hash:42 的所有 field 和 value
127.0.0.1:6379> hscan test_data:hash:42 0
1) "0"
2) 1) "name"
2) "User_Name_42"
3) "age"
4) "62"
    1. 模糊匹配 field:
1
2
3
4
127.0.0.1:6379> hscan test_data:hash:42 0 match age*
1) "0"
2) 1) "age"
2) "62"
    1. 模糊匹配 field,并返回 10 条数据:
1
2
3
4
5
6
127.0.0.1:6379> hscan test_data:hash:42 0 match *e* count 10
1) "0"
2) 1) "name"
2) "User_Name_42"
3) "age"
4) "62"
    1. 不显示value(需要redis8+)
1
2
3
4
127.0.0.1:6379> hscan test_data:hash:42 0 novalues
1) "0"
2) 1) "name"
2) "age"

SSCAN 的基本使用方法

  • SSCAN 的基本语法如下:

1
SSCAN key cursor [MATCH pattern] [COUNT count]

使用示例

    1. 获取指定key的所有元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> scan 0 type set
1) "112"
2) 1) "test_data:set:43"
2) "test_data:set:33"
3) "test_data:set:98"
4) "test_data:set:61"
5) "test_data:set:2"
6) "test_data:set:70"
127.0.0.1:6379> sscan test_data:set:43 0
1) "0"
2) 1) "set_member_43_1"
2) "set_member_43_2"
3) "set_member_43_3"
4) "set_member_43_4"
5) "set_member_43_5"
    1. 模糊匹配元素:
1
2
3
127.0.0.1:6379> sscan test_data:set:43 0 match *_3*
1) "0"
2) 1) "set_member_43_3"
    1. 返回 1 个元素: 实际上控制不住,大概率会返回所有元素
1
2
3
4
5
6
7
127.0.0.1:6379> sscan test_data:set:43 0 count 1
1) "0"
2) 1) "set_member_43_1"
2) "set_member_43_2"
3) "set_member_43_3"
4) "set_member_43_4"
5) "set_member_43_5"

ZSCAN 的基本使用方法

  • ZSCAN 的基本语法如下:

1
ZSCAN key cursor [MATCH pattern] [COUNT count]

使用示例

    1. 获取指定key的所有元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
127.0.0.1:6379> scan 0 type zset
1) "112"
2) 1) "test_data:zset:89"
2) "test_data:zset:81"
3) "test_data:zset:73"
4) "test_data:zset:79"
5) "test_data:zset:15"

127.0.0.1:6379> zscan test_data:zset:89 0
1) "0"
2) 1) "zset_member_89_1"
2) "891"
3) "zset_member_89_2"
4) "892"
5) "zset_member_89_3"
6) "893"
7) "zset_member_89_4"
8) "894"
9) "zset_member_89_5"
10) "895"
    1. 模糊匹配元素:
1
2
3
4
127.0.0.1:6379> zscan test_data:zset:89 0 match *_3*
1) "0"
2) 1) "zset_member_89_3"
2) "893"
    1. 获取指定key的所有元素,并返回 1 个元素,同样控制不住
1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379>  zscan test_data:zset:89 0 count 1
1) "0"
2) 1) "zset_member_89_1"
2) "891"
3) "zset_member_89_2"
4) "892"
5) "zset_member_89_3"
6) "893"
7) "zset_member_89_4"
8) "894"
9) "zset_member_89_5"
10) "895"