在 2.8 版本之前, redis 并没有一个能够低成本全局迭代遍历所有键的命令, 而唯一具有类似能力的 keys 命令则在当需要扫描的结果集数量庞大时面临巨大的内存压力和耗时压力;
redis 2.8 的新推出的 scan 命令刚好解决了这个问题;

scan 的适用场景

如果要在 redis 中找出指定前缀的 keys, 有一个最直接简单的命令: keys, 但是如果符合匹配条件的 key 有数万之多, keys 命令将存在致命缺陷:

  • keys 命令没有 limit 和 offset, 意味着一次请求要将所有的数据都返回, 对内存压力过大;
  • keys 命令实现是遍历匹配, 复杂度是 O(n), 如果 key 的数量过大, 由于 redis 是单线程执行, 实例会 hang 住;

redis 为了解决这个问题, 在 2.8 版本中加入了指令: scan:

  • 通过游标迭代实现 O(n) 分段查询, 不会阻塞 redis 主线程;
  • 每次迭代 redis 自动返回下一次迭代的游标, 用户只需用 redis 返回的 index 持续遍历直到返回的游标为 0 为止;

使用方式

1
2
3
4
5
6
7
8
# 遍历整个实例
SCAN cursor [MATCH pattern] [COUNT count]
# 遍历指定集合
SSCAN setName cursor [MATCH pattern] [COUNT count]
# 遍历指定有序集合
ZSCAN zsetName cursor [MATCH pattern] [COUNT count]
# 遍历指定哈希键值对
HSCAN hashMap cursor [MATCH pattern] [COUNT count]

举例:
第一次遍历:

1
2
3
4
5
> scan 0 match my-prefix* count 1000

1) "139"
2) 1) "my-prefix01"
2) "my-prefix02"

第二次遍历:

1
2
3
4
5
> scan 139 match my-prefix* count 1000

1) "512"
2) 1) "my-prefix03"
2) "my-prefix04"

第三次遍历:

1
2
3
4
> scan 512 match my-prefix* count 1000

1) "0"
2) 1) "my-prefix05"

scan 的实践

big key 扫描

redis-cli 提供的 --bigkeys 参数, 背后使用了 scan 去做 big key 的扫描;

1
2
# 每隔 100 条 scan 指令休眠 0.1s
redis-cli --bigkeys -i 0.1

bigkeys 的逻辑:

  • 如果是 string 类型, 通过 strlen 判断;
  • 如果是 list 类型, 通过 llen 判断;
  • 如果是 hash 类型, 通过 hlen 判断;
  • 如果是 set 类型,通过 scard 判断;
  • 如果是 sorted set 类型, 通过 zcard 判断;

对于 string 类型, bigkeys 命令给出的结论一定是准确的, 但是其他四种集合类型, 因为仅仅是通过 size 判断而未比较具体成员的 length, 可能给出的结论不一定准确;

参考链接