在日常的线上 Redis 维护工作中,面对成千上万的 key 需要找出特定前缀的 key 列表时,使用 Redis 的 keys 命令显得过于直接而缺乏效率。keys 命令虽简单,但存在明显的两个缺点。为解决这一问题,Redis 在 2.8 版本中引入了 scan 命令,以更高效地筛选出满足特定前缀的 key 列表。本文将深入探讨 scan 命令的基础使用、字典的结构、遍历顺序,以及字典扩容与缩容对遍历顺序的影响,并将对大 key 的扫描与处理进行解析。首先,通过在 Redis 中插入 10000 条数据进行测试,我们可以直观地体验到 scan 命令的强大之处。例如,使用 scan 命令找出以 key99 开头的 key 列表。scan 命令提供 cursor、正则模式和遍历 limit hint 三个参数。第一次遍历时,cursor 为 0,返回结果中的第一个整数值作为下一次遍历的 cursor。遍历直到返回的 cursor 值为 0 结束。尽管 limit 参数设置为 1000,但返回的结果通常只有 10 个左右,因为 limit 参数并不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量。在 Redis 中,所有的 key 存储在一个类似 Java 中 HashMap 的一维数组 + 二维链表结构的字典中,数组大小总是 2^n(n>0),且在扩容时数组空间会加倍。scan 指令返回的游标对应字典中的一维数组位置索引,即槽 (slot)。遍历顺序采用高位进位加法,避免了遍历重复和遗漏。当字典扩容或缩容时,元素会被重新哈希到新位置,但采用高位进位加法的遍历顺序能有效避免对已经遍历过的槽位进行重复遍历。Java 中的 HashMap 有扩容的概念,当 loadFactor 达到阈值时,需要重新分配一个两倍大小的数组,并将所有元素重新哈希挂到新数组下面。rehash 过程中,元素的 hash 值对数组长度进行取模运算,等价于位与操作。字典的 mask 值用于保留 hash 值的低位,高位被设置为 0。rehash 前后元素槽位的变化影响了遍历顺序,采用高位进位加法的遍历顺序在 rehash 后的槽位上是相邻的。Redis 采用渐进式 rehash 来解决 HashMap 在扩容时可能出现的卡顿问题,它会在定时任务以及后续对 hash 的指令操作中逐步迁移旧数组中的元素到新数组上。这意味着在 rehash 过程中操作字典时,需要同时访问新旧数组结构,避免对线上 Redis 造成卡顿影响。scan 指令同样需要考虑这个问题,它需要同时扫描新旧槽位,然后将结果融合后返回给客户端。除了遍历所有的 key,scan 命令还支持对特定容器集合进行遍历,如 zscan 遍历 zset 集合元素、hscan 遍历 hash 字典的元素、sscan 遍历 set 集合的元素。这些指令的原理相似,因为底层都是基于字典实现的。对于大 key 的扫描与处理,scan 命令提供了便利的工具,通过脚本或利用 Redis 官方提供的 redis-cli 指令功能,可以高效地定位并处理大 key,避免对 Redis 集群数据迁移和内存分配带来的影响。为减少指令对 Redis ops 的影响,可以增加休眠参数以控制扫描频率,避免线上报警。