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 到 1000,MATCH 过滤严格时适当调大。
MATCH 的坑在于它是在返回前过滤,不是扫描时过滤。也就是说,即使你只想找user_token*,Redis 仍然会扫描所有槽位,只是把不匹配的丢掉。所以 MATCH 不会减少扫描量,只会减少返回量。如果你的匹配模式命中率很低,遍历整个库的代价依然存在,只是被拆成了多次。
TYPE 参数在 Redis 6.0+ 可用,适合做类型清理。比如只想找所有 hash 类型的 key:
127.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_HOST="127.0.0.1" REDIS_PORT="6379" PATTERN="user_token*" COUNT=200 CURSOR=0 TOTAL=0 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(pattern="user_token*", count=200, batch_callback=None): """ 用 SCAN 游标遍历匹配的 key,避免 KEYS 阻塞 :param pattern: 匹配模式 :param count: 每次扫描的槽位数提示 :param batch_callback: 每批 key 的回调函数,用于处理或统计 :return: 匹配到的 key 总数 """ client = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True) cursor = 0 total = 0 while True: cursor, keys = client.scan(cursor=cursor, match=pattern, count=count) 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(pattern="user_token*", count=200, batch_callback=handle) 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。生产环境建议用连接池,避免每次遍历都新建连接。
这两个脚本的共同点是:游标驱动、分批处理、可中断。你随时可以 Ctrl+C 停掉,不会像 KEYS 那样一旦发出就必须等它跑完。这就是 SCAN 在运维友好性上的核心优势。
4. 用 redis-cli --scan 验证大 key 分布与成功结果
前面讲了命令和脚本,这一节讲怎么验证效果。redis-cli自带一个--scan选项,它内部就是用 SCAN 实现的,适合快速排查。
最基本的用法:
redis-cli --scan --pattern "user_token*" | head -n 20这条命令会持续输出匹配的 key,head -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来排查。下面这个组合命令可以找出匹配前缀里占用内存最大的 key:
redis-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 的表现:同样百万级 key,KEYS 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 required
Redis 设了密码但你没传。命令行加-a yourpassword,Python 里加password="yourpassword"。注意-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/api,Key 填你在控制台创建的 API Key,Model 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 侧先PING,API 侧先curl打一次原始请求。大部分报错都能通过这个顺序定位。
6. 统一 Key 通道与长期编码方案
把 Redis 的 SCAN 用熟之后,你会发现「安全遍历」这个思路在很多地方都通用:不要一次性拉全量,用游标或分页分批处理。这个原则在调用模型 API 时同样适用,尤其是做批量任务的时候。
如果你在做后端开发或缓存运维的同时,还需要接入模型能力,TaoToken 提供统一 Key 和 API 通道,省去逐个平台配置的麻烦。接入信息如下:
- 官网:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- API 地址:https://taotoken.net/api
- 模型对话入口:https://taotoken.net/api/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite
- Coding Plan:https://taotoken.net/api/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite
- 控制台:https://taotoken.net/api/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- API Keys 管理:https://taotoken.net/api/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite
- 接入文档:https://taotoken.net/api/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
- Claude Code 接入:https://taotoken.net/api/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite
配置的时候记住三件套:Base URL 用https://taotoken.net/api,Key 从 API Keys 页面创建,Model ID 按你要用的模型填。这三项在 Claude Code、Cline MCP、Codex auth.json 里都是必须的,缺一个就会报 401 或认证失败。
如果你只是偶尔验证模型效果,用模型对话入口就够了。如果是要长期做编码、跑 Agent 任务,Coding Plan 更合适,配额和调用方式都按长期使用设计。遇到接入问题先查接入文档,里面有各工具的完整配置示例。
回到 Redis 这条线,最后给你一个实用建议:把 SCAN 遍历封装成团队内部的工具函数,统一 COUNT 和错误处理,避免每个人各写一套。生产环境的遍历任务加上限流和日志,记录每次遍历的 key 数量和耗时,方便后续排查。KEYS 不是不能用,但只应该出现在你明确知道库很小的场景里,比如本地开发或测试环境。线上库,一律用 SCAN。