🚨 一次 Redis Cluster 踩坑实录:SCAN、DEL 全部踩雷

2 阅读3分钟

本文记录一次在生产环境使用 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,这类问题就会非常清晰。