单机 Redis 上跑得好好的 Lua 脚本,部署到集群后连续报了两个错——第一个是 Redis 的规矩没守好,第二个报错直接说"集群不支持",差点信了。冷静下来翻了源码才发现,这锅不该 Redis 背。


一、背景

最近做了一个基于 Redis Lua 的三方接口健康检查功能(详见:三方接口挂了还在傻傻调?教你用 Redis Lua 实现自动熔断),核心逻辑是:每次调用三方接口后,用 Lua 脚本原子地记录结果、统计成功率、判定是否屏蔽

Lua 脚本操作了两个 Key:

Key 类型 用途
channel_health:{渠道} List 滑动窗口,存储最近 N 次调用结果
channel_blocked:{渠道} String 屏蔽标记,存在即被屏蔽,TTL 到期自动恢复

脚本在测试环境(单机 Redis)上跑得好好的。然后部署到线上 Redis Cluster——好戏开始了。


二、第一个坑:CROSSSLOT

2.1 报错信息

CROSSSLOT Keys in request don't hash to the same slot

2.2 原因

Redis Cluster 将数据分布在 16384 个 slot 上。Lua 脚本操作多个 Key 时,Redis 要求所有 Key 必须在同一个 slot,否则拒绝执行。

当时的 Key 设计:

String healthKey = "channel_health:" + channelId;   // 整个 Key 参与 hash → slot A
String blockKey  = "channel_blocked:" + channelId;   // 整个 Key 参与 hash → slot B

两个 Key 前缀不同,CRC16 hash 出来的 slot 自然不同,直接 GG。

2.3 解决:Hash Tag

Redis 提供了 Hash Tag 机制:Key 中 {...} 内的部分作为 hash 计算依据。

// 改造后:只有 {channelId} 参与 hash,slot 相同
String healthKey = "channel_health:{" + channelId + "}";
String blockKey  = "channel_blocked:{" + channelId + "}";

原理:channel_health:{5}channel_blocked:{5} 都只对 5 做 CRC16 hash,结果一致,必然落在同一个 slot。

Hash Tag 不只是 Lua 脚本需要,MGETPipeline事务 等涉及多 Key 的操作在集群模式下都需要用它来保证 Key 同 slot。

加上 Hash Tag,满怀信心地重新部署——


三、第二个坑:EvalSha is not supported

3.1 报错信息

Hash Tag 加好了,再跑,换了个姿势报错:

org.springframework.dao.InvalidDataAccessApiUsageException:
    EvalSha is not supported in cluster environment.
  at o.s.d.r.connection.jedis.JedisClusterScriptingCommands.evalSha(...)
  at o.s.d.r.connection.DefaultedRedisConnection.evalSha(...)

等等,Redis Cluster 不支持 Lua?我刚才白折腾了?

3.2 原因

冷静下来看了一下源码,真相是:

这不是 Redis 的问题,是 Spring Data Redis 的问题。 Redis Cluster 完全支持 Lua 脚本(前提是所有 Key 在同一个 slot)。

问题出在 Spring Data Redis(2.x)+ Jedis 这个组合上。JedisClusterScriptingCommands.evalSha() 直接写死了抛异常:

// Spring Data Redis 源码
public <T> T evalSha(byte[] scriptSha, ReturnType returnType, int numKeys, byte[]... keysAndArgs) {
    throw new InvalidDataAccessApiUsageException(
        "EvalSha is not supported in cluster environment.");
}

DefaultRedisScript 默认走 evalSha(先缓存脚本 SHA1,用 SHA1 执行),在集群模式下直接被拦截。

你费劲考了驾照,结果发现车上了锁,钥匙在另一个人手里。

3.3 解决:换用 Redisson

既然 Spring Data Redis + Jedis 走不通,换一个支持集群 Lua 的客户端——Redisson

String healthKey = "channel_health:{" + channelId + "}";
String blockKey  = "channel_blocked:{" + channelId + "}";

List<Object> result = redissonClient.getScript(new StringCodec()).eval(
        RScript.Mode.READ_WRITE,
        LUA_SCRIPT,
        RScript.ReturnType.MULTI,
        Arrays.asList(healthKey, blockKey),
        success ? "1" : "0",
        String.valueOf(windowSize),
        String.valueOf(successRate),
        String.valueOf(blockMinutes * 60),
        String.valueOf((blockMinutes + 30) * 60));

部署,执行,终于成功了

3.4 其他可选方案

方案 说明 推荐度
Redisson 原生支持集群 Lua,API 简洁 推荐
Lettuce Spring Data Redis 换 Lettuce 驱动,支持集群 evalsha 推荐
升级 Spring Boot Spring Data Redis 3.x 已修复此问题 视项目情况
手动调用 eval 绕过 evalSha,直接用 eval 发送完整脚本 能用但不优雅

四、总结

4.1 踩坑清单

关卡 报错 根因 解决方案
第一坑 CROSSSLOT Keys in request don't hash to the same slot 多 Key 不在同一个 slot 使用 Hash Tag {...}
第二坑 EvalSha is not supported in cluster environment Spring Data Redis 2.x + Jedis 写死了 throw 换用 RedissonLettuce

4.2 经验

  1. Redis Cluster 支持 Lua 脚本,别被报错信息误导——那是客户端框架的锅
  2. Hash Tag 是集群多 Key 操作的基础,不只是 Lua,MGETPipeline事务 都需要
  3. Spring Data Redis + Jedis 在集群模式下有功能缺失,大量使用 Lua 建议直接上 Redisson 或切 Lettuce
  4. 单机 → 集群不是简单加节点,代码层面也要适配,上线前务必在集群环境跑一遍

如果你也在 Redis Cluster 上跑 Lua 脚本,收藏这篇,能帮你省掉不少排坑时间 🍻

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐