redis scan(超详细)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2900+ 小伙伴加入学习 ,欢迎点击围观
前言:为什么需要Redis Scan?
在Redis的日常使用中,我们经常需要对数据库中的键进行遍历操作,例如统计键数量、批量删除过期键或执行数据迁移。然而,当键的数量达到百万甚至亿级别时,传统的KEYS
命令就会暴露出严重的性能问题——它会阻塞Redis服务器直到操作完成,导致服务不可用。
为解决这一痛点,Redis 2.8版本引入了渐进式遍历算法,其核心命令正是本文的主角——SCAN
。通过SCAN
,开发者可以在不阻塞Redis主线程的前提下,实现对数据库的高效键遍历。
一、Redis键遍历的前世今生
1.1 传统命令的局限性
Redis早期提供了KEYS
命令用于获取所有键,但它的实现原理是单线程扫描整个数据库,当键数量超过10万时,执行时间会显著增加。例如:
127.0.0.1:6379> KEYS *
1) "user:10001"
2) "order:20230801"
...(假设返回100万条结果)
这种阻塞性操作在生产环境中是致命的,尤其当数据库规模增长时,可能引发服务雪崩。
1.2 渐进式遍历的诞生
SCAN
命令通过游标(Cursor)机制和分片(Sharding)策略,实现了非阻塞的键遍历。它将数据库划分为多个片段,每次仅扫描其中一部分,通过游标记录进度,允许开发者控制遍历节奏。
二、Redis Scan的核心原理
2.1 游标机制:像图书馆索引一样分页
想象一个图书馆的索引系统:每个书架(数据库分片)都有独立的索引卡片(键列表),而读者(客户端)通过索引卡编号(游标)逐步翻阅。
SCAN
命令的每次调用会返回两个值:
- 游标:指向下一个要扫描的位置
- 键列表:本次扫描的结果
示例流程:
127.0.0.1:6379> SCAN 0
1) "123"
2) ["user:10001", "order:20230801"]
127.0.0.1:6379> SCAN 123
1) "456"
2) ["product:abc", "cache:session:xyz"]
127.0.0.1:6379> SCAN 0
1) "0"
2) []
2.2 内部实现:哈希表与分片
Redis将所有键存储在一个或多个哈希表中(默认16个),SCAN
通过以下步骤工作:
- 根据游标确定当前扫描的哈希表索引
- 在该哈希表中按步长遍历
- 当前哈希表遍历完成后切换到下一个表
关键点:
- 非阻塞:每次仅扫描少量键
- 概率覆盖:通过随机步长降低哈希碰撞概率
- 最终一致性:可能因并发操作漏掉新键或重复旧键
三、SCAN命令的参数详解
3.1 基础语法
SCAN cursor [MATCH pattern] [COUNT count]
参数 | 描述 |
---|---|
cursor | 初始为0,后续使用返回的游标值 |
MATCH | 使用glob风格的模式匹配(如user:* ),匹配会降低性能 |
COUNT | 每次返回的键数量建议值(实际可能更多或更少) |
3.2 参数调优示例
127.0.0.1:6379> SCAN 0 MATCH order:* COUNT 100
注意:COUNT
参数是建议值而非强制,Redis会根据负载动态调整实际返回数量。
四、SCAN的高级用法与最佳实践
4.1 完整遍历流程代码示例(Python)
import redis
client = redis.Redis(host='localhost', port=6379)
def scan_all_keys(pattern=None):
cursor = '0'
while cursor != 0:
cursor, keys = client.scan(cursor=cursor, match=pattern, count=1000)
for key in keys:
print(key.decode('utf-8')) # 处理每个键
scan_all_keys("user:*")
4.2 处理并发问题的策略
由于SCAN
是概率性遍历,可能出现以下情况:
- 漏扫:新创建的键可能未被本次遍历捕获
- 重复:键被删除后可能再次出现在后续扫描中
解决方案:
- 结合
EXPIRE
机制定期清理 - 使用
UNLINK
代替DEL
减少阻塞 - 对关键操作添加二次校验
五、SCAN的典型应用场景
5.1 清理过期缓存
def clean_expired_cache():
cursor = '0'
while cursor != 0:
cursor, keys = client.scan(cursor=cursor, match="cache:*", count=1000)
for key in keys:
ttl = client.ttl(key)
if ttl < 0: # 已过期
client.unlink(key) # 非阻塞删除
5.2 数据迁移
在迁移百万级用户数据时,使用SCAN
配合管道(Pipeline)可显著提升效率:
pipe = client.pipeline()
for key in keys:
pipe.dump(key) # 序列化键值
pipe.ttl(key) # 保留TTL
if len(pipe) % 1000 == 0:
pipe.execute()
5.3 实时监控与统计
通过周期性SCAN
实现在线数据监控:
127.0.0.1:6379> EVAL "local count=0; for _,key in ipairs(redis.call('SCAN', KEYS.cursor)) do count=count+1 end; return count" 0 0
六、SCAN与其他遍历命令的对比
命令 | 是否阻塞 | 是否支持模式匹配 | 是否分页控制 | |
---|---|---|---|---|
KEYS | 是 | 是 | 否 | |
SCAN | 否 | 是 | 是 | |
SSCAN | 否 | 是 | 是 | 适用于集合类型 |
HSCAN | 否 | 是 | 是 | 适用于哈希类型 |
选择建议:
- 小规模测试环境可用
KEYS
- 生产环境优先使用
SCAN
及其变体(SSCAN
等)
七、常见问题与解决方案
7.1 为什么两次遍历结果不一致?
由于Redis是多线程环境,键可能在遍历过程中被增删改。可通过以下方式缓解:
- 在遍历开始前设置只读模式
- 对关键操作加锁
7.2 如何保证遍历100%覆盖?
理论上无法完全保证,但可通过以下方法提高可靠性:
- 缩短遍历间隔
- 结合AOF重放机制
- 使用
SCAN
的_ITER
变体命令(如HSCAN
)
结论:拥抱渐进式遍历的新时代
SCAN
命令的诞生标志着Redis在高并发场景下实现了键遍历的范式转移。通过理解其游标机制、参数配置和典型应用场景,开发者可以有效避免服务阻塞,构建更健壮的分布式系统。
在实际项目中,建议将SCAN
与业务逻辑深度结合:例如在用户活跃期外执行数据清理,在低峰时段进行数据迁移。随着Redis 7.0版本对渐进式命令的持续优化,这种非阻塞的遍历方式必将成为分布式系统设计的基石。
提示:当需要处理大规模键操作时,不妨思考:是否可以通过
SCAN
实现更优雅的解决方案?