bigywater.tech
Redis · 118

SCAN 为什么不会一次阻塞

SCAN 为什么不会一次阻塞很久?它到底保证什么、不保证什么?这篇在 100 万个 key 的库上把它的耗时、遍历时库在变化的行为、以及 MATCH 的过滤时机都量出来。

1.1 vs 95 ms
SCAN COUNT 1000 遍历期间 vs KEYS * 期间,另一个连接 PING 的最大停顿(100 万个 key)
200000/200000
遍历中大量新增 key 时,开始前就在的 key 被遍历到的个数(保证全部出现)
498/500
MATCH 只匹配 2 个 key 时,返回空数组的调用次数

游标

实测 · 本机执行输出
  前 8 次调用的 (传入游标, 返回 key 数): [('0', 11), ('589824', 10), ('98304', 11), ('491520', 10), ('212992', 11), ('442368', 11), ('598016', 10), ('565248', 10)]
  一次完整遍历(COUNT 10 时前 8 次的游标值)是: ['0', '589824', '98304', '491520', '212992', '442368', '598016', '565248'] ——不是 0,10,20…,而是按「二进制位倒序」推进

每次返回的游标不是「偏移量」,前几次依次是 0、720896、425984、671744……;返回的 key 数也不严格等于 COUNT(11、10、12、10……,COUNT 只是提示)。调用方唯一要做的是:把返回的游标原样传给下一次调用,直到游标是 0。

代价:每次调用很短

实测 · 本机执行输出
命令                          单次耗时 ms             完成一轮遍历需要调用次数         另一个连接 PING 最大停顿 ms
SCAN COUNT 10                0.038                    95445                       (未测)
SCAN COUNT 100               0.105                     9952                       (未测)
SCAN COUNT 1000              0.782                     1000                       1.11
SCAN COUNT 10000             7.113                      100                      12.59
KEYS *                        660.7                        1                      94.87

单次耗时随 COUNT 线性增长;COUNT 1000 时 0.78 ms,期间另一个连接的 PING 最大停顿 1.1 ms。KEYS * 一次性遍历整个库:661 ms(含传输),PING 被卡 95 ms(第 119 篇专门量 KEYS 的风险)。COUNT 不宜太大:COUNT 10000 时单次 7.1 ms,PING 停顿已经到 12.6 ms。

遍历时库在变化

实测 · 本机执行输出
  开始前的 20 万个 a:* key:遍历到 200000 个(应为 200000);遍历过程中新增了 400000 个 b:* key,遍历到其中 386583 个;返回的 key 总数 586583,其中重复 0 个

SCAN 的保证是:从开始到结束一直存在的 key 一定会被返回至少一次。实测边遍历边新增 40 万个 key(库从 20 万涨到 60 万,字典会扩容),开始前的 20 万个 key 一个不少,新增的 key 只有 386583 个被遍历到——新增的不保证出现。文档还说 SCAN 可能返回重复的 key,这次实验里重复是 0 个;所以调用方仍然要做好去重的准备。

MATCH 的过滤时机

实测 · 本机执行输出
  50 万个 key、只有 2 个匹配:遍历完需要 500 次调用,其中 498 次返回空数组,共找到 2 个

MATCH 不会加速遍历:服务器先按游标取出一批,再对这一批过滤。匹配很少时,大部分调用返回空数组(500 次里 498 次)。判断遍历是否结束只能看游标是否为 0,不能看返回数组是否为空。

分步演示:SCAN 的三件事

SCAN 为什么不会一次阻塞
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Redis 8.10.2 独立实例(本机回环端口,save 关闭),redis-cli 风格输出由自写的 RESP 客户端打印;计时用 time.perf_counter,数字每次运行会有小幅抖动。单实例 100 万个 key(值 v);PING 探测每 0.5 ms 一次;遍历中新增的 key 用管道写入
    • 没有验证:重复 key 在字典缩容/rehash 时出现的条件、SCAN 的 TYPE 参数、集群下的 SCAN(每个节点各自遍历)、SSCAN/HSCAN/ZSCAN 对小集合的一次性返回
    京ICP备2026067502号-1京公网安备11010502063487号