Redis scan 踩坑实录:把 Cursor Base URL 改到 TaoToken 后的大 key 遍历排查

发布时间:2026/10/7 7:41:35
Redis scan 踩坑实录:把 Cursor Base URL 改到 TaoToken 后的大 key 遍历排查 1. 从一次线上告警说起Redis scan 遍历大 key 到底慢在哪Redis 的SCAN命令常被当成KEYS的安全替代品官方文档也强调它每次只返回少量元素、不会阻塞服务器。但真到业务里尤其是库里存在几十万甚至上百万 key、还有若干超大 hash 和 zset 的时候SCAN的坑一点都不比KEYS少。我遇到的那次告警很典型一个后台清理任务用SCAN遍历全库跑了四十多分钟还没结束CPU 单核打满业务侧的读请求 P99 从 8ms 涨到 200ms 以上。问题出在哪SCAN的游标机制本身是无状态的服务器不记录迭代进度全靠客户端拿着返回的游标继续下一次调用。这意味着两件事第一遍历的完整性依赖你正确传递游标第二COUNT只是提示不是硬性保证。当数据集底层是 hashtable 时COUNT大致决定每次扫描的槽位数但当遇到 intset、ziplist、listpack 这类编码时SCAN可能一次就把整个小集合返回也可能无视COUNT。更麻烦的是MATCH是在取出元素之后才做模式过滤的所以如果你用MATCH user:*去扫一个百万级库绝大多数迭代会返回空列表但每次调用依然要扫描COUNT个槽位网络往返和 CPU 开销一点没省。这次排查还有个特殊背景我当时的调试环境把 Cursor 的 Base URL 改到了 TaoToken 的统一通道用同一套 Key 和 API 入口去复现问题。这样做的好处是本地脚本、Cursor 里的代码补全、以及线上排查用的 CLI 工具走的是同一个模型和 API 通道复现步骤不会因为环境差异而漂移。下面我把整个排查过程拆成可复制的步骤包括SCAN参数怎么调、游标怎么校验、以及怎么用压测确认瓶颈到底在 Redis 还是在客户端。先明确适用人群如果你在用 Redis 做缓存或队列库里有大 key并且写过SCAN遍历脚本这篇内容能帮你避开我踩过的坑。核心检索词就是 Redis scan 大 key 遍历性能排查下面所有操作都围绕它展开。2. 把 Cursor Base URL 切到 TaoToken统一 Key 与 API 通道的前置准备在开始复现SCAN问题之前先解决环境一致性的问题。我本地用 Cursor 写排查脚本同时用命令行跑redis-cli如果 Cursor 里的模型请求走的是默认通道而脚本里的 API 调用走另一个通道两边看到的报错和超时行为可能对不上。把 Cursor 的 Base URL 改到 TaoToken 之后模型对话、代码补全、以及脚本里的 API 请求都走同一个入口排查时少了一层变量。TaoToken 在这里的角色是统一的 API 通道官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。注意 API 地址后面不加 UTM 参数直接用于配置 Base URL。你需要先拿到一个 API Key在控制台里创建地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content Key 的管理页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Cursor 的配置入口在 Settings 里的 Models 面板找到 OpenAI API Key 那一栏把 Override OpenAI Base URL 打开填入https://taotoken.net/api然后在 API Key 里粘贴你创建的 Key。如果你用的是 Claude Code 或者 Cline 这类工具配置逻辑类似Base URL 都指向同一个 API 入口。模型 ID 根据你实际使用的模型填写比如claude-sonnet-4-20250514或者gpt-4o具体以控制台里可用的模型列表为准。这里有个细节要注意Cursor 的 Base URL 如果填成带路径的形式比如https://taotoken.net/api/v1可能会因为版本差异导致 404。我实测下来直接填https://taotoken.net/api最稳。改完之后在 Cursor 里发一条测试消息确认能正常返回再继续后面的 Redis 排查。如果你更习惯用命令行验证模型通道可以打开模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 直接测试。为什么要先做这一步因为后面写游标校验脚本时我会让 Cursor 辅助生成代码同时脚本里可能调用模型做日志分析。如果 Base URL 没统一脚本跑出来的超时可能来自模型通道而不是 Redis排查方向就偏了。统一到 TaoToken 之后所有请求的入口一致日志里的错误码也能对得上。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 遇到配置问题可以先翻文档。3. 可复制的 scan 参数配置与游标校验脚本这一节直接给可复制的配置和脚本。先看SCAN的参数选择。默认COUNT是 10对于大库来说太小网络往返次数会爆炸。我的经验值是如果库里有 50 万以上的 keyCOUNT设 500 到 1000 比较合适如果只是几万 keyCOUNT设 100 到 200 就够。但COUNT不是越大越好设成 10000 会导致单次调用耗时上升可能触发 Redis 的慢查询阈值。下面是一个 Python 脚本用redis-py做完整遍历同时记录每次迭代的游标、返回元素数量、耗时用来定位瓶颈。脚本里还加了游标校验逻辑防止因为游标传递错误导致死循环。import redis import time r redis.Redis(host127.0.0.1, port6379, db0, decode_responsesTrue) def scan_full(match_pattern*, count500, max_iter100000): cursor 0 total_keys 0 iteration 0 start time.time() seen_cursors set() while True: iteration 1 if iteration max_iter: print(f[WARN] 超过最大迭代次数 {max_iter}强制退出可能游标异常) break t0 time.time() cursor, keys r.scan(cursorcursor, matchmatch_pattern, countcount) elapsed (time.time() - t0) * 1000 total_keys len(keys) if iteration % 100 0 or len(keys) 0: print(fiter{iteration} cursor{cursor} keys{len(keys)} cost{elapsed:.1f}ms) if cursor 0: break if cursor in seen_cursors: print(f[ERROR] 游标重复出现: {cursor}迭代可能陷入循环) break seen_cursors.add(cursor) total_time time.time() - start print(f遍历结束: 迭代 {iteration} 次, 返回 key 总数 {total_keys}, 总耗时 {total_time:.2f}s) return total_keys, total_time if __name__ __main__: scan_full(match_patternuser:*, count500)这个脚本的关键点有三个。第一seen_cursors集合用来检测游标是否重复正常情况下SCAN的游标不会重复如果重复说明客户端传参有问题或者 Redis 版本有 bug。第二max_iter是兜底防止无限循环。第三每次迭代都记录耗时如果某几次迭代耗时突然飙到几百毫秒说明那几次扫描到了大 key 或者哈希冲突严重的槽位。如果你用 Cursor 辅助写这个脚本可以把 Base URL 配好之后直接让 Cursor 帮你补全redis-py的异常处理。比如连接超时、BUSY错误、以及MOVED重定向如果你用了集群。集群模式下SCAN的行为和单机不同需要每个节点单独遍历游标是节点级别的。这一点在排查时很容易忽略如果你用的是 Redis Cluster记得对每个 master 节点分别执行SCAN。再给一个redis-cli的快速验证命令用来确认游标行为redis-cli --scan --pattern user:* --count 500 | head -n 20--scan是redis-cli内置的遍历模式底层就是循环调用SCAN。如果你发现--scan跑得比自己的脚本快很多那问题大概率在脚本的游标处理或者网络往返上而不是 Redis 本身。对于大 key 的识别可以在遍历过程中对返回的 key 做类型判断和大小估算。比如对 hash 类型用HLEN对 zset 用ZCARD对 list 用LLEN。如果某个 key 的元素数量超过 5000就标记为大 key单独记录。下面这段可以追加到上面的脚本里def check_key_size(r, key, threshold5000): ktype r.type(key) size 0 if ktype hash: size r.hlen(key) elif ktype zset: size r.zcard(key) elif ktype list: size r.llen(key) elif ktype set: size r.scard(key) elif ktype string: size r.strlen(key) if size threshold: print(f[BIGKEY] {key} type{ktype} size{size}) return size把check_key_size放到遍历循环里对每个返回的 key 调用一次。注意这会产生额外的 Redis 调用如果 key 数量很大会显著增加总耗时。建议只在排查阶段开启生产环境用--bigkeys或者MEMORY USAGE采样。4. 验证请求与成功结果游标完整性、耗时分布与压测动作脚本跑起来之后怎么判断结果是正常的我关注三个指标游标是否最终归零、返回 key 总数是否和DBSIZE接近、以及耗时分布是否均匀。先看游标完整性。正常情况下SCAN从0开始每次返回一个新游标直到某次返回0表示结束。如果你在日志里看到游标在某几个值之间来回跳或者迭代了几万次还没归零那就要检查是不是MATCH模式太窄导致大量空迭代。比如MATCH user:*在百万级库里如果user:前缀的 key 只占 1%那么 99% 的迭代会返回空列表但游标依然在推进。这种情况下迭代次数会远大于DBSIZE / COUNT总耗时也会被网络往返拖长。验证方法很简单先用DBSIZE拿到总 key 数再用不带MATCH的SCAN跑一遍对比返回的 key 总数。如果两者差距在 5% 以内说明遍历基本完整。差距过大通常是因为遍历期间有 key 被删除或新增SCAN的弱一致性保证允许这种情况。redis-cli DBSIZE redis-cli --scan --count 1000 | wc -l耗时分布方面我在脚本里每 100 次迭代打印一次累计耗时。如果发现前 10% 的迭代耗时占了总耗时的 80%说明大 key 集中在某些槽位。这时候可以用CLUSTER KEYSLOT或者SLOWLOG进一步定位。SLOWLOG GET 10能看到最近 10 条慢查询如果里面有SCAN或者HGETALL之类的命令就能确认瓶颈。压测动作我一般分两轮。第一轮用COUNT100第二轮用COUNT1000对比总耗时和单次最大耗时。如果COUNT1000的总耗时反而更长说明单次扫描的 CPU 开销超过了网络往返的节省这时候应该把COUNT降回来。下面是一个简单的压测脚本片段for count in [100, 500, 1000, 2000]: print(f--- COUNT{count} ---) scan_full(match_pattern*, countcount, max_iter50000)跑完四轮把结果填到表格里对比COUNT迭代次数总耗时(s)单次最大耗时(ms)返回 key 总数100520018.34551200050010506.712051180010005305.921051210020002707.4480511900从这组数据能看出COUNT1000时总耗时最低但单次最大耗时已经到 210ms接近慢查询阈值。如果业务对延迟敏感COUNT500更稳妥。COUNT2000虽然迭代次数少但单次耗时太高反而拖慢整体。成功的结果是游标最终归零返回 key 总数和DBSIZE偏差小于 5%单次最大耗时低于 200ms总耗时在可接受范围内。如果这些条件都满足说明SCAN配置是合理的。如果单次最大耗时超过 500ms就要考虑把大 key 拆开或者改用HSCAN分批处理。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth排查过程中遇到的报错不止 Redis 本身的还有环境配置引发的。我把几个高频错误列出来对照着看能省不少时间。第一个是401 Unauthorized。如果你在 Cursor 里配了 TaoToken 的 Base URL但 API Key 填错或者过期发请求时会直接返回 401。这时候检查https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content里的 Key 状态确认没有过期。如果 Key 没问题检查 Base URL 是不是多写了/v1或者末尾多了斜杠。正确的写法是https://taotoken.net/api不带路径。第二个是local proxy failed。这个报错通常出现在你本地开了某些网络工具导致请求被拦截或者转发失败。排查方法是先关掉本地代理直接用命令行curl测试 API 入口curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer YOUR_KEY \ -H Content-Type: application/json \ -d {model:claude-sonnet-4-20250514,messages:[{role:user,content:ping}]}如果curl能通但 Cursor 里报local proxy failed那就是 Cursor 的代理设置和系统代理冲突。在 Cursor 设置里把 Proxy 关掉或者改成no proxy。第三个是reading choices相关的报错比如Error reading choices: unexpected end of JSON input。这通常是因为 API 返回的响应体不完整可能是网络中断或者超时。如果你在脚本里调用模型分析 Redis 日志把超时时间设长一点比如 60 秒。另外检查请求体里的max_tokens是不是设得太小导致响应被截断。第四个是 OAuth 相关错误。如果你用 Claude Code 或者某些 CLI 工具它们可能默认走 OAuth 流程而不是 API Key。这时候需要在配置里显式指定 API Key 模式。以 Claude Code 为例配置文件通常在~/.claude/settings.json或者项目根目录的.claude/settings.json里面需要写清楚 Base URL、API Key 和 Model ID 三件套{ apiKey: YOUR_TAOTOKEN_KEY, baseUrl: https://taotoken.net/api, model: claude-sonnet-4-20250514 }如果你用的是 Cline 或者 CC Switch 这类工具配置项名称可能不同但核心三件套是一样的Base URL 指向https://taotoken.net/apiKey 用控制台创建的Model ID 按实际模型填。缺任何一个都会导致请求失败。还有一个容易忽略的点Redis 连接本身的报错。比如NOAUTH Authentication required说明 Redis 设了密码但脚本没传。MOVED错误说明你连的是集群节点但没开集群模式。READONLY说明你连到了从节点。这些错误和 API 通道无关但排查时容易混淆建议先确认 Redis 连接正常再去看模型通道的报错。6. 语义一致的 CTA按场景选择入口排查完SCAN的问题之后如果你想把整套流程固化下来比如让 Cursor 长期辅助写 Redis 排查脚本或者用 Agent 自动分析慢查询日志可以考虑 Coding Plan。入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 适合需要长期编码和 Agent 协作的场景。如果你只是想快速验证某个模型对 Redis 日志的分析效果用模型对话页面就行https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。把慢查询日志贴进去让模型帮你归类大 key 的类型和分布。接入过程中遇到配置问题先翻接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Key 的管理和创建在 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Claude Code 相关的配置参考https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。最后说一个我踩过的坑SCAN的COUNT参数在 Redis 集群模式下每个节点是独立计算的。如果你有 3 个 master 节点总迭代次数大致是单节点的 3 倍。排查时别只看总耗时要分节点看。另外MATCH模式尽量写具体前缀避免用*开头否则每个节点都要全量扫描槽位性能会差很多。