news 2026/10/2 23:32:04

RedisTemplate 中使用 scan 方法代替 keys 指令:TaoToken 场景下的渐进式遍历实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RedisTemplate 中使用 scan 方法代替 keys 指令:TaoToken 场景下的渐进式遍历实践

1. 生产环境里 keys 到底踩了什么坑

RedisTemplate里有个方法叫keys,写起来特别顺手,一行redisTemplate.keys("user_info:*")就能把想要的 key 全捞出来。但如果你真在生产环境这么干,运维同学大概率会顺着网线来找你。原因不复杂:Redis 处理命令是单线程的,keys命令会一次性遍历整个键空间,把所有匹配的 key 攒齐了再返回。数据量小的时候你感觉不到,一旦 key 数量到了几十万、上百万,这条命令就会把主线程占住,期间其他请求全部排队等着,表现出来就是接口大面积超时、CPU 飙高,严重的时候直接触发故障。

我见过不少团队在运维规范里直接把keys和flushdb列在一起,属于高危命令,能禁就禁。那问题来了,业务上确实有「按前缀找一批 key」的需求,比如清理某个用户的缓存、统计某类业务数据、做数据迁移,总不能不让用吧。这时候正确的替代方案就是scan。

scan的核心思路是「渐进式遍历」:它不一次性返回所有结果,而是每次返回一小批 key 和一个游标(cursor),你拿着这个游标继续下一次查询,直到游标回到 0 表示遍历结束。这样每次操作只占用很短的时间片,不会长时间阻塞 Redis 主线程。代价是遍历期间如果有 key 被增删,结果可能不精确,但对于绝大多数「找一批 key 做处理」的场景,这个代价完全可以接受。

这篇文章聚焦的就是在RedisTemplate里怎么把scan用对、用好。我会先讲清楚keys的阻塞风险,然后给出可复制的ScanOptions配置模板和完整的 Helper 实现,接着用redis-cli验证游标遍历的完整性,最后把常见的报错和坑一个个排掉。整个过程中,我会结合 TaoToken 统一 Key/API 通道的接入场景,说明在真实项目里怎么把配置和调用串起来。适合正在用 Spring Data Redis、又想把keys换掉的 Java 后端同学。

2. TaoToken 前置:把 Key 和 API 通道先理顺

在动手改代码之前,先把「通道」这件事说清楚。很多同学写 Redis 相关代码时,Key 是散落在各个业务类里的字符串拼接,API 调用地址也是硬编码,改一个环境要翻半天。TaoToken 在这里扮演的角色,是帮你把 Key 管理和 API 访问通道统一起来,让配置集中、可切换、可审计。

先说 Key 的规范。Redis 的 key 最好用「业务前缀 + 分隔符 + 标识」的结构,比如user_info:1001、order_cache:202405。前缀用枚举类或者常量类定义,别在业务代码里手写字符串。这样做的好处是,当你用scan匹配的时候,pattern 直接从前缀常量拼出来,不会因为手滑写错一个字符导致扫不到数据。比如定义一个RedisKeyPrefix枚举:

public enum RedisKeyPrefix { USER_INFO("user_info:"), ORDER_CACHE("order_cache:"), SESSION_TOKEN("session_token:"); private final String prefix; RedisKeyPrefix(String prefix) { this.prefix = prefix; } public String getPrefix() { return prefix; } public String build(String suffix) { return this.prefix + suffix; } }

这样scan的 pattern 就是RedisKeyPrefix.USER_INFO.getPrefix() + "*",清晰又不容易出错。

再说 API 通道。TaoToken 提供统一的 API 入口,地址是https://taotoken.net/api,你可以在控制台里管理访问凭证、查看调用情况。对于需要接入模型对话、编码计划或者 Agent 能力的场景,建议先把 API Key 在控制台里生成好,再在项目配置里引用。控制台地址是https://taotoken.net/console,API Key 管理在https://taotoken.net/api-keys。如果你用的是 Claude Code 这类工具做辅助开发,可以参考https://taotoken.net/claude-code-anthropic的接入说明;需要长期跑编码任务或者 Agent 流程的,可以看https://taotoken.net/coding-plan。

注意:把 Key 和 API 通道集中管理,不是为了好看,而是为了在排查问题时能快速定位是「Key 拼错了」还是「通道配置不对」。后面第五节排错时你会体会到这一点。

配置层面,Spring Boot 项目里 Redis 的连接信息建议放在application.yml,通过环境变量注入敏感值。TaoToken 的 API 凭证同理,不要硬编码在代码里。下面是一个配置片段示例,路径和字段名按你项目实际情况调整:

spring: redis: host: ${REDIS_HOST:127.0.0.1} port: ${REDIS_PORT:6379} password: ${REDIS_PASSWORD:} database: 0 timeout: 3000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 taotoken: api-base: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY:}

把taotoken.api-key通过环境变量注入,本地开发用测试 Key,线上用生产 Key,切换环境只改环境变量,不动代码。这一步做完,后面写scan的时候,Key 前缀和通道配置都是现成的,专注在遍历逻辑本身就行。

3. 可复制的 ScanOptions 配置与 Helper 实现

这一节是核心,直接给可复制的代码。先说ScanOptions的几个关键参数,很多人用错就错在这里。

ScanOptions.scanOptions()提供三个链式方法:match(pattern)设置匹配模式,count(n)设置每次返回的提示数量,type(type)按数据类型过滤。重点说count。网上有些示例写count(Long.MAX_VALUE),这是典型的误用。count不是「总共返回多少」,而是「每次遍历建议返回多少」,它只是一个提示值,Redis 不保证精确返回这么多。你把它设成Long.MAX_VALUE,等于告诉 Redis 一次尽量多返回,那就退化成类似keys的行为了,失去了渐进式遍历的意义。

合理的count值一般在 100 到 1000 之间。太小会导致往返次数多、网络开销大;太大又可能单次阻塞偏久。我一般用 500 起步,根据实际 key 数量和网络情况微调。下面是一个参数对照表:

参数作用推荐值说明
match匹配 key 的模式user_info:*必须带*,否则精确匹配
count每次遍历的提示数量100~1000不是总数,别设成极大值
type按数据类型过滤按需如string、hash,可减少无效遍历

然后是完整的 Helper 实现。相比网上流传的版本,我做了两点改进:一是把count设成合理值,二是把游标遍历封装成可中断、可回调的形式,避免一次性把所有 key 塞进内存。

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @Component public class RedisScanHelper { private static final int DEFAULT_COUNT = 500; @Autowired private StringRedisTemplate stringRedisTemplate; /** * 渐进式遍历,对每个 key 执行 consumer 回调 * 适合 key 数量大、不想一次性加载到内存的场景 */ public void scan(String pattern, Consumer<String> consumer) { scan(pattern, DEFAULT_COUNT, consumer); } public void scan(String pattern, int count, Consumer<String> consumer) { stringRedisTemplate.execute((RedisConnection connection) -> { ScanOptions options = ScanOptions.scanOptions() .match(pattern) .count(count) .build(); try (Cursor<byte[]> cursor = connection.scan(options)) { while (cursor.hasNext()) { String key = new String(cursor.next(), StandardCharsets.UTF_8); consumer.accept(key); } } catch (Exception e) { throw new RuntimeException("scan 遍历失败, pattern=" + pattern, e); } return null; }); } /** * 收集符合条件的 key 到 List * 注意:key 数量极大时慎用,可能占用较多内存 */ public List<String> scanToList(String pattern) { List<String> keys = new ArrayList<>(); scan(pattern, keys::add); return keys; } }

调用的时候,pattern 一定要带*。比如你要找user_info:开头的所有 key:

List<String> keys = redisScanHelper.scanToList( RedisKeyPrefix.USER_INFO.getPrefix() + "*");

如果你只是想对每个 key 做处理,不想攒成 List,直接用回调版本:

redisScanHelper.scan(RedisKeyPrefix.SESSION_TOKEN.getPrefix() + "*", key -> { // 比如逐个删除、逐个统计 stringRedisTemplate.delete(key); });

这里有个细节:connection.scan返回的Cursor实现了Closeable,必须放在 try-with-resources 里,否则连接不会及时释放,跑几次就可能把连接池耗光。网上那个count(Long.MAX_VALUE)的示例还有个隐患,就是它把cursor.forEachRemaining和IOException捕获混在一起,异常处理不够清晰。上面这个版本把异常包成运行时异常并带上 pattern,排查时一眼能看出是哪个前缀出的问题。

提示:scan的游标遍历是「弱一致」的。遍历过程中如果 key 被删除,可能扫不到;如果新增 key,可能扫到也可能扫不到。如果你的业务要求强一致,需要在应用层加锁或者换用其他方案。

4. 用 redis-cli 验证游标遍历完整性

代码写完了,怎么确认scan真的把该扫的 key 都扫到了?最直接的办法是用redis-cli手动跑一遍游标遍历,和你的 Java 结果对照。这一节给你完整的操作步骤。

先准备测试数据。用redis-cli连上实例,批量写入一批带前缀的 key:

redis-cli -h 127.0.0.1 -p 6379

进入交互后,用循环写入 1000 个 key:

for i in $(seq 1 1000); do redis-cli set "user_info:$i" "value_$i"; done

如果你在交互模式里,也可以直接执行:

eval "for i=1,1000 do redis.call('set', 'user_info:'..i, 'v'..i) end" 0

数据准备好后,用scan手动遍历。注意scan的语法是SCAN cursor [MATCH pattern] [COUNT count],第一次游标传 0:

SCAN 0 MATCH user_info:* COUNT 100

返回结果类似:

1) "128" 2) 1) "user_info:1" 2) "user_info:2" ...

第一个值是下一次要传的游标,第二个值是这批返回的 key。你拿着128继续:

SCAN 128 MATCH user_info:* COUNT 100

如此往复,直到返回的游标变成0,表示遍历结束。为了验证完整性,可以把所有返回的 key 收集起来去重后计数,看是不是 1000。手动做太累,可以用一段 shell 脚本自动跑:

#!/bin/bash cursor=0 total=0 while true; do result=$(redis-cli SCAN $cursor MATCH "user_info:*" COUNT 100) cursor=$(echo "$result" | head -1) count=$(echo "$result" | tail -n +2 | wc -l) total=$((total + count)) if [ "$cursor" = "0" ]; then break fi done echo "总共扫描到 $total 个 key"

跑完输出应该是 1000 左右。为什么说「左右」?因为scan在遍历期间如果有数据变动,计数可能略有出入,但静态数据下应该精确等于 1000。如果差得很多,说明你的count或者 pattern 有问题。

再对照 Java 侧的结果。在你的scanToList调用后打印keys.size(),和 shell 脚本的结果比对。两边一致,说明游标遍历逻辑正确。这一步做完,你就有底气把keys从代码里彻底删掉了。

注意:redis-cli的SCAN命令和RedisTemplate的connection.scan底层是同一套游标机制,所以用redis-cli验证是可靠的。但要注意redis-cli默认可能连的是 db0,确认你的 Java 配置里 database 也是 0,否则扫的不是同一个库。

5. 本篇常见错排查:401、local proxy failed 与游标异常

改代码的过程中,报错是少不了的。这一节把几个高频错误拎出来,对照真实报错信息给排查思路。

错误一:NOAUTH Authentication required或 401 类鉴权失败。这个通常不是scan本身的问题,而是 Redis 连接没带密码,或者 TaoToken 的 API Key 没配对。先检查application.yml里的spring.redis.password是否通过环境变量正确注入。如果是 TaoToken 通道侧的 401,去控制台确认 API Key 是否有效、是否过期,地址是https://taotoken.net/api-keys。排查顺序是:先确认 Redis 密码,再确认通道 Key,两者都对了还报 401,就看是不是 Key 里带了多余空格。

错误二:local proxy failed或连接超时。这个报错一般出现在网络层,说明客户端连不上目标地址。检查spring.redis.host和port是否正确,本地能不能telnet通。如果是通过 TaoToken 通道访问 API 时出现,确认taotoken.api-base填的是https://taotoken.net/api,别多写或少写路径。另外注意连接池配置,max-active太小、并发一高就会排队超时,适当调大。

错误三:java.io.IOException: Unexpected end of stream或游标读取中断。这个多半是Cursor没正确关闭,或者遍历过程中连接被回收了。确认你的scan代码用了 try-with-resources,并且没有在遍历中途手动关闭连接。还有一种情况是count设得太大,单次返回数据过多导致读取超时,把count降到 500 以下试试。

错误四:reading choices相关报错。如果你在接入模型对话能力时看到类似error reading choices的提示,这通常和 Redis 无关,而是 API 响应解析问题。检查请求体格式是否符合文档要求,参考https://taotoken.net/doc里的接口说明。模型对话的调试可以在https://taotoken.net/model-chat里先手动验证一遍,确认通道通了再写进代码。

错误五:OAuth相关报错。如果你用的是 Claude Code 之类的工具,接入时可能遇到 OAuth 流程问题。参考https://taotoken.net/claude-code-anthropic的说明,确认回调地址和凭证配置正确。这类问题一般和 Redis 的scan无关,但如果你在同一个项目里既用 Redis 又接模型通道,排查时要分清是哪个环节报的错。

错误六:scan扫不到数据。最常见的原因是 pattern 没带*。scan的match是模式匹配,user_info:只能精确匹配这个 key,user_info:*才能匹配前缀。另外确认 database 选对了,db0 和 db1 的数据是隔离的。还有一种情况是 key 真的不存在,先用EXISTS user_info:1确认一下。

把上面这些对照着排一遍,基本能覆盖 90% 的接入问题。剩下的边角情况,去https://taotoken.net/doc翻文档,或者到控制台看调用日志,通常能找到线索。

6. 把 scan 用稳,从配置到验证走一遍

回到最开始的问题:keys为什么危险,scan为什么是正解。核心就一句话,keys一次性遍历整个键空间,阻塞主线程;scan分批返回、游标推进,把大操作拆成小操作。理解了这一点,你就知道为什么count不能设成Long.MAX_VALUE,为什么Cursor必须关闭,为什么遍历结果是弱一致的。

实际落地的时候,我建议你按这个顺序走一遍:先把 Key 前缀用枚举类管起来,pattern 从常量拼;再把ScanOptions的count设成 500 左右,别贪大;然后用redis-cli手动跑一遍游标遍历,确认能扫全;最后把 Java 侧的结果和 shell 脚本的结果对一下,两边一致再上线。这套流程走下来,keys就可以从你的代码库里彻底消失了。

还有个小技巧:如果你的scan是为了批量删除,别在遍历过程中直接删,因为删除会改变键空间,可能影响游标推进。稳妥的做法是先扫出来存到 List,再分批删。如果 key 实在太多,List 也放不下,那就用回调版本逐个处理,但要注意处理逻辑本身别太慢,否则连接占用时间会拉长。

TaoToken 在这里的价值,是让你的 Key 管理和 API 通道有统一的入口。控制台看调用、API Keys 管凭证、文档查接口,出问题时排查路径清晰。需要长期跑编码或 Agent 任务的,可以了解下 Coding Plan;想先验证模型能力的,去模型对话页面手动试一把。把这些通道理顺了,Redis 的scan只是你工具箱里的一件趁手工具,用对场景、配好参数,它就能稳稳地替你扛住那些「找一批 key」的需求。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 23:31:47

3300V SiC MOSFET模块高浪涌电流能力解析:工业可靠性提升关键

你可能没注意过&#xff0c;3300V的碳化硅&#xff08;SiC&#xff09;MOSFET模块是个“既要又要”的产品&#xff1a;既要耐高压&#xff0c;又要在浪涌电流冲过来的瞬间扛得住。东芝这次推出的3300V SiC MOSFET模块&#xff0c;把高浪涌电流能力作为核心卖点&#xff0c;目标…

作者头像 李华
网站建设 2026/10/2 23:30:05

二三极管原厂封测:从失效分析到来料验证的硬门槛

上个月被朋友拉去做一批电源板的失效分析&#xff0c;问题出得很“经典”&#xff1a;整机老化十几小时后&#xff0c;有几台电源死活启动不了&#xff0c;查下来是三极管饱和压降大得离谱&#xff0c;封装表面丝印模糊&#xff0c;引脚还有明显的二次上锡痕迹。拆开管壳一看&a…

作者头像 李华
网站建设 2026/10/2 23:28:30

工程师成长之路:从入门到独立负责的实战复盘

先说结论&#xff1a;这篇文章是写给那些正在犹豫要不要走工程师这条路、或者刚上路没多久还比较迷茫的同学的。我自己就是一路踩坑走过来的人&#xff0c;从刚开始连 Git 都不会用的纯小白&#xff0c;到后来能独立负责模块、带新人、做技术方案&#xff0c;中间确实有不少值得…

作者头像 李华
网站建设 2026/10/2 23:28:29

AP5193宽压恒流LED驱动芯片:车载与电动车照明方案全解析

做车载和电动车LED照明的工程师&#xff0c;估计都碰到过这种场景&#xff1a;客户甩过来一个需求&#xff0c;电源电压跨度大得离谱&#xff0c;一会儿12V的乘用车&#xff0c;一会儿又窜到72V的电摩&#xff0c;还要求亮度必须一致、不能频闪、器件还不能多。以前遇到这种案子…

作者头像 李华