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通过以下步骤工作:

  1. 根据游标确定当前扫描的哈希表索引
  2. 在该哈希表中按步长遍历
  3. 当前哈希表遍历完成后切换到下一个表

关键点

  • 非阻塞:每次仅扫描少量键
  • 概率覆盖:通过随机步长降低哈希碰撞概率
  • 最终一致性:可能因并发操作漏掉新键或重复旧键

三、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是概率性遍历,可能出现以下情况:

  • 漏扫:新创建的键可能未被本次遍历捕获
  • 重复:键被删除后可能再次出现在后续扫描中

解决方案

  1. 结合EXPIRE机制定期清理
  2. 使用UNLINK代替DEL减少阻塞
  3. 对关键操作添加二次校验

五、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%覆盖?

理论上无法完全保证,但可通过以下方法提高可靠性:

  1. 缩短遍历间隔
  2. 结合AOF重放机制
  3. 使用SCAN_ITER变体命令(如HSCAN

结论:拥抱渐进式遍历的新时代

SCAN命令的诞生标志着Redis在高并发场景下实现了键遍历的范式转移。通过理解其游标机制、参数配置和典型应用场景,开发者可以有效避免服务阻塞,构建更健壮的分布式系统。

在实际项目中,建议将SCAN与业务逻辑深度结合:例如在用户活跃期外执行数据清理,在低峰时段进行数据迁移。随着Redis 7.0版本对渐进式命令的持续优化,这种非阻塞的遍历方式必将成为分布式系统设计的基石。

提示:当需要处理大规模键操作时,不妨思考:是否可以通过SCAN实现更优雅的解决方案?

最新发布