Redis Scan 原理解析与注意点

文章讨论了Redis中Keys命令的缺点,如可能导致服务卡顿,以及为何应使用Scan命令替代。Scan命令通过分次遍历和提供count参数以降低阻塞风险。尽管Scan可能返回重复结果且在数据修改时存在不确定性,但其设计旨在减少对Redis线程的影响。文章还提到在特定场景下,如远程连接和大量Key时,调整Scan的Count参数对性能的影响,推荐值约为1W。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、 概述
由于 Redis 是单线程在处理用户的命令,而 Keys 命令会一次性遍历所有 Key,于是在 命令执行过程中,无法执行其他命令。这就导致如果 Redis 中的 key 比较多,那么 Keys 命令执行时间就会比较长,从而阻塞 Redis。
所以很多教程都推荐使用 Scan 命令来代替 Keys,因为 Scan 可以限制每次遍历的 key 数量。

Keys 的缺点:
1)没有limit,我们只能一次性获取所有符合条件的key,如果结果有上百万条,那么等待你的就是“无穷无尽”的字符串输出。
2)keys命令是遍历算法,时间复杂度是O(N)。如我们刚才所说,这个命令非常容易导致Redis服务卡顿。因此,我们要尽量避免在生产环境使用该命令。
相比于keys命令,Scan命令有两个比较明显的优势:
1)Scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程。
2)Scan命令提供了 count 参数,可以控制每次遍历的集合数。

可以理解为 Scan 是渐进式的 Keys。

Scan 命令语法如下:

SCAN cursor [MATCH pattern] [COUNT count]
  • cursor - 游标。
  • pattern - 匹配的模式。
  • count - 指定每次遍历多少个集合。
  1. 可以简单理解为每次遍历多少个元素
  2. 根据测试,推荐 Count大小为 1W。

Scan 返回值为数组,会返回一个游标+一系列的 Key
大致用法如下:
SCAN命令是基于游标的,每次调用后,都会返回一个游标,用于下一次迭代。当游标返回0时,表示迭代结束。

第一次 Scan 时指定游标为 0,表示开启新的一轮迭代,然后 Scan 命令返回一个新的游标,作为第二次 Scan 时的游标值继续迭代,一直到 Scan 返回游标为0,表示本轮迭代结束。

通过这个就可以看出,Scan 完成一次迭代,需要和 Redis 进行多次交互。

Scan原理
Redis使用了Hash表作为底层实现,原因不外乎高效且实现简单。类似于HashMap那样数组+链表的结构。其中第一维的数组大小为2n(n>=0)。每次扩容数组长度扩大一倍。
Scan命令就是对这个一维数组进行遍历。每次返回的游标值也都是这个数组的索引。Count 参数表示遍历多少个数组的元素,将这些元素下挂接的符合条件的结果都返回。因为每个元素下挂接的链表大小不同,所以每次返回的结果数量也就不同。


Scan 命令注意事项

  • 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
  • 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
  • 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;

二、 Scan 踩坑
使用时遇到一个 特殊场景,跨区域远程连接 Redis 并进行模糊查询,扫描所有指定前缀的 Key。
最开始也没多想,直接就是开始 Scan,然后 Count 参数指定的是 1000。

Redis 中大概几百万 Key,最后发现这个接口需要几十上百秒才返回。
什么原因呢?
Scan 命令中的 Count 指定一次扫描多少 Key,这里指定为 1000,几百万Key就需要几千次迭代,即和 Redis 交互几千次,然后因为是远程连接,网络延迟比较大,所以耗时特别长。
最后将 Count 参数调大后,减少了交互次数,就好多了。Count 参数越大,Redis 阻塞时间也会越长,需要取舍。极限一点,Count 参数和总 Key 数一致时,Scan 命令就和 Keys 效果一样了。
Count 大小和 Scan 总耗时的关系如下图:

可以发现 Count 越大,总耗时就越短,不过越后面提升就越不明显了。所以推荐的 Count 大小为 1W 左右。
如果不考虑 Redis 的阻塞,其实 Keys 比 Scan 会快很多,毕竟一次性处理,省去了多余的交互。

 

### Redis线程模型详解 Redis 采用的是基于 Reactor 模式的单线程事件驱动模型,其核心设计目标是通过高效的 I/O 多路复用机制来实现高并发处理能力。在该模型中,Redis 主线程负责监听和处理所有客户端的请求,包括网络读写、命令执行以及定时任务等操作。 Redis 使用了 I/O 多路复用技术(如 `epoll`、`kqueue`、`select` 等),根据操作系统类型选择最优的实现方式,以提高网络通信效率。这种机制允许一个线程同时管理多个连接,减少了线程切换带来的开销,并且避免了多线程编程中的同步问题 [^3]。 ### Redis 的单线程多线程演进 虽然 Redis 被广泛称为“单线程”,但准确地说,它是一个使用 Reactor 模型的单线程服务器。主线程负责接收新连接、处理已建立的连接上的数据读取和写入操作,并执行命令逻辑。这种设计简化了系统复杂性并降低了资源竞争的可能性 [^1]。 从 Redis 4.0 开始,引入了多线程来异步删除大键值对,以此减轻主线程负担并提升性能。到了 Redis 6.0 版本,则进一步增加了对网络请求处理的多线程支持,特别是针对网络 I/O 的读写操作进行了优化,从而显著提高了吞吐量和服务响应速度 。 ### 工作原理分析 当客户端发送命令到 Redis 服务端时,整个过程大致如下: 1. **事件循环**:Redis 进入事件循环,持续等待文件事件(客户端连接、数据可读/可写)或时间事件(定期清理过期键等)的发生。 2. **I/O 多路复用**:利用底层 I/O 多路复用函数库监听多个 socket 描述符的状态变化。一旦某个描述符就绪,就会触发相应的事件处理器。 3. **事件处理**:对于每个就绪的 socket,调用注册的事件回调函数进行处理。如果是新的连接到达,则接受这个连接并为其绑定读事件;如果已有连接上有数据可读,则读取数据、解析命令、执行对应的操作;如果是数据需要发送回客户端,则设置写事件以便后续可以将结果写出。 4. **命令执行**:解析完客户端发来的命令后,在内存中执行实际的数据操作。由于这些操作都是串行化的,所以不会出现并发访问的问题。 5. **回复生成输出**:完成命令执行之后,生成响应信息并通过网络接口返回给客户端。 值得注意的是,在 Redis 中存在一些潜在阻塞操作,比如 `KEYS` 命令会遍历所有数据库中的键,这可能导致主线程长时间无法响应其他请求。为了解决这个问题,推荐使用非阻塞替代方案如 `SCAN` 来逐步迭代键空间 [^4]。 ### 性能优势局限性 Redis 的单线程模型之所以能够提供出色的性能表现,主要得益于以下几个方面: - 内存存储:所有数据都保存在内存中,访问速度快; - 非阻塞网络:借助 I/O 多路复用技术实现了高效的网络通信; - 简洁协议:采用简单明了的文本协议格式,易于解析; - 无锁机制:因为只有一个线程处理业务逻辑,所以不需要考虑锁竞争和上下文切换的成本。 然而,这种方式也有一定的限制,尤其是在 CPU 密集型任务或者需要大量计算的情况下,单线程可能成为瓶颈。为此,可以通过部署集群模式或者结合 Lua 脚本来缓解部分压力 [^5]。 ```c // 示例代码片段展示了如何使用 C 语言创建简单的 Redis 客户端连接 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> int main() { int client_socket; struct sockaddr_in server_addr; // 创建 TCP 套接字 client_socket = socket(AF_INET, SOCK_STREAM, 0); if (client_socket == -1) { perror("Socket creation failed"); exit(EXIT_FAILURE); } // 设置服务器地址结构体 memset(&server_addr, '0', sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(6379); // Redis 默认端口 inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); // 连接到 Redis 服务器 if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) { perror("Connection with the server failed"); close(client_socket); exit(EXIT_FAILURE); } printf("Connected to Redis server successfully.\n"); // 此处省略具体的 Redis 协议交互细节... close(client_socket); return 0; } ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值