文章目录
-
- [Redis 渐进式遍历与数据库管理](#Redis 渐进式遍历与数据库管理)
- 一、前言
- [二、为什么需要 SCAN](#二、为什么需要 SCAN)
-
- [2.1 KEYS 命令的致命问题](#2.1 KEYS 命令的致命问题)
- [2.2 SCAN 的解决思路](#2.2 SCAN 的解决思路)
- [三、SCAN 命令详解](#三、SCAN 命令详解)
-
- [3.1 语法](#3.1 语法)
- [3.2 游标机制](#3.2 游标机制)
- [3.3 完整遍历示例](#3.3 完整遍历示例)
- [3.4 带 MATCH 过滤的示例](#3.4 带 MATCH 过滤的示例)
- [3.5 COUNT 参数的含义](#3.5 COUNT 参数的含义)
- [3.6 SCAN 的局限性](#3.6 SCAN 的局限性)
- [3.7 Hash、Set、Zset 也有对应的 SCAN 命令](#3.7 Hash、Set、Zset 也有对应的 SCAN 命令)
- 四、数据库管理
-
- [4.1 Redis 的多数据库](#4.1 Redis 的多数据库)
- [4.2 SELECT ------ 切换数据库](#4.2 SELECT —— 切换数据库)
- [4.3 FLUSHDB 和 FLUSHALL ------ 清空数据库](#4.3 FLUSHDB 和 FLUSHALL —— 清空数据库)
- 五、核心知识点回顾
-
- [5.1 五种数据类型一览](#5.1 五种数据类型一览)
- [5.2 内部编码总结](#5.2 内部编码总结)
- [5.3 单线程架构的三个核心结论](#5.3 单线程架构的三个核心结论)
- [5.4 键名设计规范](#5.4 键名设计规范)
- [5.5 数据类型选型决策表](#5.5 数据类型选型决策表)
- 六、总结
-
- [6.1 SCAN 使用模板](#6.1 SCAN 使用模板)
Redis 渐进式遍历与数据库管理
一、前言
💬 这一篇讲什么: SCAN 渐进式遍历和数据库管理命令
🚀 核心内容:
- 为什么要有 SCAN?它是如何解决 KEYS 阻塞问题的?
- SCAN 的游标机制是怎么运作的?
- 数据库切换、清空等管理命令有哪些?
- 五种数据类型的核心知识点回顾
把五种数据类型全部讲完了。这一篇收个尾:讲清楚如何在不阻塞 Redis 的情况下遍历大量 key,以及数据库管理的几个重要命令。最后对核心内容做一个完整回顾。
二、为什么需要 SCAN
2.1 KEYS 命令的致命问题
在第四篇介绍全局命令时,我们提到了 KEYS 命令可以用来查找匹配模式的 key:
bash
KEYS * # 返回所有 key
KEYS user:* # 返回所有以 user: 开头的 key
但:生产环境中禁止使用 KEYS 命令。
原因很简单:KEYS 的时间复杂度是 O(N) ,N 是数据库中所有 key 的总数。如果 Redis 里存了 1000 万个 key,执行 KEYS * 就要遍历全部 1000 万个,这期间 Redis 的单线程被完全占用,所有其他客户端的请求全部阻塞等待。
2.2 SCAN 的解决思路
SCAN 命令的核心思想是:不一次性遍历所有 key,而是分批次、渐进式地遍历。
每次调用 SCAN,只处理一小批 key,然后返回下一次遍历的起始位置(游标),由调用方控制节奏,多次调用才能完成整个遍历。
这样每次 SCAN 的时间复杂度是 O(1),不会长时间占用 Redis 的处理线程,其他命令可以正常穿插执行。
完整遍历所有 key 需要多次 SCAN,总体时间复杂度是 O(N),但代价被分摊到了多个请求上,不会造成单次阻塞。
三、SCAN 命令详解
3.1 语法
bash
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
参数说明:
| 参数 | 说明 |
|---|---|
cursor |
游标,第一次调用传 0,后续传上次返回的游标值 |
MATCH pattern |
可选,只返回匹配该模式的 key(过滤在返回前进行) |
COUNT count |
可选,提示每次扫描的数量(默认 10,注意是提示不是精确限制) |
TYPE type |
可选,只返回指定类型的 key(string、list、hash、set、zset) |
命令有效版本:2.8.0 之后
时间复杂度:O(1)(单次)
返回值:包含两个元素的数组:
- 第一个元素:下一次 SCAN 的游标值
- 第二个元素:本次扫描到的 key 列表
3.2 游标机制
SCAN 的游标遍历过程如下:
bash
# 第一次:从游标 0 开始
redis> SCAN 0 COUNT 3
1) "17" # 下一次的游标
2) 1) "key:8"
2) "key:3"
3) "key:7"
# 第二次:用上次返回的游标 17 继续
redis> SCAN 17 COUNT 3
1) "0" # 游标返回 0,说明遍历结束!
2) 1) "key:1"
2) "key:5"
3) "key:9"
遍历结束的标志 :当 SCAN 返回的游标值为 0 时,表示已经遍历了完整的一圈,遍历结束。
3.3 完整遍历示例
bash
# 假设 Redis 里有 key:0 ~ key:19 共 20 个 key
redis> SCAN 0
1) "17"
2) 1) "key:12"
2) "key:8"
3) "key:4"
4) "key:14"
5) "key:16"
6) "key:17"
7) "key:15"
8) "key:10"
9) "key:3"
10) "key:7"
11) "key:1"
redis> SCAN 17
1) "0"
2) 1) "key:5"
2) "key:18"
3) "key:0"
4) "key:2"
5) "key:19"
6) "key:13"
7) "key:6"
8) "key:9"
9) "key:11"
两次 SCAN 之后游标返回 0,遍历结束,合计找到了所有 20 个 key。
3.4 带 MATCH 过滤的示例
bash
# 只遍历 user: 开头的 key
redis> SCAN 0 MATCH "user:*" COUNT 100
1) "0"
2) 1) "user:1001"
2) "user:2089"
3) "user:3017"
注意 :MATCH 的过滤是在扫描之后进行的。也就是说,Redis 先扫描一批 key,再从里面筛选匹配的,所以即使带了 MATCH,也可能某次返回的 key 列表是空的(当这批 key 都不匹配时)。这不代表遍历结束,要继续看游标值。
3.5 COUNT 参数的含义
COUNT 不是精确地控制每次返回多少个 key,而是给 Redis 的一个提示:每次大概扫描多少个槽位。实际返回的 key 数量可能多也可能少。
通常来说,COUNT 越大,单次 SCAN 返回的 key 越多(但单次耗时也越长);COUNT 越小,单次更快但需要更多次 SCAN 才能遍历完。默认值是 10,在实际中通常可以调大到 100-1000 来平衡性能。
3.6 SCAN 的局限性
SCAN 虽然解决了阻塞问题,但也有需要注意的地方:
局限一:遍历期间 key 有变化时,结果不保证完整。 如果在 SCAN 遍历过程中有其他客户端在增删 key,可能会导致:
- 某些 key 被重复遍历(多次出现在结果中)
- 某些 key 被遗漏(没有出现在任何一次结果中)
在实际开发中要考虑这个问题,通常的处理方式是:对于可能重复的 key 做去重处理;对于允许少量遗漏的场景可以直接使用。
局限二:不保证每次返回的数量精确等于 COUNT。
3.7 Hash、Set、Zset 也有对应的 SCAN 命令
SCAN 是针对数据库全局 key 的渐进式遍历。Redis 还提供了针对单个数据类型内部元素的渐进式遍历命令:
| 命令 | 用途 |
|---|---|
HSCAN key cursor [MATCH pattern] [COUNT count] |
渐进式遍历 Hash 的所有 field-value |
SSCAN key cursor [MATCH pattern] [COUNT count] |
渐进式遍历 Set 的所有 member |
ZSCAN key cursor [MATCH pattern] [COUNT count] |
渐进式遍历 Zset 的所有 member-score |
用法和 SCAN 完全一样,只是作用对象从数据库 key 变成了某个数据类型内部的元素。在需要遍历有大量字段的 Hash,或者元素很多的 Set/Zset 时,应该使用这些命令代替 HGETALL、SMEMBERS、ZRANGE 0 -1。
四、数据库管理
4.1 Redis 的多数据库
Redis 提供了多数据库的支持,默认配置下共有 16 个数据库,编号从 0 到 15。
不同于 MySQL 用字符串名称区分数据库,Redis 用数字作为数据库标识。每个数据库之间的数据是完全隔离的,互不干扰:
text
数据库 0 数据库 1 ... 数据库 15
key: k1 key: k1 key: k1
val: v1 val: v100 val: v999
三个数据库里都可以有 k1 这个 key,但它们的值各自独立,不会冲突。
默认情况下,我们连接 Redis 后处于数据库 0。
4.2 SELECT ------ 切换数据库
bash
SELECT dbIndex
示例:
bash
127.0.0.1:6379> SELECT 0
OK
127.0.0.1:6379> SET k1 "in database 0"
OK
127.0.0.1:6379> SELECT 15
OK
127.0.0.1:6379[15]> SET k1 "in database 15"
OK
127.0.0.1:6379[15]> SELECT 0
OK
127.0.0.1:6379> GET k1
"in database 0" # 数据库 0 和数据库 15 的 k1 完全独立
命令行提示符中的 [15] 表示当前在数据库 15,不显示编号时表示数据库 0。
❗ 实际上不建议使用多数据库特性。 原因有三:
- Redis 并没有为多数据库提供太多特性,比如不同数据库之间不能直接做数据迁移。
- 无论用多少个数据库,Redis 仍然是单线程模型,彼此之间还是要排队等待命令执行,没有真正的隔离效果。
- 多数据库会让开发、调试和运维工作变得复杂,出问题时容易搞混是哪个数据库的问题。
更好的做法 :如果真的需要隔离不同的数据,维护多个独立的 Redis 实例,而不是在一个实例里使用多个数据库。实践中,始终使用数据库 0 是一个好习惯。
4.3 FLUSHDB 和 FLUSHALL ------ 清空数据库
bash
FLUSHDB # 清空当前数据库的所有 key
FLUSHALL # 清空所有数据库的所有 key
☠️ 这两个命令是高危操作,永远不要在生产环境中执行,除非你做好了承担后果的准备。 执行之后数据库中的所有数据会被立即、不可恢复地删除。
即使在开发和测试环境,执行前也要三思,确认当前连接的是哪个 Redis 实例、哪个数据库。
五、核心知识点回顾
5.1 五种数据类型一览
| 数据类型 | 特点 | 内部编码 | 典型场景 |
|---|---|---|---|
| String | 最基础,value 可以是字符串/数字/二进制 | int、embstr、raw | 缓存、计数器、Session、验证码 |
| Hash | key 下有多个 field-value,适合存储对象 | ziplist、hashtable | 用户信息、商品属性存储 |
| List | 有序、可重复、支持双端操作 | quicklist(ziplist+linkedlist) | 消息队列、时间线 |
| Set | 无序、不重复,支持集合运算 | intset、hashtable | 标签、共同好友、UV 统计 |
| Zset | 有序(按 score)、不重复 | ziplist、skiplist | 排行榜、延迟队列、限流 |
5.2 内部编码总结
每种数据类型都有多种内部编码实现,Redis 根据数据规模自动切换:
text
数据量小/元素少 → 紧凑编码(ziplist、intset、embstr):省内存
数据量大/元素多 → 高效编码(hashtable、skiplist、raw):高性能
这种设计让 Redis 在小规模数据下节省内存,在大规模数据下保证性能,对用户完全透明。
5.3 单线程架构的三个核心结论
学完了所有数据类型,回过头来再强化三个关于单线程架构的核心结论:
结论一:Redis 的高性能来自三个方面。 纯内存操作(速度快)、epoll IO 多路复用(不阻塞等待网络)、单线程串行执行(无锁竞争开销)。
结论二:单线程的最大风险是慢命令。 任何一条执行时间过长的命令都会阻塞所有其他客户端。以下命令在大数据量场景下要格外谨慎:
| 危险命令 | 原因 | 替代方案 |
|---|---|---|
KEYS * |
O(N),遍历所有 key | SCAN |
HGETALL |
O(N),大 hash 会阻塞 | HMGET 或 HSCAN |
SMEMBERS |
O(N),大 set 会阻塞 | SSCAN |
ZRANGE 0 -1 |
O(N),大 zset 会阻塞 | ZSCAN 或分页查询 |
DEL bigkey |
删除大 key 时很慢 | UNLINK(异步删除) |
结论三:批量命令要控制数量。 MGET、MSET、HMGET 等批量命令虽然减少了网络往返,但单次处理的数据量过大同样会阻塞 Redis,实践中建议单次批量不超过几百到几千个 key。
5.4 键名设计规范
Redis 对键名没有强制要求,但良好的键名设计对可维护性至关重要。推荐使用以下格式:
test
业务名:对象名:唯一标识[:属性]
实际示例:
bash
user:info:1001 # 用户 1001 的信息(Hash)
user:1001:tags # 用户 1001 的标签(Set)
video:playcount:5253 # 视频 5253 的播放量(String)
user:ranking:2025-11-15 # 2025年11月15日的用户排行榜(Zset)
task:queue # 任务队列(List)
键名过长会消耗更多内存,也会影响 Redis 的性能。在团队内约定好缩写规范,比如 u:1001:fr 代替 user:1001:friends,在保证可读性的前提下控制键名长度。
5.5 数据类型选型决策表
面对一个业务需求,如何选择合适的数据类型?
| 需求 | 推荐类型 | 原因 |
|---|---|---|
| 存储单个值(字符串/数字) | String | 最基础、最通用 |
| 存储对象(多个属性,部分更新) | Hash | 字段级操作更高效 |
| 存储对象(总是整体读写) | String(JSON) | 序列化简单,整体性能好 |
| 有序列表,允许重复 | List | 双端操作,消息队列 |
| 无序集合,需要去重 | Set | 天然去重,支持集合运算 |
| 需要排序的集合 | Zset | 按分数排序,O(log N) |
| 需要统计共同元素 | Set | SINTER 求交集 |
| 需要维护排行榜 | Zset | ZADD + ZREVRANGE |
六、总结
到这里,基础数据类型就全部学完了。
✅ SCAN 命令:渐进式遍历,解决 KEYS 的阻塞问题;游标从 0 开始,返回 0 时结束;HSCAN/SSCAN/ZSCAN 用于遍历数据类型内部元素
✅ 数据库管理:16 个数据库(SELECT 0~15);FLUSHDB/FLUSHALL 是高危命令;实践中建议始终使用数据库 0
✅ 五种数据类型全部掌握:String / Hash / List / Set / Zset,每种都有对应的内部编码和适合的使用场景
✅ 单线程架构的核心结论:纯内存 + epoll + 单线程 = 高性能;慢命令是最大风险;批量命令控制数量
✅ 键名设计规范 :业务:对象:id[:属性] 格式,控制长度,避免冲突
6.1 SCAN 使用模板
python
# 完整遍历所有 key 的标准写法
cursor = 0
all_keys = []
while True:
cursor, keys = redis.scan(cursor, match="user:*", count=100)
all_keys.extend(keys)
if cursor == 0: # 游标返回 0,遍历结束
break
# all_keys 包含所有匹配的 key(可能有少量重复,需要去重)
all_keys = list(set(all_keys))
下一篇预告:Redis 持久化 ------ RDB 快照与 AOF 日志的完整原理,触发机制、优缺点对比,以及生产环境中的最佳实践。