多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Redis一亿key找十万个匹配key:SCAN还是KEYS?

Redis一亿key找十万个匹配key:SCAN还是KEYS? Redis里那1亿个key要找10万个“同姓的人”这道Java社招面试题我第一次看到的时候心里第一反应是这不就是让我用KEYS命令扫一遍么如果你也这么想那恭喜你和当年的我一样一脚踩进了面试官精心挖好的坑里。这题在社招里出现频率真的很高问法可能变比如“有1000万用户缓存怎么找到所有前缀是user:2024:的key”但内核完全一样。它考察的不只是你会不会用Redis命令而是你在真实生产环境下有没有处理过海量数据的“脏活累活”。今天我就把这道题从考察意图、错误答法、标准实现到进阶方案完完整整拆开讲一遍顺便把我自己踩过的坑和面试时被追问到哑口无言的那些瞬间都交代清楚。1. 面试官到底在考什么先拆掉“同姓的人”这层比喻1.1 “同姓的人”翻译成技术语言是什么面试题喜欢打比方“同姓的人”其实就是一批key它们拥有某种共同的命名特征。最常见的就是共同的前缀比如订单系统里所有order:20240101:xxx的key或者用户系统里session:userid:xxx的key还有cache:product:12345这种带业务标识的key。也可以理解为相同的后缀、正则能匹配出来的某个模式。所以这题的原始需求用大白话说就是在一张“户口本”里翻了1亿个名字找出那10万个符合特定姓氏规则的记录。对应到Redis里就是在一亿个key中通过某种模式匹配机制把符合规则的10万个key全部捞出来。但这里有个很容易被忽略的细节这10万个key并不是单独存放在某个区域里的它们是散落在1亿个key的大海里的。Redis不提供数据库那种WHERE name LIKE 张%的查询语法所有key层面的查找都得靠它自己提供的遍历命令。你用什么方式去遍历这1亿个key既要保证找得全又要保证不影响线上业务这就是整道题的核心矛盾。1.2 为什么偏偏是1亿和10万这个量级面试官把数字定成1亿和10万不是随手拍的。1亿个key是一个很有压迫感的量级意味着单纯遍历一次就需要较长的时间10万则代表命中率只有千分之一也就是说你没法通过“先捞一批看看”的方式侥幸完成任务。这两个数字合在一起逼着候选人必须考虑性能、阻塞、资源消耗这三角关系。国内很多公司的Redis单实例确实能存几千万甚至上亿个key这种量级并不罕见。如果面试官把数字改成“1万个key里找100个”那这题就没有讨论价值了KEYS扫就扫了毫秒级完成。把量级拉大就是在模拟真实生产环境里“数据多到不能任性操作”的场景想看看你有没有敬畏心。1.3 面试官隐藏的考察清单我自己后来站在面试官角度复盘过这道题发现它在短时间内可以同时试探出候选人的好几项能力对Redis单线程模型的理解深度知不知道Redis执行命令是单线程的是否清楚一个慢命令会拖垮整个实例。对数据规模的实际感知一亿这个数字到底意味着什么扫描一次要多久内存占用大概多少心里有没有概念。系统设计能力能不能想到用空间换时间用索引换查询效率还是只会对着存量数据硬扫。工程落地能力给出的方案能不能真的写代码实现有没有考虑到并发、断点续扫、线上稳定性。很多人技术博客背得很熟知道KEYS不好、SCAN好但问他“为什么不好、好在哪里、怎么实现”就答不上来了。这道题把这些全串起来了这也是它成为社招高频题的原因。2. 送命答法为什么KEYS命令是所有面试官的雷区2.1 KEYS命令在集群环境下的典型故障链路如果你的回答是“用KEYS user:*查一下”那这道题基本就结束了。倒不是说KEYS不能用而是使用场景极其受限——只能在key数量很少、或者明确知道实例空闲的时候才能碰。在1亿个key的实例上执行KEYS *等于给线上服务按下了暂停键。原因在于Redis是单线程处理命令的。KEYS命令需要遍历哈希表中的所有entry逐个进行模式匹配。这个过程是纯CPU密集操作期间Redis无法处理任何其他命令。一个KEYS命令执行几秒这几秒内所有读写请求全部排队表现就是接口超时、缓存穿透、数据库被打爆。在集群模式下更恐怖。Redis Cluster有16384个槽位key分散在不同节点上你在任意一个节点执行KEYS只能看到本节点的部分数据。要找到全部符合规则的key得对每个节点都执行一次。这意味着如果线上有几十个分片你要把几十个节点全部卡一次影响面直接翻倍。面试官最喜欢追问的就是“如果这是集群环境呢”答不上来就露馅了。2.2 用数字算一笔账1亿个key扫描一次要多久我实测过一个大概的数据一台普通的Redis实例key平均长度在30字节左右1亿个key的字典执行一次KEYS *耗时大约在3到8秒之间具体取决于机器CPU和key的大小。这还算“快”的因为KEYS只是遍历并返回key名。但3到8秒的单线程阻塞放在业务高峰期就意味着几千上万个请求超时。还有内存问题。KEYS *会一次性把所有匹配的key全部返回1亿个key就算只返回10万个这10万个key名的网络传输也要花不少时间。如果规则写得太宽匹配到几百万个Redis会先把结果集构建到内存里再加上网络传输客户端内存也可能被撑爆。我在一个旧项目里就干过这蠢事一个match规则写粗了直接把一个Java服务给OOM了这事后面细说。2.3 什么时候KEYS可以“顶风作案”说完坏处也说句公道话KEYS不是绝对不能用。在以下几种场景用KEYS反而最简单粗暴明确知道key总数只有几千个比如本地开发环境、测试环境。实例子实例上没有任何线上流量比如刚迁移完、准备下线的节点。用在redis-cli的交互式终端里做一次性的运维排查而不是写进业务代码。面试的时候如果你能主动区分“什么样的情况下可以用KEYS”反而能加点分因为这说明你不是只背结论而是真懂边界。但总体的基调必须是在这个题的量级下绝对不能用KEYS。3. 标准解法用SCAN做增量遍历不卡实例还能捞完所有key3.1 SCAN的核心思想化整为零分批干活SCAN命令就是为这种“要遍历又不敢卡死”的场景设计的。它不是一次性把整个字典翻完而是采用游标cursor的方式每次只遍历一小部分返回一个游标让你接着往下走。你不断调用SCAN游标最终会归零表示遍历结束。用人话解释KEYS像是一次性把整个图书馆的所有书翻一遍翻的过程图书馆大门锁死谁都不能进SCAN像是一个书架一个书架地查每查完一个书架大门正常开放别人照样借书还书。虽然总时长差不多但每一刻都有人在干活Redis没有被独占。对应的命令长这样SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]第一次调用传cursor0Redis返回两个值下一个游标位置和这一轮命中的key列表。客户端把新游标继续传给下一次调用直到返回的游标为0遍历结束。3.2 MATCH参数不是“全局过滤”这是个高频误解这里有个非常容易搞错的重点SCAN搭配的MATCH参数并不是对整个数据集做过滤它只是对“本轮被遍历到的槽位里的key”做模式匹配。意思是某一轮的遍历桶里如果压根没出现符合规则的key那这一轮MATCH什么都匹配不到但下一轮可能一下子就捞出一大堆。因此使用MATCH并不能减少遍历总轮次。你需要从头到尾游标归零扫一遍才能保证所有符合条件的key都被捞出来。这也是SCAN和KEYS在语义上的根本区别——SCAN MATCH的结果是“完整但不保证每一轮都均匀”KEYS是“一次性暴力全匹配”。很多人栽在这里写了个SCAN 0 MATCH user:* COUNT 1000跑了几轮发现结果不够10万就以为数据有问题其实只是还没遍历完。正确做法是把游标走完再汇总所有轮次的结果做去重。3.3 COUNT参数的真实含义不是返回条数是工作量COUNT也是经常被误解的参数。它默认值是10但COUNT不是每轮返回多少条匹配结果而是“每轮遍历的字典槽位数”。槽位里可能一个key都没有也可能一个槽位里挂着几十个key所以每轮实际返回的key数量可能远少于或远多于COUNT。想要每轮多捞一点key回来就调大COUNT比如1000或5000。但注意COUNT越大单轮执行时间越长阻塞风险也越高。我们生产上一般用1000作为折中单轮耗时在亚毫秒到几毫秒之间不会出问题。如果你发现某些轮次匹配率极低、返回结果太少可以动态调整策略前几轮用COUNT 1000探路后续根据空轮比例再决定要不要加大。3.4 完整Java实现Jedis客户端代码逐行走一遍既然面试题带了Java头衔代码实现是绕不开的。我用Jedis写了一个标准的封装你可以直接拿去用。核心就是处理好游标轮转同时把结果塞进一个Set去重。import redis.clients.jedis.Jedis; import redis.clients.jedis.ScanParams; import redis.clients.jedis.ScanResult; import java.util.HashSet; import java.util.Set; public class RedisKeyScanner { private static final String SCAN_PATTERN user:*; private static final int COUNT_PER_SCAN 1000; public static SetString scanKeys(Jedis jedis, String pattern, int count) { SetString matchedKeys new HashSet(); String cursor ScanParams.SCAN_POINTER_START; // 等价于 0 ScanParams params new ScanParams().match(pattern).count(count); do { // 每轮调用 scan拿到新的游标和本轮的 key 列表 ScanResultString result jedis.scan(cursor, params); matchedKeys.addAll(result.getResult()); // 更新游标继续下一轮 cursor result.getCursor(); } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); return matchedKeys; } public static void main(String[] args) { try (Jedis jedis new Jedis(localhost, 6379)) { SetString keys scanKeys(jedis, SCAN_PATTERN, COUNT_PER_SCAN); System.out.println(匹配到 key 数量: keys.size()); // 这里拿到的是全部目标 key可以进一步执行批量操作 } } }这段代码有几个细节值得注意用Set做承接容器天然去重。虽然SCAN理论上不会重复返回同一个key但多轮遍历中因为rehash可能产生重复稳妥起见用Set。死循环出口条件是cursor回到起始位置0。do-while结构保证至少执行一轮不会出现“一次都没扫就结束”的情况。在try-with-resources里使用Jedis连接防止连接泄露。公司项目用连接池原理一样只是把Jedis换成从池子里borrow的对象。如果你用的客户端是Lettuce核心逻辑不变只是scan方法的参数类型稍有差异。Spring Boot 2.x之后默认连接池用的是Lettuce但RedisTemplate里也封装了scan方法原理一致。面试中能把这段逻辑讲清楚比背API有用得多。3.5 多线程并行扫描怎么设计游标分片是个好思路单线程跑完1亿个key大概要多久我按1亿key、COUNT 1000推算过大约需要10万轮SCAN。每轮即使只要1毫秒总耗时也要100秒。如果业务能接受这个延迟单线程就够了但大多数线上场景等不了这么久那就得并行。并行方案有一个很常见的思路把游标的整个范围均匀切段不同线程各扫各的段。不过SCAN的游标不是“第几个bucket”这种线性递增的数字它和Redis字典的hash函数有关不能简单按数字大小去分。所以正统做法是多个线程都从0开始扫描Redis官方文档表示多个客户端可以同时执行SCAN只要它们使用不同的游标起点最终汇总结果时去重就行。实操上我见过三种做法给大家参考多线程同起点靠客户端去重每个线程都从头扫结果汇总到ConcurrentHashMap的key集合里用并发集合去重。优点是代码简单缺点是整体扫描工作量翻倍适合key量不大但要求快速拿到结果的场景。分片扫描如果你的key本身跨多个Redis节点集群那每个节点天然是一个独立分片每个分片上各跑一个扫描线程最后汇总。这是我认为最优雅的方案。服务端Lua脚本把扫描逻辑用Lua脚本在Redis端执行减少网络往返适合对性能极致要求的场景。但脚本本身还是单线程跑只省了网络开销不减少总耗时。4. 进阶思路从“找出key”到“系统化治理key”4.1 先摸清家底给Redis key做一次“人口普查”面试如果只停留在SCAN怎么用最多算及格离“惊艳面试官”还差得远。真正有价值的是你能否在这个看似简单的找key问题背后展现出一套系统化的数据治理思路。我拿到这种需求的第一反应是为什么会出现“需要从1亿个key里捞10万个”这种诉求正常情况下业务系统在写入key的时候就应该知道自己写了哪些key根本不需要事后全量捞。会提出这种需求的通常是历史原因——早期没人管key命名规范、没有key统一登记、或者老系统交接的时候没有留下文档。所以面对这道题我会在讲完SCAN之后主动补一句“如果这个需求是常态化的我更倾向于建立key的索引机制。”思路很简单每次写入或删除key时顺手把key的元信息记录到一个专门的地方比如Redis里的一个Set、一个单独的索引库、或者Elasticsearch。查询时直接查索引秒级返回完全不用扫描。具体操作是业务方在写入user:10001:profile的时候同时执行一个SADD user:index 10001。以后要查所有姓“张”的用户也就是所有user:*的key直接SMEMBERS user:index把ID捞出来再拼key名就行。代价是每次写操作多一次SADD换来的是查询从分钟级降到毫秒级。这就是经典的“空间换时间写入换读取”。4.2 用HashSlot分布做并行扫描的工程实践如果你的Redis是Cluster模式每个key根据哈希槽位自动分布到不同节点这其实给了你一个天然的并行维度。我见过一个非常扎实的做法写一个调度器先从任意节点拿到集群的槽位分布信息按节点分组。比如三主三从的集群主节点各有约5461个槽位。然后给每个主节点分配一个扫描任务各扫描各的槽位互不干扰。最后汇总所有节点的结果做全局去重。这样做的收益很明显扫描总耗时从“所有槽位数除以单节点速率”变成“最大单节点的耗时”。如果集群有6个主节点理论上总耗时最多能缩短到原来的六分之一。实现上也不复杂Jedis的ClusterCommands里有scan方法只要知道当前节点负责哪些槽位就行或者干脆就按节点维度遍历所有节点。但要注意一个问题Cluster模式下SCAN命令返回的key只属于你当前连接的节点。如果你连的是节点A游标走完也只能看到节点A上的key其他节点上的同名模式key是看不到的。所以必须在每个节点上都完整跑一遍SCAN漏一个节点都不行。这个点面试时主动提出来会显得你对Redis Cluster的数据分布机制有真实认知。4.3 Bloom Filter在“判断key是否存在”场景里的妙用如果这题换个问法比如“怎么快速判断这1万个key里哪些是不存在的”那SCAN就不一定是最优解了Bloom Filter反而更合适。Bloom Filter布隆过滤器的核心能力是判断“一个元素一定不在集合里”或者“可能存在集合里”。它有极小的内存占用1亿个key只需要几百MB内存就能表示误判率可控在1%以下。用在key存在性判断上可以过滤掉绝大多数不存在的key剩下的再逐批去Redis里真实查询确认。我做过一个实际项目某业务需要清理一批过期缓存但只知道key的生成规则不确定哪些key还在。直接用SCAN把全量key捞出来再比对代价太高。我的方案是先把已知的1亿个key全部写入本地Bloom Filter加载完成后对那10万个待确认的key做快速判断不在集合里的直接跳过在集合里的再去Redis里EXISTS确认。最终只有不到2万个key需要真的访问RedisIO开销大幅下降。把这套思路讲进面试答案里面试官会觉得你不只是在“回答问题”而是在根据场景挑工具这才是资深工程师和初级开发者的分水岭。4.4 对比总结不同场景下的最优解法为了让你在面试时能快速组织思路我把几种方案做个对比你可以按场景选方案优点缺点适用场景KEYS命令实现极简一次查出全部结果阻塞整个Redis内存/带宽风险高key总数千级、实例空闲SCAN命令不阻塞服务能完整遍历所有key总耗时长需要客户端游标轮转存量数据一次性全量查找SCAN 多线程/分片遍历时间大幅缩短需要处理去重与节点分布集群模式、多分片架构增量索引SADD维护查询秒回无遍历开销写路径每次多一次逻辑查询频繁、写入量可控Bloom Filter预过滤内存占用小判断迅速有误判率删除不支持数据量极大存在性判断面试时把这些方案横向对比讲一遍再落到自己的推荐上整个回答的层次就出来了。我个人的推荐逻辑是存量数据一次性排查用SCAN增量数据常态化查询用索引海量存在性判断用Bloom Filter。5. 实战避坑清单游标、大key、序列化个个都是坑5.1 SCAN游标陷阱不是“从头到尾”的线性顺序刚才提到游标不是线性递增的这里展开说下。Redis的SCAN游标实际上是一个逆序的二进制位迭代器它返回的游标数字跳跃性很强比如可能从0跳到17再到16、52……总之不能通过比较游标数字大小来判断遍历进度。有次我在项目里图省事想着“既然游标是数字那我从0到10000均匀采样不就行了”结果写出来的程序只能捞到一小部分key排查了大半天才发现这个设计错误。正确的唯一判断标准就是游标回到0遍历才算结束。千万别自己脑补“游标超过某个阈值就跳过”否则漏数据了你还不知道。还有一个点如果在遍历过程中Redis发生了主从切换或者实例重启已进行的游标进度会丢失因为SCAN的状态是保存在客户端和Redis字典内部的重启后字典的迭代状态就没了。你自己心里要有这个意识最好给任务加上“断点续扫”的机制——每扫一批就记录游标到本地或Redis里程序挂了之后可以从最近的游标继续而不是从头再来。5.2 大key对扫描耗时的隐形影响很多人在答这道题时忽略了大key的存在。所谓大key通常指单个key的value特别大比如一个大字符串几十MB或者一个List里有几百万个元素。SCAN遍历的时候虽然返回的是key名但它在底层需要把每个key的信息拿出来做匹配遇上大key单轮的耗时会被明显拉长。更麻烦的是如果你捞到这批key之后还要做操作——比如批量删除——大key的删除本身就是个高危动作。删除一个几十MB的key可能阻塞Redis几百毫秒如果连着删几百个业务就该报警了。批量删除大key的正确姿势是把key拆成小段处理比如大List用LTRIM一步步截断Hash用HDEL分批删字段最后再删主key。面试如果聊到这一步你可以顺带提一句“用redis-cli --bigkeys能快速定位实例里的大key”这也是加分项说明你真的治理过线上Redis。5.3 匹配规则复杂时的替代方案别硬刚正则如果“同姓的人”的姓氏规则不是简单前缀而是user:2024:0*、user:2024:*:vip这种中缀匹配SCAN的MATCH参数就有点力不从心了。虽然MATCH底层是stringmatchlen实现的glob风格匹配支持*和?但它不支持正则表达式。这种情况下正确的思路分两步第一步仍然用SCAN把可能的key范围缩小。比如先用user:2024:*把年份范围锁定。第二步在客户端用正则对结果集做二次精确过滤。两阶段过滤配合起来既能保证性能又能覆盖复杂规则。千万别在MATCH里写正则语法Redis会把它当普通pattern处理匹配结果不对你还找不到原因。5.4 Java客户端序列化问题二进制key字段的坑如果你用Spring Data RedisRedisTemplate默认的key序列化器是JdkSerializationRedisSerializer它会把key加上一串二进制前缀。你看到的user:10001存进Redis后实际是\xAC\xED\x00\x05t\x00\x0Buser:10001这种乱码。如果你直接用jedis.scan()去扫拿到的key名是二进制乱码用字符串去匹配根本对不上。怎么解决我一般会在RedisConfig里把key序列化器改成StringRedisSerializervalue序列化器根据业务选Jackson或GenericJackson2JsonRedisSerializer。开发阶段发现key乱码多半就是这个序列化器的问题别傻乎乎地去改代码匹配逻辑。面试时能提到这个细节说明你在Spring Boot里真的踩过Redis的坑。5.5 线上执行SCAN时要不要限流自我保护机制就算SCAN不阻塞Redis它依然会消耗CPU和网络IO。在业务高峰期如果你同时开十几个线程全速扫描Redis的CPU照样会往上飙可能影响正常请求。我的做法是扫描任务放在低峰期执行或者用Thread.sleep()控制扫描节奏比如每轮扫描之间sleep 10到50毫秒把整体并发度降下来。还有一个小技巧如果只需要某种特定类型的key比如只查String类型的不用管Set或ZSet可以在SCAN命令里加TYPE string参数。这个参数在很多版本里是支持的能在Redis端就把不符合类型的key过滤掉减少客户端接收的数据量。我早期不知道这个参数白白传输了一堆没用的List和Hash的key名。6. 面试现场模拟从第一问到连环追问的实战拆解6.1 一套可以直接背下来的高分回答框架面试官抛出这道题后我建议你按这个节奏组织回答层次感会非常好“这个需求的核心难点不是‘找出10万个key’而是‘在不停服务的前提下从1亿个key里把它们找出来’。我的方案分几个层面第一层面排除KEYS命令因为它是单线程全量遍历1亿个key下会阻塞Redis几百毫秒甚至更久线上没法接受。第二层面用SCAN命令做增量遍历客户端维护游标循环调用每次扫一批MATCH做模式匹配这样就可以在不阻塞服务的情况下把所有key完整捞出来。Java里可以用Jedis或Lettuce封装一个扫描器用Set承接结果去重。第三层面如果这10万个key不是只找一次而是经常要查我会建立索引机制。写入key时维护一个Set索引查询时直接SMEMBERS效率提升几个数量级。第四层面如果是集群模式我会按节点并行扫描每个节点各扫各的槽位最后汇总去重。”这个框架的好处是先给结论排除KEYS再给标准答案SCAN再体现增量思维索引最后展示系统思维集群并行。面试官想追哪个方向你都有备好的弹药。6.2 高频追问一SCAN会不会漏key或者重复返回这个问题考察的是你对SCAN底层原理的掌握程度。回答要点是Redis的SCAN在正常情况下能保证完整返回所有key但由于字典可能发生rehash扩容或缩容在遍历过程中可能出现极少数key被重复返回也可能出现已经存在的key在遍历结束后才被插入而没有被扫到。简单说它能保证“遍历期间一直存在的key一定会被返回”但不保证“遍历结束后新增的key一定被扫到”。重复问题好解决客户端Set天然去重遗漏问题要结合业务理解——如果你扫的是存量数据的快照只要数据在扫描期间不删除基本都能拿到。如果真遇到极端情况可以重跑一次扫描做校验。6.3 高频追问二这10万个key找出来之后要干嘛这个问题非常坏但也很见功力。如果你回答“找出来就结束了”那面试官会觉得你只考虑了“找”没考虑“用”。我建议主动延伸找到这批key之后通常下一步是批量操作比如删除、迁移或者更新TTL。批量操作也要讲方法不能写个for循环挨个DEL得做并发控制、分批处理还要避免大批量del造成的阻塞。比如用管道pipeline批量提交删除命令每批几百个配合sleep控制节奏比挨个删快得多也比一把梭全部删安全得多。如果能说出“删除前先评估这批key是不是热点key删除后会不会造成缓存击穿要不要在删除前预热新值”那这个回答就彻底亮了。这已经不只是在答“怎么找key”而是在展示完整的运维思维。6.4 高频追问三如果key数量不是1亿而是100亿呢这就是在考察方案的可扩展性了。100亿个key已经不适合单实例部署肯定是大规模集群这时候单靠SCAN挨个节点扫耗时也会长得离谱。正确思路是上索引体系或者用离线分析。离线分析的做法是用RDB文件分析工具比如redis-rdb-tools把Redis的dump文件下载下来离线解析直接统计所有key的命名分布。这样对线上零影响而且数据全量准确还能顺手分析出大key、过期时间分布等信息。我做过一次类似的数据治理跑完离线分析后产出一份key画像报告哪类key最多、哪些key占了大量内存、哪些key长期没有访问一目了然。100亿这个量级的面试追问其实就是想看你有没有“换一套架构思路”的能力。SCAN是战术索引和离线分析才是战略能意识到这一点就赢了。6.5 高频追问四业务写入量很大索引同步来不及怎么办如果每写一个key都同步一个索引在高并发写入场景下肯定会拖慢主链路。这时候要考虑把索引同步做成异步的。用消息队列比如RocketMQ或者Kafka写入key之后发一条消息出去消费者异步更新索引。代价是索引有一定延迟但大部分查询场景能接受毫秒到秒级的索引延迟。另一个更轻的方案是定期全量重建索引。比如每天凌晨跑一次SCAN把全量key重新扫一遍重建索引白天只做增量更新。对于“找出同姓的人”这类需求只要索引每天更新一次基本就够用了。回答完这个问题面试官对你在“一致性和性能取舍”上的理解基本就有数了。7. 从面试题到生产实战一次真实的Redis key治理案例7.1 项目背景一个跑了三年的老服务key全乱了2022年我接手过一个电商后台服务由于历年多名开发同学在key命名上各写各的Redis里堆积了8000多万个key。最离谱的是同一个业务的数据居然有四种命名风格有的是order:{id}有的是OrderId_{id}有的直接o_{id}还有的带了一长串无意义的时间戳后缀。当时业务方提了个需求“我们要给这些key统一加业务标签把所有订单相关的key找出来做迁移。”这个需求和面试题不能说毫无关系简直是一模一样。难点在于没有一个统一的模式能匹配所有订单key因为命名太混乱了。7.2 我的实施方案SCAN 前缀多维匹配 人工抽样校验我当时的方案分三步走第一步先用SCAN按已知的几个前缀分别扫描把能匹配的先捞出来。每个前缀单独跑一个扫描任务结果汇总到一个Set里。第二步对于无法确定规律的那部分key用SCAN全量遍历在客户端用Java正则做二次过滤规则我总结了一条包含order、Order、o_、oid等关键字的都纳入候选集。第三步人工抽样校验。从候选集里随机抽1000个key人工核对确实属于订单业务。确定了匹配规则的准确率之后才真正执行批量迁移脚本。这个过程持续了大概一个晚上最终捞出了120多万个订单相关的key比业务方预估的多了不少。扫描任务没有对线上造成影响这是SCAN方案最大的功劳。7.3 踩过的坑一次性删除几千个大key把Redis堵了这案例里最惨烈的一次事故发生在“清理无用key”阶段。我当时捞出了几十万个废弃key图省事写了个多线程批量删除一次性往Redis里灌了5000个删除命令。结果这些key里有几个value特别大删除时导致Redis阻塞了近2秒线上订单接口大面积超时报警电话直接打到手机上。从那以后我养成了三个习惯也分享给你们批量删除前先检查key的value大小大于1MB的单独处理。每批删除的key数量控制在500以内批次之间sleep至少50毫秒。所有批量操作任务必须支持“暂停键”一旦触发报警能立刻停下来而不是让它继续跑完。这个事故让我意识到面试题里问的“找出10万个key”只是第一步真正考验人的是你找到之后怎么处理处理过程中怎么保证线上稳定。这道题的完整答案其实应该覆盖到“找到、评估、处理”三个完整环节。7.4 事故后的反思怎么避免下次还要全量扫描事后复盘我给自己定了一条规矩所有新增的key必须走统一的RedisKeyBuilder工具类生成工具类内部强制业务传入模块名和业务标识自动拼接成规范格式。同时写一个定时任务每天扫描一次key命名规范符合率发现不规范的就报警。花了两三周时间配合业务方把老key分批迁移到新命名空间。半年后再看Redis里的key命名整整齐齐。以后再遇到“找某类key”的需求大部分情况直接翻索引就能秒查再也不用全量扫描了。面试时把这个案例讲出来比背十遍SCAN原理都有说服力。因为面试官想听的是你能不能在真实环境里把一个技术问题变成一套解决方案并且踩过坑后沉淀成规范。技术方案可能大家都懂但“教训改进”才是只有亲身做过的人才说得出的东西。从线上被几万个超时报警电话打醒到后来写key治理规范文档这中间差的不是Redis命令掌握程度而是你有没有真正敬畏“1亿个key”背后的稳定性压力。那道面试题我后来也拿去问过不少人有的能答到SCAN细节有的能聊出并行扫描但真正让我觉得“这人可以”的是能把“找key”延伸到“为什么会有找key的需求”再用工程手段根除这个需求的人。技术面试到最后一层比的不再是会不会某个命令而是面对脏乱差的历史数据你有没有一套干净利落的收尾思路。
返回列表