为什么需要渐进式遍历key
有时候需要从 Redis 实例成千上万的 key 中找出特定前缀的 key 列表来手动处理数据,可能是修改它的值,也可能是删除 key。这里就有一个问题,如何从海量的 key 中找出满足特定前缀的 key 列表来?
Redis 提供了一个简单暴力的指令 keys 用来列出所有满足特定正则字符串规则的 key。
!redis-cli keys key67*
1) "key6764"
2) "key6738"
3) "key6774"
4) "key673"
5) "key6710"
6) "key6759"
7) "key6715"
8) "key6746"
9) "key6796"
这个指令使用非常简单,提供一个简单的正则字符串即可,但是有很明显的两个缺点。 没有 offset、limit 参数,一次性吐出所有满足条件的 key,万一实例中有几百 w 个 key 满足条件, 当你看到满屏的字符串刷的没有尽头时,你就知道难受了。
keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿, 所有读写 Redis 的其它的指令都会被延后甚至会超时报错, 因为 Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续。
建议生产环境屏蔽 keys 命令
scan
Redis 为了解决这个问题,它在 2.8 版本中加入了指令——scan。
scan 相比 keys 具备有以下特点:
- 复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程; 提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是对增量式迭代命令的一种提示 (hint),返回的结果可多可少;
- 同 keys 一样,它也提供模式匹配功能;
- 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
- 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
- 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
- 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零
scan 基础使用
SCAN cursor [MATCH pattern] [COUNT count]
初始执行 scan 命令例如 scan 0。SCAN 命令是一个基于游标的迭代器。 这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程。
当 SCAN 命令的游标参数被设置为 0 时,服务器将开始一次新的迭代,而当 redis 服务器向用户返回值为 0 的游标时, 表示迭代已结束,这是唯一迭代结束的判定方式,而不能通过返回结果集是否为空判断迭代结束。
scan 参数提供了三个参数,第一个是 cursor 整数值,第二个是 key 的正则模式,第三个是遍历的 limit hint。
第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。 一直遍历到返回的 cursor 值为 0 时结束。
!redis-cli scan 0 match key99* count 1000
1) "13912" # 返回的游标
2) 1) "key997"
2) "key9906"
3) "key9957"
4) "key9902"
5) "key9971"
6) "key9935"
7) "key9958"
8) "key9928"
9) "key9931"
10) "key9961"
11) "key9948"
12) "key9965"
13) "key9937"
!redis-cli scan 1