Redis SCAN vs KEYS:选择正确的Key遍历方式


Redis SCAN vs KEYS:深入剖析,选择正确的Key遍历方式

Redis,作为一款高性能的内存键值数据库,广泛应用于缓存、消息队列、会话管理等多种场景。在其日常运维和开发过程中,我们常常需要遍历数据库中的键(Key)以进行调试、分析、迁移或清理等操作。Redis 提供了两种主要的命令来实现这一目的:KEYSSCAN。然而,这两者在设计理念、性能表现和对生产环境的影响上存在着天壤之别。错误地选择遍历方式,尤其是在大规模数据集或高并发的生产环境中,可能会带来灾难性的后果。

本文将深入探讨 KEYSSCAN 命令的内部机制、优缺点、适用场景,并提供实践建议,帮助您在不同的需求下做出明智的选择,确保 Redis 服务的稳定性和高效性。本文篇幅较长,旨在提供全面而细致的分析。

一、 为何需要遍历 Redis 中的 Key?

在深入比较 KEYSSCAN 之前,我们先明确在哪些场景下会产生遍历 Key 的需求:

  1. 调试与问题排查: 当遇到预期之外的数据或行为时,开发者可能需要查看当前存在的 Key,特别是符合特定模式的 Key,以诊断问题根源。例如,检查某个用户相关的缓存 Key 是否都已正确设置或清理。
  2. 数据分析与统计: 需要对存储的数据进行概览性分析,比如统计符合特定前缀的 Key 的数量,了解某种类型数据的存储规模。
  3. 数据迁移与备份: 在进行 Redis 版本升级、实例迁移或特定数据导出时,可能需要获取所有或部分 Key 列表,然后逐一处理。
  4. 批量操作与清理: 需要对符合特定模式的 Key 执行批量删除、修改 TTL(生存时间)等操作。例如,清理不再使用的旧版本缓存 Key。
  5. 缓存预热: 在系统启动或更新后,主动加载某些 Key 到缓存中,可能需要先知道哪些 Key 需要被加载。
  6. 了解数据分布: 观察 Key 的命名模式和分布,有助于优化数据结构或发现潜在的设计问题。

尽管这些需求是真实存在的,但需要强调的是,频繁或大规模地遍历 Key 通常不是 Redis 的最佳实践。良好的 Key 设计(例如,使用 Hash、Set 等集合类型聚合相关数据)可以减少全库扫描的需求。然而,当遍历不可避免时,选择正确的方式至关重要。

二、 KEYS 命令:简单直接,但暗藏风险

KEYS 命令是最早提供的 Key 遍历方式,其语法非常简单:

KEYS pattern

它接受一个 Glob 风格的模式(pattern)作为参数,返回所有匹配该模式的 Key 列表。模式匹配支持以下通配符:

  • *:匹配任意数量的任意字符(包括零个)。例如 KEYS user:* 会匹配所有以 "user:" 开头的 Key。
  • ?:匹配单个任意字符。例如 KEYS session:?? 会匹配 "session:" 后面跟两个字符的 Key。
  • []:匹配方括号内的任意一个字符。例如 KEYS log:[abc] 会匹配 "log:a", "log:b", "log:c"。可以使用 - 表示范围,如 KEYS job:[0-9] 匹配 "job:0" 到 "job:9"。
  • \:用于转义特殊字符。

KEYS 的工作机制:

KEYS 命令的实现方式是一次性遍历整个 Redis 数据库的 Key 空间。它会迭代检查数据库中的每一个 Key,判断是否与提供的模式匹配。如果匹配,就将其添加到结果列表中。最后,将包含所有匹配 Key 的完整列表返回给客户端。

KEYS 的致命缺陷:阻塞性操作

KEYS 命令最核心、也是最危险的问题在于它的阻塞性。Redis 的命令处理是基于单线程模型的(注意:网络 I/O 和部分后台任务如持久化是多线程的,但核心命令执行是单线程的)。这意味着当 Redis 服务器正在执行一个命令时,它无法同时处理其他任何客户端请求。

执行 KEYS 命令时,Redis 服务器会:

  1. 开始遍历所有 Key。
  2. 对每个 Key 进行模式匹配。
  3. 将匹配的 Key 收集起来。
  4. 在整个遍历和匹配过程完成之前,Redis 不会响应任何其他命令。

这个过程的耗时与 Redis 数据库中的 Key 总数(N) 成正比,即时间复杂度为 O(N),而不是与匹配结果的数量成正比。即使你的模式只想匹配几个 Key,如果你的数据库里有数百万甚至数亿个 Key,KEYS 命令仍然需要检查每一个 Key。

阻塞带来的后果:

  • 服务停顿:KEYS 执行期间,所有其他客户端的请求都会被阻塞,无法得到响应。对于依赖 Redis 的应用来说,这会导致请求超时、用户体验下降,甚至服务完全不可用。
  • 高延迟: 即使 KEYS 执行时间不算太长(例如几百毫秒),对于需要低延迟响应的应用(如实时计算、高频交易),这种突发的延迟尖峰也是不可接受的。
  • 集群影响: 在 Redis Cluster 或 Sentinel 模式下,一个节点的长时间阻塞可能导致主从切换、脑裂等更复杂的问题,影响整个集群的稳定性。

KEYS 的性能考量:

除了阻塞性,KEYS 命令的性能开销也很大。遍历所有 Key 本身就是 CPU 密集型操作,模式匹配也会消耗计算资源。返回大量 Key 列表还需要占用可观的网络带宽和服务器、客户端内存。

何时可以(谨慎地)使用 KEYS

鉴于其严重的阻塞风险,强烈不推荐在生产环境中使用 KEYS 命令,尤其是在数据量较大或对服务可用性要求高的场景下。

那么,KEYS 是否完全没有用武之地?在以下非常有限且可控的情况下,可以极其谨慎地考虑使用:

  1. 开发和测试环境: 在数据量很小、没有真实用户访问的开发或测试环境中,KEYS 可以用于快速检查。但即使在这种情况下,养成使用 SCAN 的习惯也是更好的选择。
  2. 调试特定小问题: 当 Redis 实例非常小(例如只有几千个 Key),且你能接受短暂的阻塞时,可以用于临时调试。
  3. 明确的维护窗口: 在计划内的、通知用户的停机维护窗口期,且明确知道 KEYS 操作能在可接受的时间内完成。
  4. Redis 实例即将废弃: 在准备下线一个不再提供服务的 Redis 实例前,进行最后的数据检查。

总而言之,KEYS 命令就像一把双刃剑,虽然简单易用,但其潜在的阻塞风险使其在绝大多数生产场景下都是一个应该被严格禁止使用的命令。

三、 SCAN 命令:安全可靠的迭代式遍历

为了解决 KEYS 命令的阻塞问题,Redis 从 2.8 版本开始引入了 SCAN 命令及其家族(SSCAN, HSCAN, ZSCAN)。SCAN 提供了一种基于游标(cursor)的增量式迭代方式来遍历 Key 空间。

SCAN 的基本语法:

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

  • cursor:游标,用于标识迭代状态。第一次调用时,cursor 必须为 0。每次调用 SCAN 后,Redis 会返回一个新的 cursor,客户端需要在下一次调用时传入这个新的 cursor。当返回的 cursor 再次为 0 时,表示整个迭代过程完成。
  • MATCH pattern(可选):与 KEYS 类似,用于过滤 Key 的 Glob 风格模式。重要区别在于: SCAN 是先从数据库中获取一批 Key,然后对这批 Key 应用模式匹配。这意味着即使使用了 MATCHSCAN 仍然可能返回空的 Key 列表(但 cursor 不为 0),因为获取的那批 Key 可能都不匹配模式。
  • COUNT count(可选):提示 Redis 每次迭代期望返回的元素数量。这不是一个精确的限制,只是一个建议值。Redis 实际返回的数量可能会比 count 大或小。默认值通常是 10。增大 COUNT 值可以减少网络往返次数,但每次迭代的处理时间会相应增加。
  • TYPE type(可选,Redis 6.0+):只返回指定类型的 Key(如 "string", "list", "set", "zset", "hash", "stream")。这可以在服务器端进行类型过滤,减少返回给客户端的数据量。

SCAN 的工作机制:

SCAN 命令的核心思想是将一次完整的遍历分解成多次小的、独立的迭代步骤。

  1. 客户端发起第一次 SCAN 调用,cursor 设为 0
  2. Redis 服务器接收到请求,根据内部数据结构(通常是哈希表)和 cursor 的当前位置,扫描一小部分 Key 空间(数量受 COUNT 提示影响)。
  3. 对扫描到的 Key 应用 MATCH 模式(如果提供)和 TYPE 过滤(如果提供)。
  4. 将匹配的 Key 列表和下一个 cursor返回给客户端。这个过程非常快,通常不会阻塞服务器。
  5. 客户端接收到结果,处理返回的 Key 列表。
  6. 如果返回的 cursor 不为 0,客户端使用这个新的 cursor 发起下一次 SCAN 调用,重复步骤 2-5。
  7. 如果返回的 cursor0,表示所有 Key 空间(理论上)已经被遍历完毕。

SCAN 的主要优点:

  1. 非阻塞性: 这是 SCAN 相对于 KEYS 最根本的优势。每次 SCAN 调用只处理一小部分工作,执行时间很短,不会像 KEYS 那样长时间阻塞 Redis 服务器。在 SCAN 的多次调用之间,Redis 可以正常处理其他客户端的请求,保证了服务的可用性和低延迟。
  2. 增量迭代: 将大的遍历任务分解为小的、可管理的部分。客户端可以根据需要控制迭代的速度,甚至可以在两次迭代之间暂停。
  3. 适用于大规模数据集: 即使 Redis 中有数亿个 Key,SCAN 也能平稳地完成遍历,不会因为数据量过大而导致服务崩溃。
  4. 可控性: 可以通过 COUNT 参数影响每次迭代的工作量(虽然只是提示),并通过 MATCHTYPE 参数在服务器端进行初步过滤。

SCAN 的注意事项和潜在“陷阱”:

虽然 SCAN 非常安全,但使用时需要注意以下几点,这些是其非阻塞、增量迭代特性的自然结果:

  1. 可能返回重复的 Key:SCAN 遍历期间,如果 Redis 的哈希表发生了扩容或缩容(rehash),同一个 Key 可能在不同的迭代中被多次返回。因此,客户端需要自行处理重复的 Key,例如使用 Set 数据结构来存储结果。
  2. 可能漏掉 Key: 如果一个 Key 在 SCAN 开始迭代后被添加到 Redis,并且其添加的位置恰好是 SCAN 已经扫过的区域,那么这个 Key 可能会在本次完整的 SCAN 过程中被漏掉。反之,如果在迭代过程中 Key 被删除,也可能导致其不被返回。SCAN 不保证返回迭代开始时的精确快照。它提供的是“大部分” Key 的遍历,对于迭代期间发生的少量修改不提供强一致性保证。
  3. COUNT 参数只是提示: 不要依赖 COUNT 精确控制每次返回的数量。Redis 为了优化内部扫描,实际返回的数量可能与 COUNT 值有差异。特别是在使用了 MATCH 过滤后,或者哈希表桶内元素较少时,返回的 Key 数量可能远小于 COUNT 值,甚至为 0。
  4. 空结果不代表结束: 即使某次 SCAN 调用返回的 Key 列表为空,只要返回的 cursor 不为 0,就必须继续使用该 cursor 进行下一次迭代,直到 cursor 变为 0 为止。
  5. 遍历顺序不确定: SCAN 返回 Key 的顺序是不确定的,与插入顺序或字典序无关。
  6. 多次网络往返: 完成一次完整的遍历需要多次 SCAN 调用,这意味着会产生多次网络往返的开销。相比 KEYS 的一次调用,网络延迟累积可能会更长,但这是为了换取服务器不被阻塞。

SCAN 家族:SSCAN, HSCAN, ZSCAN

除了遍历数据库级别的 Key (SCAN),Redis 还提供了针对特定集合类型内部元素遍历的命令:

  • SSCAN cursor [MATCH pattern] [COUNT count]:遍历 Set 集合中的元素。
  • HSCAN key cursor [MATCH pattern] [COUNT count]:遍历 Hash 字典中的 field-value 对。
  • ZSCAN key cursor [MATCH pattern] [COUNT count]:遍历 Sorted Set 中的 member-score 对。

这些命令的工作原理与 SCAN 完全相同,都是基于游标的增量迭代,用于安全地遍历大型 Set、Hash 或 Sorted Set,避免一次性加载所有元素导致内存和性能问题。

四、 性能对比:KEYS vs SCAN

特性 KEYS pattern SCAN cursor [MATCH pattern] [COUNT count]
阻塞性 是 (阻塞整个实例) 否 (每次迭代阻塞时间极短)
复杂度 O(N),N 为总 Key 数 每次迭代 O(1) (摊销),总复杂度仍与 N 相关
安全性 低 (生产环境危险) 高 (生产环境推荐)
迭代方式 一次性返回所有匹配结果 增量式,基于游标
内存占用 可能返回巨大列表,占用服务器/客户端内存 每次返回少量结果,内存占用低
网络开销 单次请求/响应 多次请求/响应
结果保证 返回精确快照(阻塞期间) 不保证快照,可能重复或遗漏(非阻塞)
适用数据量 只适用于极小数据集 适用于任意大小数据集
命令家族 SCAN, SSCAN, HSCAN, ZSCAN

总结:

  • KEYS性能风险极高。其阻塞特性使其在任何有一定数据量或对可用性有要求的生产环境中都应被禁用。时间消耗与总 Key 数相关,与匹配结果数无关。
  • SCAN性能友好且安全。通过增量迭代避免了长时间阻塞。虽然完成整个遍历的总工作量与 KEYS 类似,但它将负载分散到多次短暂的操作中,对 Redis 服务的稳定性影响极小。需要客户端处理重复项和多次网络往返。

五、 如何选择:决策框架与实践建议

基于以上的详细分析,选择 KEYS 还是 SCAN 的决策通常非常明确:

选择 SCAN 的场景(绝大多数情况):

  • 所有生产环境: 无论是线上服务、核心业务支撑的 Redis 实例,只要对服务的稳定性和可用性有要求,必须使用 SCAN
  • 数据量较大或不确定的环境: 即使在开发或测试环境,如果数据量可能增长到一定规模,或者你不确定数据量大小,优先选择 SCAN
  • 需要遍历大型集合内部元素: 使用 SSCAN, HSCAN, ZSCAN
  • 需要后台、异步处理 Key: SCAN 的增量特性非常适合构建后台任务,可以分批处理 Key,并控制处理速率。
  • 脚本或自动化任务: 在脚本中进行 Key 遍历时,SCAN 是更健壮、更安全的选择。

选择 KEYS 的场景(极其罕见且需谨慎评估):

  • 交互式调试小型实例: 在你完全控制的、Key 数量极少(例如 < 1000)的本地开发实例上,为了快速查看几个 Key,或许可以使用 KEYS。但即使如此,熟悉 SCAN 的用法并坚持使用是更好的习惯。
  • 一次性、离线的分析任务: 如果你能将 Redis 实例下线,或者复制一份数据到单独的分析环境,并且确信 KEYS 能在可接受时间内完成,那么它的简单性可能有点吸引力。但通常离线分析也有更好的工具。

实践建议:

  1. 默认禁止 KEYS 在生产环境中,考虑通过 Redis 的 rename-command 配置将 KEYS 命令重命名为一个复杂或空字符串,从而禁用它,防止误用。
    redis.conf
    rename-command KEYS ""
    # 或者 rename-command KEYS "DO_NOT_USE_KEYS_HERE_lkjhasdflkjh"
  2. 封装 SCAN 逻辑: 编写或使用库函数来封装 SCAN 的循环、游标处理和结果收集(包括去重)逻辑。这能简化应用层代码。大多数现代 Redis 客户端库都提供了 SCAN 的迭代器或类似抽象。

    • Python (redis-py):
      ```python
      import redis

      r = redis.Redis(decode_responses=True)

      Basic SCAN loop

      cursor = '0'
      all_keys = set() # Use a set for automatic deduplication
      while cursor != 0:
      cursor, keys = r.scan(cursor=cursor, match='user:*', count=100)
      for key in keys:
      all_keys.add(key)
      # Optional: Add a small sleep to reduce load further
      # time.sleep(0.01)

      print(f"Found {len(all_keys)} matching keys.")

      Using the built-in iterator (recommended)

      for key in r.scan_iter(match='product:*', count=500):
      # Process each key
      print(key)
      # The iterator handles cursor management and potentially deduplication
      # depending on the library version and implementation. Check docs.
      ```

  3. 合理设置 COUNT COUNT 的值需要在单次迭代效率和网络往返次数之间取得平衡。一般建议从 100 到 1000 开始尝试,根据实际情况调整。太小会增加网络开销,太大则可能轻微增加单次迭代的延迟(但仍远好于 KEYS)。

  4. 客户端去重: 始终假设 SCAN 可能返回重复 Key,并在客户端逻辑中进行去重处理,除非你使用的库明确保证了去重。
  5. 考虑网络延迟: 对于延迟敏感的应用,SCAN 的多次往返可能是个问题。如果需要非常快速地获取大量 Key,可能需要审视业务设计,看是否能避免全库扫描,或者采用其他策略(如维护专门的索引 Set)。
  6. 监控 SCAN 操作: 长时间运行的 SCAN 循环(即使是安全的)仍然会消耗服务器资源和网络带宽。监控其执行情况,并考虑在业务低峰期执行大规模扫描任务。
  7. TYPE 过滤 (Redis 6.0+): 如果你只关心特定类型的 Key,使用 TYPE 参数可以显著减少网络传输的数据量和客户端处理负担。

六、 结论

Redis 的 KEYSSCAN 命令都提供了遍历 Key 的能力,但它们的设计哲学和对系统稳定性的影响截然不同。KEYS 命令以其简单性诱惑用户,但其固有的阻塞特性使其成为生产环境中的一颗定时炸弹,极易引发服务中断。除非在极其特殊且严格控制的场景下,否则应坚决避免使用 KEYS

SCAN 命令及其家族(SSCAN, HSCAN, ZSCAN)是 Redis 官方推荐的、安全可靠的遍历方式。通过基于游标的增量迭代,SCAN 避免了长时间阻塞服务器,保证了 Redis 服务的高可用性和性能。虽然它在使用上比 KEYS 稍微复杂一些(需要处理游标、潜在的重复项和多次网络往返),但这些代价是为了换取系统稳定性而完全值得付出的。

掌握 SCAN 的正确用法,理解其工作机制和注意事项,是每一个 Redis 使用者和运维人员必备的技能。在需要遍历 Redis Key 或集合元素时,请始终优先选择 SCAN 及其家族,为您的应用保驾护航。选择正确的工具,不仅关乎效率,更关乎服务的生命线。


THE END