恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
集群模式下RedisTemplate使用Scan命令全节点模糊匹配key:TaoToken统一Key通道下的可复制配置与验证
首页
资讯中心
/
集群模式下RedisTemplate使用Scan命令全节点模糊匹配key:TaoToken统一Key通道下的可复制配置与验证
集群模式下RedisTemplate使用Scan命令全节点模糊匹配key:TaoToken统一Key通道下的可复制配置与验证
发布时间:2026/10/9 22:29:32
1. 集群里 keys 命令为什么不能乱用一次线上卡顿的复盘先说结论在 Redis 集群模式下RedisTemplate直接调keys做模糊匹配基本等于给自己埋雷。keys是单线程阻塞式遍历数据量一上来整个节点会被它占住其他请求全部排队。我见过一个缓存实例里大概几十万个 key运维同学手一抖执行了keys md:*接口 P99 从 20ms 飙到 3s 以上监控告警直接炸了。那不用keys用什么官方给的是scan。scan用游标分批返回每次只取一小撮不会长时间占住主线程。语法是scan cursor [MATCH pattern] [COUNT count]其中cursor是游标位置MATCH后面跟模糊匹配模式COUNT是每次建议扫描的数量注意是建议不保证精确。它返回一个数组第一个元素是下一次要传的游标第二个元素是这一批匹配到的 key 列表。当返回的游标为0时表示遍历结束。但问题来了scan只能作用于单个节点。Redis 集群把数据分片到多个 master 节点上你在任意一个节点执行scan只能扫到这个节点负责的槽位里的 key其他节点的数据完全看不到。这就是为什么很多人写完redisTemplate.scan()发现结果少了一大半——不是代码写错了是集群本身就不支持跨节点的单次 scan。所以真正要解决的问题是怎么在集群模式下用RedisTemplate遍历所有节点把每个节点上匹配的 key 聚合起来。这篇就围绕这个场景把可复制的配置、游标遍历代码、节点聚合逻辑以及本地验证步骤一次讲清楚。同时我会结合 TaoToken 统一 Key/API 通道把多环境下的连接凭据管理也顺带落地避免你在测试、预发、生产之间来回改配置。适合谁看正在用 Spring Boot Redis 集群、需要按前缀或模式批量找 key 的后端同学被keys坑过、想换成scan但发现集群扫不全的同学以及想把 Redis 连接配置和密钥统一管理起来、不想把密码硬编码在 yml 里的同学。核心检索词先摆出来RedisTemplate、Scan 命令、集群模式、模糊匹配 key。下面从环境准备开始一步步来。2. TaoToken 统一 Key 通道准备多环境凭据不再散落各处在写代码之前先把连接凭据这件事理顺。很多项目里 Redis 密码、API Key 这类敏感信息散落在application-dev.yml、application-prod.yml、甚至硬编码在某个常量类里改一次要翻好几个文件还容易提交到仓库。我的做法是用 TaoToken 作为统一的 Key/API 通道把多环境凭据集中管理代码里只引用环境变量或配置中心下发的值。TaoToken 官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 这个不加 UTM。你需要先在控制台创建对应的 Key控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建完在 API Keys 页面拿到密钥页面是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。如果你后面还要接模型对话做调试辅助可以看模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 如果是长期编码或 Agent 场景Coding Plan 在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。这里要强调一点TaoToken 是统一的 Key/API 通道用来管理你调用各类服务时的凭据不是让你拿它去替代 Redis 本身。Redis 集群还是你自己的 Redis 集群TaoToken 负责的是把访问凭据、多环境配置统一收口代码里通过环境变量读取避免明文散落。具体操作上我一般这样做在 TaoToken 控制台为 dev、staging、prod 分别建 Key命名带上环境前缀比如redis-dev、redis-prod。然后在项目的配置里用占位符引用环境变量spring: redis: cluster: nodes: - ${REDIS_NODE_1:127.0.0.1:7001} - ${REDIS_NODE_2:127.0.0.1:7002} - ${REDIS_NODE_3:127.0.0.1:7003} password: ${REDIS_PASSWORD} timeout: 3000msREDIS_PASSWORD这个环境变量在本地开发时从 TaoToken 控制台复制对应环境的 Key 值填进去在 CI/CD 里通过密钥管理注入。这样代码仓库里永远看不到明文密码切换环境也只改环境变量不动代码。如果你用的是 Spring Cloud Config 或者 Nacos 这类配置中心也可以把 TaoToken 下发的值作为配置源之一。核心思路就一句话凭据集中管代码只读变量。这一步做完后面的 RedisTemplate 配置和 scan 代码才有干净的基础。顺便提一下如果你的项目里还涉及调用大模型做代码辅助或日志分析TaoToken 的同一套 Key 通道也能覆盖不用再单独维护一套密钥体系。接入文档里有完整的 Base URL、Key、Model ID 三件套说明照着配就行。3. 可复制配置RedisTemplate 集群配置与 Scan 聚合代码这一节是重点直接给可复制的片段。先看 RedisTemplate 的集群配置。Spring Boot 下集群模式用RedisClusterConfiguration配合 Lettuce 连接池。下面是一个完整的配置类import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.util.Arrays; Configuration public class RedisClusterConfig { Bean public RedisConnectionFactory redisConnectionFactory() { RedisClusterConfiguration clusterConfig new RedisClusterConfiguration( Arrays.asList( 127.0.0.1:7001, 127.0.0.1:7002, 127.0.0.1:7003 ) ); clusterConfig.setPassword(${REDIS_PASSWORD}); clusterConfig.setMaxRedirects(3); return new LettuceConnectionFactory(clusterConfig); } Bean public RedisTemplateString, String redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, String template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer serializer new StringRedisSerializer(); template.setKeySerializer(serializer); template.setValueSerializer(serializer); template.setHashKeySerializer(serializer); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }注意setPassword那里实际项目里从环境变量读不要写死。maxRedirects是集群重定向次数默认就行网络抖动多的话可以调到 5。接下来是核心跨节点 scan 聚合。思路是拿到RedisClusterConnection通过clusterGetNodes()获取所有节点然后对每个节点单独执行scan把结果合并到一个 Set 里。代码如下import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ScanOptions; import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.Set; public class RedisClusterScanUtil { private final RedisTemplateString, String redisTemplate; public RedisClusterScanUtil(RedisTemplateString, String redisTemplate) { this.redisTemplate redisTemplate; } public SetString scanMatch(String matchPattern) { SetString keys new HashSet(); RedisConnectionFactory connectionFactory redisTemplate.getConnectionFactory(); if (connectionFactory null) { return keys; } RedisClusterConnection clusterConnection connectionFactory.getClusterConnection(); IterableRedisClusterNode nodes clusterConnection.clusterGetNodes(); for (RedisClusterNode node : nodes) { if (node.isMaster()) { Cursorbyte[] cursor clusterConnection.scan( node, ScanOptions.scanOptions() .match(matchPattern) .count(1000) .build() ); while (cursor.hasNext()) { keys.add(new String(cursor.next(), StandardCharsets.UTF_8)); } try { cursor.close(); } catch (Exception ignored) { } } } return keys; } }几个关键点解释一下。第一clusterGetNodes()返回的节点里既有 master 也有 slavescan 只需要在 master 上做slave 的数据是复制的扫了会重复所以加node.isMaster()判断。第二count(1000)是每次扫描的建议数量不要设太大设成 10000000 这种极端值反而可能让单次返回过大、内存吃紧1000 到 5000 是比较稳的区间。第三cursor.close()一定要调否则连接资源可能泄漏。如果你用的是RedisTemplate的execute回调方式也可以写成SetString keys redisTemplate.execute((RedisCallbackSetString) connection - { SetString result new HashSet(); RedisClusterConnection clusterConnection (RedisClusterConnection) connection; for (RedisClusterNode node : clusterConnection.clusterGetNodes()) { if (!node.isMaster()) continue; Cursorbyte[] cursor clusterConnection.scan(node, ScanOptions.scanOptions().match(md:v2:pd:preSell:*).count(2000).build()); while (cursor.hasNext()) { result.add(new String(cursor.next(), StandardCharsets.UTF_8)); } } return result; });两种写法效果一样看团队习惯。我倾向第一种抽成工具类方便单测。关于匹配模式Redis 的MATCH支持?任意单字符、*任意长度字符、[]字符集合。比如你的 key 长这样md:v2:pd:preSell:storeCodeAndSkuId[storeCode,skuId]:[0008,63168] md:v2:pd:preSell:storeCodeAndSkuId[storeCode,skuId]:[0008,12685]想匹配门店 0008 下的所有商品模式可以写成md:v2:pd:preSell:storeCodeAndSkuId?storeCode,skuId?:?0008,*?注意[和]在 Redis 的匹配语法里是特殊字符表示字符集合所以这里用?代替单个字符来绕过。如果你确实要匹配字面量的方括号需要转义但转义规则在不同客户端里表现不一致最稳的办法还是用?或*绕开。配置和代码都齐了下一节讲怎么在本地验证结果是否完整。4. 本地验证起一个三节点集群跑通 scan 并核对结果光有代码不够得验证。本地起一个三主三从的 Redis 集群用 Docker 最快。下面是我常用的 compose 片段只列三个 master从节点可自行加version: 3.8 services: redis-7001: image: redis:7.2 command: redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes-7001.conf --cluster-node-timeout 5000 --appendonly yes ports: - 7001:7001 redis-7002: image: redis:7.2 command: redis-server --port 7002 --cluster-enabled yes --cluster-config-file nodes-7002.conf --cluster-node-timeout 5000 --appendonly yes ports: - 7002:7002 redis-7003: image: redis:7.2 command: redis-server --port 7003 --cluster-enabled yes --cluster-config-file nodes-7003.conf --cluster-node-timeout 5000 --appendonly yes ports: - 7003:7003起来之后进任意一个容器执行集群创建redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 --cluster-replicas 0然后灌一批测试数据。注意集群模式下 key 会按槽位分散到不同节点所以你要多写几个不同前缀的 key确保它们落在不同节点上。可以用redis-cli -c的集群模式批量写for i in $(seq 1 50); do redis-cli -c -p 7001 set md:v2:pd:preSell:storeCodeAndSkuId[storeCode,skuId]:[0008,$i] value-$i done for i in $(seq 1 30); do redis-cli -c -p 7001 set md:v2:pd:preSell:storeCodeAndSkuId[storeCode,skuId]:[0009,$i] value-$i done写完后先确认数据确实分散了。分别连三个节点执行dbsize你会看到每个节点的 key 数量不一样说明分片生效了。接下来跑我们的scanMatch方法匹配模式用md:v2:pd:preSell:storeCodeAndSkuId?storeCode,skuId?:?0008,*?预期输出是 50 个 key全部是门店 0008 的。如果你只扫单个节点可能只能拿到十几个这就是跨节点聚合的价值。验证的时候有个细节要注意scan的COUNT只是建议值实际返回数量可能多也可能少所以判断完整性不能靠单次返回数量要靠游标是否归零。我们的代码里while (cursor.hasNext())会一直读到游标结束所以结果是完整的。再给一个核对脚本用原生redis-cli在每个节点上分别 scan把结果合并和 Java 代码的结果对比for port in 7001 7002 7003; do redis-cli -p $port --scan --pattern md:v2:pd:preSell:storeCodeAndSkuId?storeCode,skuId?:?0008,*? done | sort -u | wc -l如果这个数字和 Java 代码返回的 Set 大小一致说明聚合逻辑没问题。实测下来两边结果一致才算真正跑通。另外验证时建议把count调小一点比如 10这样能观察到游标多次返回的过程确认循环逻辑正确。生产环境再调回 1000 左右。5. 常见报错排查401、local proxy failed、reading choices、OAuth这一节把实际会撞到的报错列出来对照着查。报错一NOAUTH Authentication required或 401。这是密码没配对。检查RedisClusterConfiguration.setPassword()里的值是否和环境变量一致。如果你用 TaoToken 管理凭据确认当前环境变量指向的是对应环境的 Key别把 dev 的密码用到 prod 上。另外集群模式下每个节点密码要一致有一个节点密码不同就会报这个。报错二local proxy failed或连接超时。这个通常出现在本地连远程集群时集群返回的节点地址是内网 IP你本地路由不到。解决办法是在配置里显式指定节点映射或者用支持集群感知的客户端配置。Lettuce 下可以自定义RedisClusterClientOptions把节点地址做一次转换。如果是 Docker 本地集群确认端口映射没漏。报错三reading choices相关异常。这个一般不是 Redis 本身的错而是你在 scan 结果处理时把返回的byte[]直接当字符串用了编码不对导致解析异常。统一用StandardCharsets.UTF_8转别用平台默认编码。如果 key 里有中文更要注意。报错四OAuth 或鉴权失败。如果你是通过 TaoToken 的 Key 通道去调用某些带鉴权的服务出现 OAuth 报错先检查 Key 是否过期、是否绑定了正确的环境。控制台里可以重新生成 Key。注意 TaoToken 的 Key 是用于统一通道鉴权的和 Redis 自身的密码是两回事别混在一起排查。报错五scan 返回结果为空但明明有数据。九成是匹配模式写错了。Redis 的MATCH是 glob 风格不是正则。[]?*都有特殊含义。如果你要匹配的 key 里本身含这些字符要么用?绕要么用\转义但转义在集群客户端里行为可能不一致。最稳的办法是先用*全匹配确认能扫到数据再逐步加模式。报错六Cursor未关闭导致连接耗尽。这个不报错但跑一段时间后连接池满了。确保每个cursor用完调close()或者用 try-with-resources。排查顺序建议先确认连接通不通ping再确认密码对不对再确认节点是否都扫到了最后才怀疑匹配模式。大部分问题在前两步就能定位。6. 把 Key 通道和 Scan 聚合一起用起来到这里集群模式下用RedisTemplate跨节点 scan 模糊匹配 key 的完整链路就走通了。回顾一下关键点scan只能扫单节点集群必须自己遍历所有 master 节点聚合count是建议值别设太大匹配模式是 glob 不是正则方括号要绕开游标要读到归零才算完整。凭据管理这块用 TaoToken 统一 Key 通道把多环境的 Redis 密码、以及可能用到的其他服务 Key 收口代码里只读环境变量切换环境不动代码。控制台建 Key、API Keys 页面取密钥、接入文档看三件套这几个入口前面都给过了。如果你后面还要做更复杂的缓存治理比如按前缀批量清理、按模式统计 key 数量这套 scan 聚合工具类可以直接复用。把匹配模式参数化封装成一个通用方法团队里谁需要谁调。最后留一个我踩过的坑集群扩容或缩容后节点列表会变clusterGetNodes()拿到的节点可能包含正在迁移的槽位这时候 scan 结果可能短暂不完整。生产环境做批量操作时尽量避开扩容窗口或者加重试。这个坑不常遇到但遇到一次就够记一辈子。