【Redis篇】Redis 渐进式遍历与数据库管理

文章目录

    • [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 时,应该使用这些命令代替 HGETALLSMEMBERSZRANGE 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 会阻塞 HMGETHSCAN
SMEMBERS O(N),大 set 会阻塞 SSCAN
ZRANGE 0 -1 O(N),大 zset 会阻塞 ZSCAN 或分页查询
DEL bigkey 删除大 key 时很慢 UNLINK(异步删除)

结论三:批量命令要控制数量。 MGETMSETHMGET 等批量命令虽然减少了网络往返,但单次处理的数据量过大同样会阻塞 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 日志的完整原理,触发机制、优缺点对比,以及生产环境中的最佳实践。

相关推荐
Byron__1 小时前
Redis高频面试:数据结构+编码+分布式锁+缓存问题
redis·缓存·面试
xcLeigh1 小时前
KES数据库运维监控与故障排查实战
运维·数据库·sql·故障排查·运维监控·kes
GlobalSign数字证书1 小时前
中小企业的 SSL/TLS 证书管理,有更轻量的方案
数据库·网络协议·ssl
梓䈑1 小时前
【MySQL】库的操作(数据库的创建、查看、修改 和 备份)
数据库·mysql
小马爱打代码1 小时前
Redis Key 过期后会立刻删除吗?过期删除与内存淘汰策略详解
java·redis·缓存
yuzhiboyouye1 小时前
原生 SQL 常用核心语句基础语法
数据库·sql·oracle
我是一颗柠檬1 小时前
【Redis】事务与Lua脚本Day7(2026年)
数据库·redis·后端·lua·database
流星白龙1 小时前
【MySQL高阶】14.MySQL存储结构
android·数据库·mysql
一只fish1 小时前
Oracle官方文档翻译《Database Concepts 26ai》第18章-进程架构
数据库·oracle