作为分布式缓存领域的扛把子,Redis在5.0版本后已经成为互联网架构的标配组件。而Lua脚本作为Redis的"瑞士军刀",能实现复杂的原子操作,在限流、秒杀、熔断等场景中扮演着关键角色。但当我们把单机环境调试好的Lua脚本迁移到集群环境时,往往会遇到两个经典报错:
CROSSSLOT Keys in request don't hash to the same slot
EvalSha is not supported in cluster environment
第一个报错是Redis集群的规则使然,第二个报错则完全是客户端库的"甩锅"行为。本文将带大家深入这两个问题的技术细节,并给出生产级的解决方案。
假设我们有一个接口熔断场景的Lua脚本,操作两个Key:
lua复制-- KEYS[1] = channel_health:{渠道ID}
-- KEYS[2] = channel_blocked:{渠道ID}
-- ARGV[1]~ARGV[4] = 各种参数
local healthKey = KEYS[1]
local blockKey = KEYS[2]
-- 具体业务逻辑...
在单机Redis上运行良好,迁移到集群环境后立即报错:
code复制CROSSSLOT Keys in request don't hash to the same slot
Redis Cluster采用分片架构,数据分布在16384个slot中。关键机制:
CLUSTER NODES查看)当脚本涉及多个Key时,Redis会检查这些Key是否属于同一个slot。示例:
java复制String healthKey = "channel_health:" + channelId; // slot = CRC16("channel_health:123") % 16384
String blockKey = "channel_blocked:" + channelId; // slot = CRC16("channel_blocked:123") % 16384
由于前缀不同,即使channelId相同,两个Key也会落到不同slot。
Redis提供了{}标记机制:
{}内的内容参与slot计算{}外部分完全忽略改造后的Key设计:
java复制String healthKey = "channel_health:{" + channelId + "}";
String blockKey = "channel_blocked:{" + channelId + "}";
此时slot计算:
code复制slot = CRC16("123") % 16384 // 两个Key计算结果相同
生产环境注意事项:
- Hash Tag不宜滥用,否则会导致数据分布不均
- 建议只对必须保证同slot的Key使用
- 可通过
CLUSTER KEYSLOT命令验证slot计算结果
添加Hash Tag后,继续报错:
code复制org.springframework.dao.InvalidDataAccessApiUsageException:
EvalSha is not supported in cluster environment.
问题出在Spring Data Redis 2.x + Jedis的组合上。关键源码:
java复制// Spring Data Redis 2.x
public class JedisClusterScriptingCommands {
public <T> T evalSha(byte[] scriptSha, ReturnType returnType,
int numKeys, byte[]... keysAndArgs) {
throw new InvalidDataAccessApiUsageException(
"EvalSha is not supported in cluster environment.");
}
}
而DefaultRedisScript默认使用evalSha:
java复制public class DefaultRedisScript<T> implements RedisScript<T> {
private String sha1;
// ...
}
根本矛盾:
java复制RScript script = redissonClient.getScript();
List<Object> result = script.eval(
RScript.Mode.READ_WRITE,
luaScript,
RScript.ReturnType.MULTI,
Arrays.asList(key1, key2),
arg1, arg2);
优势:
在Spring Boot配置:
yaml复制spring:
redis:
client-type: lettuce
Lettuce的集群evalSha实现:
java复制public class LettuceClusterScriptingCommands {
public <T> T evalSha(byte[] scriptSha, ReturnType returnType,
int numKeys, byte[]... keysAndArgs) {
// 实际调用Redis集群的EVALSHA命令
}
}
java复制@Bean
public DefaultRedisScript<Boolean> myScript() {
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(...));
script.setUseEval(true); // 强制使用eval
return script;
}
性能对比:
方案 网络开销 服务端负载 适用场景 eval 每次传输脚本 每次编译 脚本频繁变更 evalSha 只传SHA1 缓存编译结果 脚本稳定
redis.replicate_commands()确保脚本可复制bash复制# 检查Key的slot分布
redis-cli -c -p 7000 CLUSTER KEYSLOT "channel_health:{123}"
# 直接执行测试
redis-cli -c -p 7000 --eval script.lua "channel_health:{123}" "channel_blocked:{123}" , arg1 arg2
当集群发生reshard或节点变更时:
MOVED重定向-ASK和-MOVED响应SCRIPT LOAD预加载脚本SCRIPT EXISTS确保缓存有效关键监控项:
bash复制# 查看脚本执行情况
INFO commandstats | grep eval
# 监控内存使用
INFO memory
# 跟踪慢查询
SLOWLOG GET
| 特性 | Jedis | Lettuce | Redisson |
|---|---|---|---|
| 集群evalSha支持 | ❌ | ✅ | ✅ |
| 连接模式 | 直连 | 异步 | 异步+响应式 |
| 分布式对象 | ❌ | ❌ | ✅ |
| Spring集成 | 完善 | 完善 | 需要额外配置 |
| 生产推荐度 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
在实际项目中,如果重度使用Lua脚本,建议直接采用Redisson。它不仅完美支持集群脚本,还提供了丰富的分布式数据结构和服务。