☰
集群模式下RedisTemplate使用Scan命令全节点模糊匹配key:TaoToken统一Key通道下的可复制配置与验证
2026/10/9 22:29:29 网站建设 项目流程

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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api (这个不加 UTM)。你需要先在控制台创建对应的 Key,控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,创建完在 API Keys 页面拿到密钥,页面是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。如果你后面还要接模型对话做调试辅助,可以看模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite ;如果是长期编码或 Agent 场景,Coding Plan 在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ;接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。

这里要强调一点: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: 3000ms

REDIS_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 RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, 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 RedisTemplate<String, String> redisTemplate; public RedisClusterScanUtil(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; } public Set<String> scanMatch(String matchPattern) { Set<String> keys = new HashSet<>(); RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory(); if (connectionFactory == null) { return keys; } RedisClusterConnection clusterConnection = connectionFactory.getClusterConnection(); Iterable<RedisClusterNode> nodes = clusterConnection.clusterGetNodes(); for (RedisClusterNode node : nodes) { if (node.isMaster()) { Cursor<byte[]> 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 也有 slave,scan 只需要在 master 上做,slave 的数据是复制的,扫了会重复,所以加node.isMaster()判断。第二,count(1000)是每次扫描的建议数量,不要设太大,设成 10000000 这种极端值反而可能让单次返回过大、内存吃紧,1000 到 5000 是比较稳的区间。第三,cursor.close()一定要调,否则连接资源可能泄漏。

如果你用的是RedisTemplate的execute回调方式,也可以写成:

Set<String> keys = redisTemplate.execute((RedisCallback<Set<String>>) connection -> { Set<String> result = new HashSet<>(); RedisClusterConnection clusterConnection = (RedisClusterConnection) connection; for (RedisClusterNode node : clusterConnection.clusterGetNodes()) { if (!node.isMaster()) continue; Cursor<byte[]> 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 结果可能短暂不完整。生产环境做批量操作时,尽量避开扩容窗口,或者加重试。这个坑不常遇到,但遇到一次就够记一辈子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询