资讯动态

Redis百万key模糊查找:KEYS与SCAN原理、调优与避坑

发布时间:2026/10/26 15:41:26 来源:尧图企业网站定制
在百万 keys 的 Redis 实例里做模糊查找是那种一句话能问出口、真动手处处是坑的活儿。核心就两个命令KEYS和SCAN。前者是所有人脑子里的第一反应KEYS *order*pending*敲下去本地测试秒出结果感觉非常顺手后者是真正能放到线上跑的方案但它有一堆反直觉的细节——游标不是页码、返回结果可能重复、COUNT只是提示、集群下默认扫不全。我处理过好几次线上缓存实例卡了三十秒的排查最后一查都是有人图省事在业务代码里调了KEYS。所以这篇东西不打算写成命令手册而是按真实场景的顺序讲先搞清楚为什么不能随便KEYS再看SCAN的机制到底特殊在哪然后给出可以直接抄走的脚本、参数区间和排查清单。不管你是刚接手 Redis 的新手还是想给自己团队定个规范的老手应该都能拿走点能用上的东西。1. 百万 key 下模糊查找难的不是找而是不打扰1.1 KEYS 的直觉陷阱KEYS的语义极其简单给定一个 glob 模式遍历整个数据库把所有匹配的 key 一次性返回。写代码的时候一行就完事redis-cli keys *order:pending*敲下去也有反馈所以很多人第一次接触 Redis 模糊查询就是用它然后就再也没换过。问题在于它的执行方式——Redis 处理命令是在主线程上串行执行的一条命令没跑完后面排队的几千条命令全部等着。KEYS要做的事情是把整个字典从头到尾走一遍时间复杂度是 O(N)N 就是当前库里的 key 总数。百万级 key 意味着什么假设平均每个 key 三十来个字节字典里一百万个 entry一次完整遍历在主频正常的机器上大概要几十毫秒到一两百毫秒这还是在 key 比较短、value 比较小的情况下。如果这个实例里混着一些几百 KB 的大 value遍历过程中的内存访问开销会明显上升耗时往几百毫秒甚至更久走。更麻烦的是返回结果的构造KEYS会把所有匹配项拼成一个大数组一次性回给客户端如果模式写得宽比如*order*命中了二十万条这个响应体本身就有十几 MB服务端的输出缓冲区会迅速膨胀触发client-output-buffer-limit之后连接直接被断开客户端看到的是一句语焉不详的连接重置。所以KEYS的问题从来不是能不能查出结果而是查的这几十毫秒里整个实例都在为它让路。对一个 QPS 几千的缓存实例来说一次两百毫秒的阻塞意味着这段时间内所有请求全部堆积超时、熔断、雪崩可能就跟着来了。这也是为什么几乎所有 Redis 运维规范里都有同一条生产环境禁用KEYS。不是它不好用是它的成本模型和在线服务不兼容。1.2 SCAN 把一次深呼吸拆成若干次小喘气SCAN解决的就是这个一口气憋太久的问题。它的思路很朴素不要求一次把全库走完而是每次调用只走一小段返回一小批结果和一个新的游标你拿着游标再调下一次直到游标回到 0 表示遍历结束。整趟下来总工作量还是 O(N)但被摊薄到了几十上百次调用里每次只占用主线程很短的一小段时间中间还能插进去处理正常请求。这个设计带来的第一个直接后果是SCAN不是快照。它不保证在遍历期间数据不变你在扫描过程中新写入的 key 可能被扫到也可能不被扫到被删除的 key 可能出现在结果里也可能不出现。这对我要统计全库某个前缀的数量这类需求是有影响的得接受结果是一个近似值。第二个后果是SCAN可能返回重复元素。字典在扩容或者缩容rehash的时候元素的桶位置会变化游标推进的过程中同一个元素有机会被访问两次。这不是 bug是增量遍历在动态数据结构上必须付出的代价官方文档写得很清楚去重得客户端自己做。理解了这两点很多为什么我扫出来的结果对不上的疑惑就自然消解了。SCAN给你的是尽量完整、允许重复、不保证一致的遍历能力用它来做模糊查找、批量清理、数据盘点都是够的但你不能拿它当SELECT用更不能指望它给出精确的实时总数。1.3 两个命令的能力边界对照把两者的差异拉成一张表会更清楚。我在给团队做内部分享的时候一般就用这张表打头让大家先建立什么场景该用哪个的直觉再去抠细节。维度KEYSSCAN单次执行耗时O(N)随 key 总量线性增长单次 O(1) 均摊整体 O(N)对主线程影响全程阻塞期间无法处理其他命令每次只短暂占用可与其他命令交错返回方式一次性返回全部匹配结果分批返回靠游标推进一致性保证执行瞬间的完整视图无快照遍历期间数据可变结果重复不会重复可能重复需客户端去重内存压力结果集全部驻留内存峰值仅为单批结果生产可用性基本不可用可用需控制频率与批量典型用途本地调试、极小的库、离线工具线上模糊查找、批量迁移、盘点表格里结果重复那一栏值得多说一句。很多人第一次看到重复结果会怀疑是自己代码写错了其实大概率是正常的。判断方法很简单把匹配结果收集到一个set里如果len(set)比len(list)小而两次运行的差值不大那基本就是 rehash 导致的重复属于预期行为。真正需要警惕的是重复量异常大那可能是扫描期间写入特别频繁或者COUNT设得过大导致单批扫描跨越了多次 rehash。2. 把机制拆开看KEYS 与 SCAN 到底怎么跑的2.1 KEYS 的遍历路径与阻塞面Redis 的键空间本质上是一个哈希表用拉链法解决冲突每个槽位挂着一个链表或者经过优化的紧凑结构。KEYS做的事情就是从这个哈希表的第一个槽位开始逐个槽位往下走对每个槽位里的每个 key 做一次 glob 模式匹配命中就塞进结果数组。整个过程没有任何让步的机制主线程一直在这个循环里事件循环里的其他就绪事件全部得等它回来。这里有个容易被忽略的点KEYS的开销不只是遍历本身还包括模式匹配。glob 匹配虽然是线性扫描字符串但模式里如果有多个*回溯的代价会上升。比如*a*b*c*d*这种模式在最坏情况下匹配成本会明显高于单个前缀匹配。所以哪怕是在允许KEYS的小库上也应该尽量把模式写得具体前缀明确的就用order:pending:*别动不动就*pending*。另外KEYS执行期间如果正好碰到字典在 rehashRedis 用的是渐进式 rehash平时会把 rehash 的工作分摊到各个操作里遍历逻辑要同时处理两张表成本会更高。这也是为什么线上偶发的KEYS卡顿有时比预估的还严重——不是 key 变多了是正好撞上了扩容窗口。2.2 SCAN 的游标不是偏移量这是SCAN最容易被误解的地方我见过不少人把它当成分页用觉得游标 1000 就意味着跳过了一千个元素然后拿它去做分页展示结果数据全乱。事实上SCAN返回的游标是哈希表桶索引经过位反转之后的值它标记的是下一轮从哪个桶开始扫跟元素序号没有任何关系。为什么要做位反转因为哈希表的桶数量通常是 2 的幂扩容和缩容都是翻倍或减半。如果游标直接是桶索引那么在扩容发生时原来的桶会分裂成两个已经扫过的位置语义就变了容易漏掉元素。位反转之后高位的变化对应的是低位扩容时新分裂出来的桶恰好落在游标的已扫过侧从而保证不漏代价是可能重复。这是SCAN能用一组合适的语义在动态字典上工作的关键设计。由此可以推出几个实用结论。第一游标不能用来做分页想要分页就自己维护页码和偏移。第二游标本身没有业务含义不要存下来做上次扫到哪的业务标记虽然技术上可以存下来做断点续扫但在数据变动剧烈时续扫的语义会变弱。第三游标的数值可能很大它是一个 64 位无符号整数看到几亿的游标值不要惊讶那是位反转后的桶索引不是 key 的数量。2.3 MATCH、COUNT、TYPE 三个参数的语义SCAN从 Redis 2.8 开始提供最初只有cursor和可选的MATCH、COUNTRedis 6.0 又加了TYPE。这三个参数经常被误用值得逐个说清楚。MATCH是在服务端对每一批返回结果做过滤注意是每批不是全库索引。这意味着MATCH不减少扫描的工作量它只减少返回给你的数据量。哪怕你的模式只命中三条服务端照样要把百万个 key 全走一遍只是最后传回客户端的只有那三条。所以别指望加了MATCH就变快它省的是网络带宽和客户端内存不是 CPU。COUNT是一个提示值不是硬性上限。你写COUNT 1000服务端会尽量每轮扫描大约一千个桶但实际返回的元素数可能远小于这个数桶稀疏的时候也可能略多一点。它的作用是控制单次调用的工作量从而控制单次阻塞时长。默认值是 10这个默认值对于本地调试还行对百万级实例来说太保守了会导致往返次数过多。TYPE是服务端按数据类型过滤只返回指定类型的 key比如SCAN 0 TYPE hash。它的成本比MATCH低一些因为类型判断是读对象头不需要跑 glob。在实际排查里如果你知道要找的是 hash 类型加上TYPE hash能显著减少返回量也就顺带减少了后续处理成本。3. 上手实操从百万 key 里捞出目标那几条3.1 命令行先摸底DBSIZE 与 INFO keyspace动手扫之前先摸清规模这一步很多人跳过结果扫到一半才发现库里有八百万 key脚本跑了半小时。DBSIZE返回当前库的 key 总数一条命令的事。INFO keyspace更细一些会按 db 列出 key 数量、带过期时间的 key 数量、以及平均存活时间。redis-cli -h 127.0.0.1 -p 6379 DBSIZE redis-cli -h 127.0.0.1 -p 6379 INFO keyspaceINFO keyspace的输出大概长这样# Keyspace db0:keys1048576,expires983040,avg_ttl86400000这里的expires值得留意。如果一个库里有大量设置了过期时间的 key直接用SCAN遍历时那些逻辑上已经过期但还没被惰性删除的 key 可能会出现。Redis 的过期删除是惰性 定期抽样结合的方式过期但未被访问的 key 会一直待在字典里直到被采样到或者被访问到。所以DBSIZE给的数量和SCAN实际能扫到的数量之间可能有差异这是正常的别慌。如果你是在排查资源占用问题还可以顺手看一眼MEMORY USAGE和INFO memory确认一下是不是有超大 value 拖慢了遍历。不过这些属于附加信息不是模糊查找的必要步骤。3.2 redis-cli --scan 快速验证确认了规模之后最快的验证手段是redis-cli自带的--scan参数。它在客户端侧封装了SCAN的循环你不用自己管游标。redis-cli -h 127.0.0.1 -p 6379 --scan --pattern *order:pending* --count 1000注意--count这个参数--scan模式下它的默认值也是 10如果不显式指定一百万 key 会被拆成十万次往返本地跑可能还好跨机房跑就是一个灾难。指定一千左右能明显压缩往返次数。这个命令适合两种情况一是快速确认模式有没有写对能不能命中预期的 key二是把结果重定向到文件做离线分析。redis-cli --scan --pattern session:* --count 2000 /tmp/session_keys.txt wc -l /tmp/session_keys.txt不过它有两个明显的局限一是没有去重输出文件里可能有重复行需要自己sort -u二是没法限速跑起来就是全速对线上实例还是有一定压力的。所以它更适合只读副本或者低峰期使用真正要跑大批量任务还是得写脚本。3.3 Python 脚本scan_iter 去重、限速、断点真正落到生产环境我一般用 Python 写一个小脚本因为要处理去重、限速、断点续扫这些事。redis-py提供了两种方式手动scan循环或者直接用scan_iter生成器。后者更省心但前者控制力更强。手动循环的写法是这样的import time import redis r redis.Redis( host127.0.0.1, port6379, db0, decode_responsesTrue, socket_timeout5, socket_connect_timeout3, ) PATTERN *order:pending* COUNT 1000 SLEEP 0.01 cursor 0 seen set() matched [] rounds 0 while True: cursor, batch r.scan(cursorcursor, matchPATTERN, countCOUNT) rounds 1 for key in batch: if key in seen: continue seen.add(key) matched.append(key) if rounds % 50 0: print(fround{rounds}, cursor{cursor}, matched{len(matched)}) if cursor 0: break time.sleep(SLEEP) print(fdone. rounds{rounds}, unique_matched{len(matched)})这段代码里有几个刻意的设计。decode_responsesTrue让返回的 key 是字符串而不是字节串方便后续处理但如果你的 key 里可能有二进制内容就应该关掉它用bytes自己编码处理。seen集合承担去重职责这是必须的不能省。SLEEP是限速每轮之间睡十毫秒看起来微不足道但对线上实例来说能明显削平 CPU 毛刺。用scan_iter的话代码短很多for key in r.scan_iter(match*order:pending*, count1000): matched.append(key)它内部就是SCAN循环的封装游标推进和终止判断都帮你做了但不去重这点要特别注意。如果你的场景对重复敏感还是得自己套一层set。断点续扫的写法是在循环里把游标和已处理状态落盘import json STATE_FILE /tmp/scan_state.json def load_state(): try: with open(STATE_FILE) as f: return json.load(f) except FileNotFoundError: return {cursor: 0, rounds: 0} def save_state(state): with open(STATE_FILE, w) as f: json.dump(state, f)每隔若干轮调用一次save_state脚本被 kill 或者超时中断之后下次从记录的游标继续。这里要说清楚中断期间如果数据发生了大量增删续扫的结果完整性会打折扣因为游标对应的桶索引语义在 rehash 后可能已经变了。所以断点续扫更适合数据基本静止的盘点场景对高频写入的库不建议依赖它做精确统计。3.4 集群环境逐节点扫描单机模式下SCAN就够用了但集群模式cluster下有个大坑SCAN只能扫当前连接的节点它不会自动帮你遍历整个集群。一百万个 key 分布在三个主节点上你在其中一个节点上扫最多只能扫到三分之一。redis-py的RedisCluster对象提供scan_iter内部会遍历所有主节点用起来比较省事from redis.cluster import RedisCluster rc RedisCluster( host127.0.0.1, port7000, decode_responsesTrue, skip_full_coverage_checkTrue, ) all_keys set() for key in rc.scan_iter(match*order:pending*, count1000): all_keys.add(key) print(len(all_keys))不过生产环境里我更倾向自己控制显式拿到节点列表再逐个扫这样能控制每个节点的扫描节奏也方便定位问题from redis.cluster import RedisCluster import redis rc RedisCluster(host127.0.0.1, port7000, decode_responsesTrue) result {} for node in rc.get_nodes(): if node.server_type ! primary: continue conn redis.Redis( hostnode.host, portnode.port, decode_responsesTrue, socket_timeout5, ) keys set() cursor 0 while True: cursor, batch conn.scan(cursorcursor, match*order:pending*, count1000) keys.update(batch) if cursor 0: break result[f{node.host}:{node.port}] keys print(node.host, node.port, len(keys)) total set().union(*result.values()) print(total unique:, len(total))这段代码的关键在于每个节点单独建连接、单独维护游标、单独限速。如果你图省事用一个连接串在集群里到处SCANredis-cli的重定向机制会把你转到别的节点上去游标就串了结果完全不可靠。这一点在排查为什么扫出来的 key 少了一半的时候特别常见八成就是没逐节点扫。3.5 结果落盘与二次校验扫出来的 key 不要直接拿去做删除或者批量修改先落盘再校验。原因有两个一是SCAN的结果允许重复也允许在遍历期间出现数据的增删直接拿去做批量操作有可能操作到不该动的 key二是落盘之后可以做交叉验证比如用另一套方式确认这些 key 是否真的符合你的业务规则。落盘我一般用两种格式纯文本一行一个 key方便丢给xargs做批量处理或者 JSON Lines带上扫描时间、来源节点、游标位置方便回溯。sort -u /tmp/scan_raw.txt /tmp/scan_unique.txt wc -l /tmp/scan_raw.txt /tmp/scan_unique.txt这两行输出能直接告诉你重复率。如果原始二十万行去重后只剩十五万行重复率四分之一那说明扫描期间写入非常频繁建议换个时间窗口重扫一次做对比。二次校验的方式取决于你的目的。如果是清理脏数据可以抽样取几十个 key 用TYPE和TTL看看符不符合预期如果是统计可以把结果按前缀分组数一下看分布是否合理如果是迁移那就更应该先跑一次只读扫描拿到全集再分批处理。4. 参数调优与耗时估算COUNT 到底填多少4.1 一次往返的成本模型COUNT的选择本质上是在两个成本之间做权衡单次调用的服务端成本和往返的网络成本。写个粗略的公式总耗时 ≈ (N / E) × (RTT C_batch) N × C_match其中 N 是 key 总数E 是每轮实际返回的元素数接近但不等于 COUNTRTT 是一次网络往返时间C_batch 是服务端单批处理的固定开销C_match 是每个 key 的模式匹配成本。从这个式子能看出两件事。第一COUNT越大往返次数 N/E 越少网络成本越低但单次 C_batch 会上升阻塞窗口变长。第二网络 RTT 的权重很高本地回环 RTT 可能只有几十微秒同机房内网大概零点几毫秒跨机房就到几毫秒了。同样的COUNT在三种环境下的总耗时能差一个数量级。举个具体的算例。一百万个 keyCOUNT取 1000假设每轮实际返回 800 个那大致需要 1250 轮。本地回环 RTT 取 0.1 ms总网络成本约 0.125 秒同机房取 0.5 ms约 0.625 秒跨机房取 3 ms约 3.75 秒。再加上服务端每批的处理时间和客户端逐条处理的 Python 开销实际耗时会比这个数字再高一些尤其 Python 的字符串处理不便宜MATCH命中多的时候客户端侧的循环可能成为瓶颈。4.2 实测数据与经验区间在几个不同规模的实例上我做过粗略对比给个区间参考。数据量一百万 key模式是一个中等宽度的前缀匹配客户端和 Redis 同机房Python 单线程COUNT往返轮次约服务端总耗时客户端总耗时单次最大阻塞10默认100000较高20 秒以上小于 1 ms10010000 左右中等5 秒左右1 ms 量级10001000 左右较低1 到 2 秒几 ms5000200 左右低1 秒内十几 ms2000050 左右最低1 秒内几十 ms有风险这个表里的数字是量级参考具体到你的环境会浮动但趋势是清楚的COUNT从 10 提到 1000收益最大往返次数直接砍掉两个数量级从 1000 再往上提网络成本的边际收益递减单次阻塞风险却在上升。所以我给的经验值是线上实例用 1000 作为起点根据实例的 QPS 和延迟敏感度在 500 到 5000 之间调。QPS 上万的核心缓存往 500 靠内部工具类实例可以到 5000。还有一个容易忽略的点COUNT设大之后单次 SCAN 阻塞可能达到十几毫秒。对普通业务这可能无所谓但对 P99 延迟卡在 5 ms 的接口来说这就是一次可观测的抖动。所以调整COUNT之前先确认这个实例的延迟预算别光看总耗时。提示COUNT是每轮扫描的桶数量提示不是返回结果数量的保证。字典稀疏时实际返回会明显少于COUNT这时往返次数会比估算的多别以为是参数没生效。4.3 匹配模式的写法与转义MATCH用的是 glob 风格不是正则这一点经常有人搞混。支持的通配符有这些通配符含义示例*匹配任意长度字符含空order:*?匹配单个字符user:?:profile[abc]匹配括号内任一字符user:[135]:profile[^abc]匹配不在括号内的字符user:[^0]:profile[a-z]匹配范围内的字符order:[0-9]*\转义下一个字符key\*literal如果你的 key 里本身就含有*、?、[这些字符模式里必须转义否则匹配结果会莫名其妙地多出来。比如业务里有一个 key 叫config:rate[*]你想精确匹配它模式得写成config:rate\[\*\]。这种情况在拼接业务 ID 当 key 后缀的系统里不算罕见尤其是把用户的输入直接拼进 key 的地方更是要小心。大小写敏感也是常见坑。MATCH *Order*和MATCH *order*是两个不同的模式glob 匹配是区分大小写的没有忽略大小写的开关。如果你的 key 命名规范混乱有的大写有的小写那就只能扫两遍或者把模式放宽到*[Oo]rder*。5. 常见问题排查实录5.1 扫出来的 key 有重复前面说过SCAN允许重复这是设计上的取舍。判断重复是否正常看两个指标重复率和重复的分布。如果重复的 key 集中出现在某几轮而且那几轮正好赶上业务写入高峰那基本就是 rehash 引起的属于正常。如果重复率高得离谱比如三分之一都是重复的那要怀疑是不是扫描期间有大量 key 被批量写入或删除导致字典频繁扩容缩容。处理方式很直接客户端用set去重。但要注意内存如果匹配结果有几十万条全塞进set会占几百 MB 内存脚本本身可能被 OOM 拖死。这种量级下我会改成写文件加外部去重或者用sort -u处理甚至用布隆过滤器做近似去重代价是可能误判不适合要求精确的场景。还有一种情况是假重复你在集群环境下没逐节点扫而是用redis-cli -c在一个连接里反复SCAN重定向导致同一个节点被扫了多次看起来是重复实际是重复扫了同一个节点。这种要从扫描方式上改不是去重能解决的。5.2 明明存在却扫不到这是最让人抓狂的一类问题。key 明明在redis-cli里GET得到SCAN就是扫不出来。常见原因有这么几种。第一种模式写错了。*order:pending*和order:pending:*完全不同前者是中间匹配后者是前缀匹配如果你的 key 是order:pending:123而模式写成了*pending那就匹配不上。排查方法很简单先用*扫一小批出来看看实际 key 长什么样。第二种集群下只扫了一个节点。前面讲过这是最高频的原因。验证方法是CLUSTER KEYSLOT key看看目标 key 落在哪个槽再确认那个槽对应的节点有没有被扫到。第三种逻辑过期。key 设置了 TTL实际已经过期但还没被删除GET的时候会触发惰性删除返回空但SCAN遍历字典时它可能还在。这就不是扫不到而是读到空的问题本质是 TTL 行为不是扫描的问题。第四种游标处理有 bug。比如有的客户端库返回的游标是bytes类型你拿去做if cursor 0判断永远不成立或者游标是字符串0跟数字 0 比较不等。这类问题在跨语言客户端里特别多Python 的redis-py返回 intNode 的ioredis在旧版本里返回字符串切语言的时候容易踩。5.3 线上卡顿先查有没有人偷用 KEYS如果实例突然出现周期性卡顿而你怀疑是某个业务在滥用命令最快的定位方式是看命令统计。INFO commandstats会列出每条命令的调用次数、总耗时、平均耗时。redis-cli INFO commandstats | grep -E cmdstat_(keys|scan)输出大概是这样cmdstat_keys:calls37,usec1842018,usec_per_call49784.27 cmdstat_scan:calls1204,usec94312,usec_per_call78.33keys调用了 37 次平均每次将近 50 毫秒这个数字已经足够解释卡顿了。再配合SLOWLOG GET看慢查询里有没有对应的记录时间点能对上就基本坐实了。redis-cli SLOWLOG GET 20定位到调用方之后处理方式通常是把代码里的KEYS换成SCAN或者在代码评审里加一条静态检查规则。有些团队会在客户端封装层直接禁用KEYS通过重命名或者权限控制来兜底效果不错。Redis 6 之后可以用 ACL 限制特定账号只能用哪些命令这是更彻底的方案。注意INFO commandstats的统计是自实例启动以来累计的重启会清零。如果实例刚重启不久数据参考价值有限得多观察一段时间。5.4 问题速查表把上面这些排在一起方便出问题时对着查。现象最可能的原因快速验证处理方式扫描结果有大量重复rehash 或扫描期间写入频繁对比去重前后数量客户端 set 去重或换低峰期重扫部分 key 扫不到集群只扫了一个节点CLUSTER KEYSLOT核对槽位逐节点扫描扫描很慢COUNT用了默认值 10看往返次数提到 1000 左右扫描期间实例卡顿COUNT过大或 QPS 太高看单次阻塞耗时降COUNT、限速、换只读副本脚本内存爆掉结果全量收进内存看进程 RSS改流式写文件分批处理模式匹配不到glob 未转义或大小写不符用*扫一批看实际 key 名修正模式转义特殊字符游标循环不结束游标类型比较错误打印游标类型统一转成整型比较6. 比全库扫描更省事的工程化做法6.1 用 Set/Hash 维护二级索引SCAN再优化也还是全库遍历一百万个 key 就是一百万个 key 的工作量。如果这类模糊查找是高频需求那更划算的做法是在写入的时候就把索引建好。最朴素的一种是维护一个Set把所有需要按前缀查询的 key 名放进去SADD idx:order:pending order:pending:1001 order:pending:1002 ...查询的时候SMEMBERS或者更省内存的SSCAN直接拿结果成本从 O(全库) 降到 O(索引大小)。写入侧需要在业务代码里同步维护删除的时候记得SREM还要处理过期清理。另一种更紧凑的方式是用Hash做索引HSET idx:order order_id status查询时用HSCAN idx:order 0 MATCH 1001* COUNT 100既省内存又能带上一些元数据。这种结构在按业务 ID 前缀查一堆 key的场景里特别顺手一个 Hash 存几十万个 field 也就几十 MB。代价也很明显双写一致性。业务侧每次创建或删除 key 都得同步更新索引一旦漏了就会出现索引里有但 key 已经没了或者反过来。我的做法是给索引加一个定期的对账任务用SCAN抽样比对把对不上的条目修掉。索引本身不是权威数据源对账兜底就够了。6.2 key 命名规范与分桶很多模糊查找的需求本质上是 key 设计没做好。比如把时间戳、随机串拼在 key 中间导致连前缀都提取不出来只能全库扫。合理的做法是把可查询的维度放在前面形成稳定的层级结构{业务}:{实体}:{状态}:{ID} order:pending:20240101:88421 order:paid:20240101:88422这样按状态查就是order:pending:*按日期查就是order:*:20240101:*虽然中间匹配仍然要全扫但至少前缀匹配能用上索引方案。再进一步可以按日期分桶每天的 pending 订单放到一个独立的 Hash 或者 Set 里过期了整桶删掉既控制了单个结构的规模也让查询变得直接。还有个小技巧是把要找的东西和要找它的线索分开。比如你要找的是订单数据那 key 可以是order:{id}另外维护一个order:index:pending的 Set 存待处理订单 ID 列表。查询走索引取数据走 key各司其职比在一个大字典里瞎翻要靠谱得多。6.3 什么时候该考虑搜索型模块如果查询模式已经复杂到多条件组合、按字段过滤、还要排序分页那再折腾SCAN和手工索引就是在硬扛了。这种时候可以考虑 Redis 生态里的搜索类模块它们支持在 Redis 内部建立字段级索引查询走倒排而不是全库遍历能力上更接近一个轻量搜索引擎。不过引入新组件是有成本的运维复杂度上升、内存占用增加、团队要学习新的查询语法。我的判断标准是看查询频率和数据规模——如果这类查询一天跑不了几次SCAN加限速完全够用如果每秒都在查而且条件越来越复杂那才值得上专用方案。别为了一个季度才跑一次的盘点任务去引入一套需要长期维护的组件。实际项目里更常见的折中方案是日常查询走索引结构兜底的模糊查找走SCAN两条路并存。前者覆盖百分之九十的高频场景后者处理偶发的排查需求成本可控实现也简单。我在几个百万级实例上反复折腾下来最深的体会是SCAN的坑几乎都不在命令本身而在你以为它是什么。把它当成分页工具会翻车指望它给出精确一致的结果会失望集群下忘了逐节点扫会漏数据COUNT用默认值会慢得莫名其妙。把这几个认知纠正过来剩下的就是写个带限速和去重的循环跑完落盘校验一次搞定。至于那些天天要查的模糊匹配早点把索引建起来比每次扫全库都划算。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑