1. 为什么 KEYS 会拖垮线上 Redis一次缓存雪崩的复盘Redis 里查 key很多人第一反应就是KEYS user_token*。这条命令在本地测试库上跑得飞快几十毫秒就返回结果于是它被写进了运维脚本、定时任务、甚至后端接口里。问题在于Redis 是单线程处理命令的KEYS的执行方式是一次性遍历整个 keyspace在它跑完之前后面所有客户端的请求全部排队等待。你的库里有 10 万个 key它可能几十毫秒有 500 万个 key它就能卡住好几秒。这几秒里所有读写请求全部超时缓存层直接变成故障源。我见过最典型的一次事故某业务用KEYS session:*做会话清理平时 key 量小没事大促期间 session 数量涨到几百万定时任务一触发Redis 主线程被占满上游接口大面积 504最后靠重启才恢复。事后排查发现罪魁祸首就是这条看起来人畜无害的KEYS。KEYS的另一个坑是它不支持分页。你没法告诉它「先给我 100 个剩下的下次再取」它要么全给你要么一个不给。这意味着你无法控制它对主线程的占用时长只能被动接受它跑完。对于缓存运维和后端开发来说这等于把稳定性交给了一个不可控的变量。那正确的做法是什么答案是SCAN。SCAN是增量式游标遍历每次只返回一小批 key 和一个新的游标你拿着游标继续下一次查询直到游标回到 0 表示遍历结束。它把「一次长时间阻塞」拆成了「多次短时间访问」每次调用只占用很少的 CPU 时间片不会让 Redis 假死。这就是本文要讲清楚的核心用 SCAN 替代 KEYS把不可控的阻塞变成可控的遍历。这篇文章面向的是正在维护 Redis 缓存、写运维脚本、或者做后端缓存层开发的同学。我会从命令格式讲起给出 COUNT 和 MATCH 的配置建议写出可复制的游标循环脚本再用redis-cli --scan验证大 key 分布。如果你正在被KEYS的延迟问题困扰或者想提前规避这个坑下面的内容可以直接跟做。需要说明的是SCAN 并不是银弹它有自己的一套使用约束比如 COUNT 只是提示不是精确值、遍历期间新增或删除的 key 可能重复返回。这些细节我会在对应章节里逐个拆开讲避免你踩我踩过的坑。2. SCAN 命令格式与 COUNT/MATCH 参数配置实战先把命令格式摆出来这是后面所有操作的基础SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]四个部分逐个解释。cursor是游标第一次调用传0之后每次传上一次返回的游标值直到返回的游标又是0表示遍历完成。MATCH pattern是模式匹配和KEYS的通配符规则一致比如user_token*、session:*。COUNT count是每次迭代返回的元素数量提示注意是提示不是精确值。TYPE type是 Redis 6.0 之后加入的可以按数据类型过滤比如只看 string 或 hash。先看一个最基础的例子感受一下游标的流转127.0.0.1:6379 SCAN 0 MATCH user_token* COUNT 5 1) 6 2) 1) user_token:1000 2) user_token:1001 3) user_token:1010 4) user_token:2300 5) user_token:1389返回结果是一个两元素数组第一个元素6是下一次要传的游标第二个元素是这批匹配到的 key 列表。你拿着6继续查127.0.0.1:6379 SCAN 6 MATCH user_token* COUNT 5 1) 0 2) 1) user_token:4521 2) user_token:7788这次返回的游标是0说明遍历结束。整个过程没有一次性锁住主线程每次只处理一小批。关于 COUNT有几个实战要点必须说清楚。第一COUNT 默认值是 10这个值偏小遍历大库时网络往返次数会很多。第二COUNT 不是「返回 count 个 key」而是「每次扫描 count 个哈希槽位」实际返回的 key 数量可能远小于 COUNT尤其是用了 MATCH 过滤之后。第三COUNT 调大能减少往返次数但单次阻塞时间会变长需要权衡。我的经验值是普通遍历用 COUNT 100 到 1000MATCH 过滤严格时适当调大。MATCH 的坑在于它是在返回前过滤不是扫描时过滤。也就是说即使你只想找user_token*Redis 仍然会扫描所有槽位只是把不匹配的丢掉。所以 MATCH 不会减少扫描量只会减少返回量。如果你的匹配模式命中率很低遍历整个库的代价依然存在只是被拆成了多次。TYPE 参数在 Redis 6.0 可用适合做类型清理。比如只想找所有 hash 类型的 key127.0.0.1:6379 SCAN 0 TYPE hash COUNT 100这里给一个参数对照表方便你按场景选参数作用推荐值注意事项cursor游标位置首次 0必须用返回值继续不能自己编MATCH模式过滤按业务前缀不减少扫描量只减少返回COUNT单次扫描槽位数100–1000是提示非精确MATCH 后返回更少TYPE按类型过滤string/hash 等需 Redis 6.0还有一个容易被忽略的点SCAN 的遍历顺序是不保证的同一个库两次遍历顺序可能不同。所以不要依赖 SCAN 返回的顺序做业务逻辑它只保证「遍历完所有 key」不保证「按什么顺序」。如果你在写脚本建议把 COUNT 设成变量方便按库大小调整。小库几万 key用 100 就够大库千万级可以上到 1000但要注意单次返回的数据量对客户端内存的影响。3. 可复制的游标循环脚本Shell 与 Python 双版本光知道命令格式不够实际运维里你需要一个能跑起来的循环脚本。这一节给出 Shell 和 Python 两个版本都是可以直接复制使用的。先看 Shell 版本适合放在服务器上做快速排查#!/bin/bash # scan_keys.sh - 用 SCAN 安全遍历匹配的 key REDIS_HOST127.0.0.1 REDIS_PORT6379 PATTERNuser_token* COUNT200 CURSOR0 TOTAL0 while true; do # 执行 SCAN读取游标和 key 列表 RESULT$(redis-cli -h $REDIS_HOST -p $REDIS_PORT SCAN $CURSOR MATCH $PATTERN COUNT $COUNT) CURSOR$(echo $RESULT | head -n 1) KEYS$(echo $RESULT | tail -n 2) # 统计并输出本批 key if [ -n $KEYS ]; then NUM$(echo $KEYS | wc -l) TOTAL$((TOTAL NUM)) echo $KEYS fi # 游标回到 0 表示遍历结束 if [ $CURSOR 0 ]; then break fi done echo 遍历完成共匹配 $TOTAL 个 key这个脚本的核心逻辑就是「拿游标、查一批、更新游标、判断是否结束」。注意head -n 1取游标、tail -n 2取 key 列表这是解析redis-cli输出的常用手法。COUNT 设成 200兼顾往返次数和单次阻塞。再看 Python 版本适合集成到运维平台或做更复杂的处理import redis def scan_keys(patternuser_token*, count200, batch_callbackNone): 用 SCAN 游标遍历匹配的 key避免 KEYS 阻塞 :param pattern: 匹配模式 :param count: 每次扫描的槽位数提示 :param batch_callback: 每批 key 的回调函数用于处理或统计 :return: 匹配到的 key 总数 client redis.Redis(host127.0.0.1, port6379, decode_responsesTrue) cursor 0 total 0 while True: cursor, keys client.scan(cursorcursor, matchpattern, countcount) if keys: total len(keys) if batch_callback: batch_callback(keys) else: for k in keys: print(k) if cursor 0: break return total if __name__ __main__: def handle(batch): # 这里可以替换成删除、迁移、统计等逻辑 print(f本批 {len(batch)} 个 key) n scan_keys(patternuser_token*, count200, batch_callbackhandle) print(f共匹配 {n} 个 key)Python 版本用redis-py的scan方法它内部已经处理了游标解析返回的是(cursor, keys)元组比解析命令行输出干净得多。batch_callback的设计是为了让你能在每批 key 上做处理比如批量删除、批量迁移、或者统计前缀分布。这里要提醒一个实战细节遍历过程中不要在同一批里做大量写操作。比如你在回调里对每个 key 执行DEL如果一批 200 个 key 全删虽然 SCAN 本身不阻塞但 200 次 DEL 累积起来也会占用主线程。更稳妥的做法是把 key 收集起来分批用UNLINK异步删除处理或者控制每批的处理量。另外如果你的 Redis 有密码或用了非默认库记得在连接参数里补上password和db。生产环境建议用连接池避免每次遍历都新建连接。这两个脚本的共同点是游标驱动、分批处理、可中断。你随时可以 CtrlC 停掉不会像 KEYS 那样一旦发出就必须等它跑完。这就是 SCAN 在运维友好性上的核心优势。4. 用 redis-cli --scan 验证大 key 分布与成功结果前面讲了命令和脚本这一节讲怎么验证效果。redis-cli自带一个--scan选项它内部就是用 SCAN 实现的适合快速排查。最基本的用法redis-cli --scan --pattern user_token* | head -n 20这条命令会持续输出匹配的 keyhead -n 20取前 20 个就退出。注意--scan默认的 COUNT 是 10遍历大库时可能比较慢可以配合--count调整redis-cli --scan --pattern session:* --count 500 | wc -l这条命令统计session:*的 key 总数。--count 500让每次扫描 500 个槽位减少往返。实测下来百万级 key 的库用 COUNT 500 遍历通常几秒到十几秒能跑完而且期间 Redis 的延迟曲线是平稳的不会出现尖刺。验证大 key 分布是另一个高频场景。SCAN 本身不返回 key 的大小但你可以结合MEMORY USAGE或STRLEN来排查。下面这个组合命令可以找出匹配前缀里占用内存最大的 keyredis-cli --scan --pattern cache:* --count 500 | \ while read key; do size$(redis-cli MEMORY USAGE $key 2/dev/null) if [ -n $size ] [ $size -gt 102400 ]; then echo $size $key fi done | sort -rn | head -n 20这段脚本遍历所有cache:*的 key用MEMORY USAGE取每个 key 的内存占用过滤出大于 100KB 的按大小倒序取前 20。这就是一个典型的「SCAN 遍历 逐 key 检查」的大 key 排查流程。注意MEMORY USAGE本身也是 O(1) 到 O(N) 的操作对超大集合类型可能较慢所以建议先用 SCAN 缩小范围再逐个检查。怎么判断「成功」有几个可观测的信号。第一遍历期间用redis-cli --latency观察延迟应该保持在正常水平不会出现几百毫秒的尖刺。第二遍历能完整跑完并返回总数和DBSIZE量级对得上考虑 MATCH 过滤后会更少。第三如果你在遍历时同时压测读写业务请求的 P99 延迟不受明显影响。对比一下 KEYS 的表现同样百万级 keyKEYS cache:*执行期间redis-cli --latency会看到明显的延迟飙升业务侧可能出现超时。而 SCAN 遍历期间延迟曲线基本平稳。这个对比就是选择 SCAN 的最直接理由。还有一个实用技巧如果你只是想确认某个前缀的 key 是否存在不需要遍历全部用 SCAN 取第一批就够了redis-cli --scan --pattern user_token:1000* --count 100 | head -n 5有输出说明存在没输出也不代表一定不存在可能在前缀的后面批次但结合业务前缀设计通常第一批就能判断。需要强调的是--scan是排查工具不是生产代码。生产环境里的遍历逻辑应该用第 3 节的脚本加上错误处理、日志、限流。--scan适合你在终端里快速看一眼确认问题范围。5. 本篇常见报错排查从 401 到游标死循环这一节集中处理你在用 SCAN 和 TaoToken 接入时可能遇到的报错。先说 Redis 侧的再说 API 侧的。报错一(error) ERR invalid cursor这个通常是你手动传了一个非法的游标值。SCAN 的游标必须是上一次返回的字符串不能自己编也不能传负数。正确做法是严格用返回值继续。如果你在脚本里把游标当整数处理注意它可能超出整数范围要用字符串保存。报错二游标一直不回到 0循环停不下来这种情况多半是你在遍历期间大量新增 key导致遍历「追不上」新增速度。SCAN 只保证遍历开始时存在的 key 会被返回遍历期间新增的 key 可能被返回也可能不被返回但不会导致游标永不归零。如果真出现死循环检查你的循环条件是不是写成了while cursor ! 0但没更新 cursor或者把返回的游标解析错了。用第 3 节的脚本模板可以避免这个问题。报错三(error) NOAUTH Authentication requiredRedis 设了密码但你没传。命令行加-a yourpasswordPython 里加passwordyourpassword。注意-a在命令行会暴露密码生产环境建议用REDISCLI_AUTH环境变量。报错四local proxy failed/connection refused这类是网络层问题通常是 Redis 地址或端口不对或者服务没起来。先用redis-cli -h host -p port PING确认能通。如果你是通过统一 API 通道访问模型服务时遇到类似连接错误检查 Base URL 是否写对。报错五401 Unauthorized这个在调用模型 API 时常见原因是 Key 无效或没带。如果你用 TaoToken 的统一通道需要在请求头里带上正确的 Key。配置三件套是Base URL 填https://taotoken.net/apiKey 填你在控制台创建的 API KeyModel ID 填你要调用的模型名。三者缺一不可401 基本都是 Key 的问题。报错六reading choices相关解析错误这是调用模型接口后解析响应时常见的问题通常返回体不是预期的 JSON 结构。先确认你请求的路径和参数正确再用curl直接打一次看原始返回。如果返回的是错误信息而不是 choices 数组说明请求本身失败了先解决请求问题再解析。报错七OAuth 相关错误如果你在用 Claude Code 之类的工具遇到 OAuth 报错通常是认证配置没对齐。检查你的配置文件里 Base URL、Key、Model ID 是否和实际使用的一致。Claude Code 的配置可以放在 settings 里Cline 的 MCP 配置、Codex 的 auth.json 也是同理三件套必须完整。排查的通用思路是先确认连接通不通再确认认证过不过最后确认返回结构对不对。Redis 侧先PINGAPI 侧先curl打一次原始请求。大部分报错都能通过这个顺序定位。6. 统一 Key 通道与长期编码方案把 Redis 的 SCAN 用熟之后你会发现「安全遍历」这个思路在很多地方都通用不要一次性拉全量用游标或分页分批处理。这个原则在调用模型 API 时同样适用尤其是做批量任务的时候。如果你在做后端开发或缓存运维的同时还需要接入模型能力TaoToken 提供统一 Key 和 API 通道省去逐个平台配置的麻烦。接入信息如下官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 地址https://taotoken.net/api模型对话入口https://taotoken.net/api/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewriteCoding Planhttps://taotoken.net/api/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite控制台https://taotoken.net/api/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理https://taotoken.net/api/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite接入文档https://taotoken.net/api/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteClaude Code 接入https://taotoken.net/api/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-code-anthropicutm_campaignrewrite配置的时候记住三件套Base URL 用https://taotoken.net/apiKey 从 API Keys 页面创建Model ID 按你要用的模型填。这三项在 Claude Code、Cline MCP、Codex auth.json 里都是必须的缺一个就会报 401 或认证失败。如果你只是偶尔验证模型效果用模型对话入口就够了。如果是要长期做编码、跑 Agent 任务Coding Plan 更合适配额和调用方式都按长期使用设计。遇到接入问题先查接入文档里面有各工具的完整配置示例。回到 Redis 这条线最后给你一个实用建议把 SCAN 遍历封装成团队内部的工具函数统一 COUNT 和错误处理避免每个人各写一套。生产环境的遍历任务加上限流和日志记录每次遍历的 key 数量和耗时方便后续排查。KEYS 不是不能用但只应该出现在你明确知道库很小的场景里比如本地开发或测试环境。线上库一律用 SCAN。