本文记录一次在生产环境使用 Redis Cluster 时踩到的两个典型坑:
SCAN 报错 + DEL 多 key 报错,并深入分析背后的 slot 机制。
一、问题背景
业务场景很简单:
清理某类“按月份存储”的缓存数据
key 结构如下:
biz:module:monthly:data:04/2026
biz:module:monthly:data:03/2026
...
目标:
👉 删除当前月及之前 N 个月的数据
二、第一版实现(使用 SCAN)
public void clearRedisData() {
List<String> scanKeys = redisUtil.scan("biz:module:monthly:data:", 50);
if (CollectionUtils.isEmpty(scanKeys)) {
return;
}
redisUtil.del(scanKeys.toArray(new String[0]));
}
❌ 生产报错
JedisCluster only supports SCAN commands with MATCH patterns containing hash-tags
🔍 原因分析
在 Redis Cluster 中:
❗ JedisCluster.scan() 只支持带 hash tag 的 pattern
例如:
{tag}:*
而我们用的是:
biz:module:monthly:data:*
不包含 {},所以直接被客户端拒绝
三、第二版实现(放弃 SCAN,改为穷举)
既然 key 是按月份生成的,可以直接构造:
private List<String> buildKeys(int monthsBefore) {
List<String> keys = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/yyyy");
YearMonth now = YearMonth.now();
for (int i = 0; i <= monthsBefore; i++) {
keys.add("biz:module:monthly:data:" + now.minusMonths(i).format(formatter));
}
return keys;
}
然后:
redisUtil.del(keys.toArray(new String[0]));
❌ 再次报错
No way to dispatch this command to Redis Cluster because keys have different slots.
四、问题本质:Redis Cluster 的 slot 机制
1️⃣ 什么是 slot?
Redis Cluster 将所有 key 分布到:
16384 个 slot(0 ~ 16383)
每个 key 会被映射到一个 slot:
slot = CRC16(key) % 16384
2️⃣ 为什么会报错?
你的 key:
biz:module:monthly:data:04/2026
biz:module:monthly:data:03/2026
虽然前缀一样,但:
CRC16(...) ≠ CRC16(...)
👉 slot 不同
👉 分布在不同节点
3️⃣ Redis Cluster 的核心限制
多 key 操作必须满足:
所有 key 必须在同一个 slot
否则:
❌ 无法路由 → 报错
五、关键误区
❌ 误区1:前缀一样 → slot 一样
错误!
biz:module:monthly:data:04/2026
biz:module:monthly:data:03/2026
slot 不一样 ❌
❌ 误区2:在同一个节点就可以
错误!
Redis 只看 slot,不看节点
六、正确理解:hash tag({})
1️⃣ hash tag 规则
如果 key 中有 {}:
slot = CRC16(括号里的内容)
2️⃣ 示例
{monthly_data}:04/2026
{monthly_data}:03/2026
实际参与 hash 的是:
monthly_data
所有 key → 同一个 slot ✅
3️⃣ 结论
| key形式 | 是否同slot |
|---|---|
| biz:xxx:A / biz:xxx:B | ❌ 不同 |
| {biz:xxx}:A / {biz:xxx}:B | ✅ 相同 |
七、最终解决方案
✅ 方案一(当前最优):逐个删除
public void clearRedisData() {
List<String> keys = buildKeys(20);
for (String key : keys) {
redisUtil.del(key);
}
}
👉 每个 key 单独路由
👉 完全兼容 Cluster
✅ 方案二(长期优化):引入 hash tag
key 设计改为:
{biz:module:monthly:data}:04/2026
这样就可以:
jedisCluster.del(k1, k2, k3); // ✅
✅ 方案三(高级):维护索引集合
SADD biz:monthly:index key1 key2 key3
删除时:
SMEMBERS → DEL
八、为什么 Redis 要这么设计?
为了保证:
- ✔ 路由简单
- ✔ 数据一致性
- ✔ slot 迁移可控
- ✔ 客户端实现简单
九、核心总结
🔥 一句话
Redis Cluster 中,多 key 操作必须在同一个 slot
📌 三条铁律
1️⃣ slot 才是核心,不是节点
2️⃣ 前缀相同 ≠ slot 相同
3️⃣ {} 才能强制同 slot
📌 工程经验
| 场景 | 建议 |
|---|---|
| 模糊删除 | ❌ 不用 scan |
| 批量删除 | ✔ 单个删 |
| 分组操作 | ✔ 使用 {} |
| 高性能批处理 | ✔ 同 slot key |
十、最后的建议
如果你有以下需求:
- 批量删除
- 批量查询
- pipeline
- Lua 脚本
👉 一定要提前设计 key:
{业务维度}:具体数据
否则:
👉 后面一定踩坑(就像这次一样)
结尾
这次问题的本质不是 API 用错,而是:
❗ 对 Redis Cluster 分片机制理解不够
一旦理解了 slot,这类问题就会非常清晰。