Redis Scan 原理解析与踩坑
1. 概述
由于 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 - 指定每次遍历多少个集合。
- 可以简单理解为每次遍历多少个元素
- 根据测试,推荐 Count大小为 1W。
Scan 返回值为数组,会返回一个游标+一系列的 Key
大致用法如下:
SCAN命令是基于游标的,每次调用后,都会返回一个游标,用于下一次迭代。当游标返回0时,表示迭代结束。
第一次 Scan 时指定游标为 0,表示开启新的一轮迭代,然后 Scan 命令返回一个新的游标,作为第二次 Scan 时的游标值继续迭代,一直到 Scan 返回游标为0,表示本轮迭代结束。
通过这个就可以看出,Scan 完成一次迭代,需要和 Redis 进行多次交互。
Scan 命令注意事项:
- 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
- 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
- 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;
2. 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 会快很多,毕竟一次性处理,省去了多余的交互。
3. Scan原理
Redis使用了Hash表作为底层实现,原因不外乎高效且实现简单。类似于HashMap那样数组+链表的结构。其中第一维的数组大小为2n(n>=0)。每次扩容数组长度扩大一倍。
Scan命令就是对这个一维数组进行遍历。每次返回的游标值也都是这个数组的索引。Count 参数表示遍历多少个数组的元素,将这些元素下挂接的符合条件的结果都返回。因为每个元素下挂接的链表大小不同,所以每次返回的结果数量也就不同。