ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Redis 中使用 scan 替换 keys 的使用:TaoToken 统一 Key 通道下的配置与验证

Redis 中使用 scan 替换 keys 的使用:TaoToken 统一 Key 通道下的配置与验证 1. 生产环境里 KEYS 为什么会把 Redis 拖垮Redis 的KEYS pattern命令能做什么一句话它能在一次调用里返回所有匹配的键名写起来爽跑起来要命。适合谁只适合本地调试、离线脚本、键总量几百个以内的玩具场景。一旦放到线上键空间动辄几十万上百万KEYS就是一颗定时炸弹。原因在于 Redis 的命令执行模型。Redis 处理命令是单线程的指核心命令执行路径KEYS会一次性遍历整个键空间遍历期间其他命令全部排队等待。键越多阻塞越久。我见过一个真实案例某业务用KEYS user:token:*做定时清理键量到 80 万时一次KEYS阻塞主线程接近 1.2 秒接口 P99 直接飙红监控上就是一根尖刺。SCAN就是为这个场景设计的替代品。它不是一次性返回全部结果而是基于游标cursor分批返回每次只扫描一小部分把一次长阻塞拆成很多次短操作。代价是调用方要自己循环、自己处理游标、自己接受「同一批数据可能重复返回」的语义。这篇就围绕「怎么把KEYS平滑换成SCAN」展开同时把工具侧的 Key/API 通道用 TaoToken 统一起来让配置和验证都有据可查。核心检索词先摆出来Redis SCAN 替换 KEYS、渐进式遍历、游标遍历、COUNT/MATCH 调优、不阻塞主线程。下面从环境准备讲到可复制配置再到验证和排障。2. TaoToken 前置统一 Key 与 API 通道在动手改代码之前先把「通道」这件事理清楚。很多团队的问题不是不会写SCAN而是 Key 散落在各个配置文件、环境变量、CI 密钥里改一个地方要翻五个仓库。TaoToken 在这里扮演的是统一入口把模型调用、编码工具、控制台管理的 Key 收敛到一处工具侧只需要读一份配置。官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 这个不加 UTM。你需要用到的几个 deep link模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteClaudeCodeAnthropichttps://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite注意TaoToken 是统一的 Key/API 通道不是 Redis 客户端也不替代你的编辑器或 Redis 服务。它解决的是「配置和凭据从哪来、怎么统一管」的问题SCAN逻辑仍然跑在你自己的应用里。为什么要在讲 Redis 的文章里提这个因为「配置与验证」这条线需要可复现。你把 Redis 连接参数、扫描脚本用到的凭据、以及调用模型辅助排查的 Key 都放在同一套通道里换环境时只改一处验证动作才能稳定复现。下面第三节给出的config.toml和settings.json骨架就是按这个思路组织的。3. 可复制配置config.toml 与 settings.json 骨架先给一份config.toml把 Redis 连接和扫描参数集中管理。注意scan_count和scan_match是重点调优项后面第五节会展开。# config.toml [redis] host 127.0.0.1 port 6379 db 0 password timeout_ms 2000 pool_max_idle 16 pool_min_idle 4 [redis.scan] # 每次 SCAN 返回的近似条数不是精确值 scan_count 500 # 匹配模式按业务前缀填写 scan_match user:token:* # 单次全量遍历的最大游标轮数防止死循环 max_iterations 100000 [taotoken] # 统一通道基址凭据从环境变量注入不写死在文件里 api_base https://taotoken.net/api api_key_env TAOTOKEN_API_KEY再给一份settings.json适合 Node/前端工具链或需要 JSON 配置的场景{ redis: { host: 127.0.0.1, port: 6379, db: 0, scan: { count: 500, match: user:token:*, maxIterations: 100000 } }, taotoken: { apiBase: https://taotoken.net/api, apiKeyEnv: TAOTOKEN_API_KEY } }两份配置的字段是一一对应的选你项目里已有的那套即可。凭据一律走环境变量别写进仓库。API Key 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 生成接入细节看 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。配置就绪后Java 侧的核心替换代码如下。这是从redisTemplate.keys(...)迁移过来的标准写法public SetString scanKeys(String pattern, long count) { return redisTemplate.execute((RedisCallbackSetString) connection - { SetString result new HashSet(); ScanOptions options new ScanOptions.ScanOptionsBuilder() .match(pattern) .count(count) .build(); try (Cursorbyte[] cursor connection.scan(options)) { while (cursor.hasNext()) { result.add(new String(cursor.next(), StandardCharsets.UTF_8)); } } catch (IOException e) { log.error(scan cursor close failed, pattern{}, pattern, e); } return result; }); }关键点有三个try-with-resources保证游标释放match传前缀通配count控制每批大小。count不是「返回条数上限」而是 Redis 每次内部扫描的槽位数提示实际返回可能多也可能少别拿它当分页大小用。4. 验证请求与成功结果配置和代码都齐了接下来验证。分两步先确认SCAN真的在渐进返回再确认它不阻塞主线程。第一步用 redis-cli 手动跑一轮游标观察返回结构redis-cli -h 127.0.0.1 -p 6379 --scan --pattern user:token:* --count 500 | head -n 20--scan是 redis-cli 对SCAN的封装输出是纯键名列表。如果你想看原始游标用底层命令redis-cli SCAN 0 MATCH user:token:* COUNT 500返回形如1) 17408 2) 1) user:token:1001 2) user:token:1002 ...第一个元素是下一轮游标第二个是这批键。游标回到0表示遍历结束。这就是渐进式遍历的本质你拿着游标一轮轮问Redis 一轮轮答每轮都很短。第二步验证不阻塞。开两个终端一个持续跑SCAN循环另一个用redis-cli --latency观察延迟redis-cli --latency -h 127.0.0.1 -p 6379跑SCAN期间--latency的 min/max/avg 应该保持平稳max 不会出现几百毫秒的尖刺。作为对比你可以临时跑一次KEYS user:token:*会看到 max 明显跳高。这个对比动作就是最直观的证据。第三步在应用侧打日志确认。给上面的scanKeys加一行计数日志跑一次看总条数和轮数log.info(scan done, pattern{}, totalKeys{}, pattern, result.size());成功结果长这样总条数和DBSIZE里匹配前缀的数量对得上允许少量重复导致的偏差日志里没有IOException--latency全程平稳。到这一步KEYS到SCAN的替换就算验证通过了。5. 本篇常见错排查5.1 COUNT 设太大反而变慢很多人以为COUNT越大越快直接写 50000。实际上COUNT是每轮扫描的槽位提示设得过大单轮耗时上升阻塞窗口变长就失去了渐进的意义。生产环境建议从 100 到 1000 起步键量大、延迟敏感的场景取小值离线批处理可以取大值。我试过在 50 万键的实例上把COUNT从 500 调到 5000单轮耗时从约 0.3ms 涨到约 2ms虽然总量不变但尖刺变明显了。5.2 误以为 SCAN 返回结果不重复SCAN的官方语义是在遍历过程中如果键空间发生变化同一个键可能被返回多次。所以你的收集容器必须是Set而不是List或者收集后去重。用List会导致结果里出现重复键后续清理逻辑可能重复执行。5.3 游标没释放导致连接泄漏上面代码里try (Cursorbyte[] cursor ...)是必须的。如果手动connection.scan()后忘记关闭游标持有的连接不会归还连接池跑一段时间就Could not get a resource from the pool。这个坑很隐蔽因为单次调用看不出问题压测或定时任务跑久了才暴露。5.4 MATCH 模式写错导致扫全库MATCH是过滤不是索引。如果你写成MATCH *或者前缀写错SCAN仍然会遍历整个键空间只是返回结果被过滤掉等于白扫。务必确认前缀和业务键命名一致比如user:token:*别写成user_token_*。5.5 在集群模式下用单机 SCANRedis Cluster 里SCAN只作用于当前节点不会跨分片。你需要对每个 master 节点分别执行SCAN再合并结果。单机写法直接搬到集群会漏键。这个场景建议用SCAN遍历每个节点的游标或者改用redis-cli --cluster相关工具辅助。5.6 把 SCAN 当强一致快照用SCAN不保证遍历期间键空间的一致性。遍历过程中新增或删除的键可能被返回也可能不被返回。如果你的业务需要精确快照SCAN不满足得换其他方案。绝大多数「枚举键做清理/统计」的场景弱一致是可以接受的。6. 收尾把通道和脚本一起固化下来回到最初的问题KEYS阻塞主线程SCAN用游标把长阻塞拆成短操作。这篇给了一条完整链路——配置集中在config.toml/settings.json凭据走 TaoToken 统一通道扫描逻辑用try-with-resources包住游标验证靠--latency对比阻塞排障覆盖了COUNT、重复、泄漏、集群这几个高频坑。如果你后续要把这套扫描脚本接到编码工具或 Agent 里做自动化清理长期编码场景可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。需要边调脚本边问模型走模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。凭据管理在控制台和 API Keys 页接入细节以接入文档为准。最后一个实用技巧把scan_count做成可配置项而不是硬编码不同环境用不同值压测环境调大、生产环境调小这样同一份代码能适配多种负载不用改代码重新发版。
返回列表