直接用execute()会丢数据,因其不处理网络中断、超时、集群重定向等异常,且未确保key同slot、未包装DefaultRedisScript、未显式指定resultType。

为什么直接用 execute() 会丢数据
Spring 的 RedisTemplate.execute() 默认不保证 Lua 脚本原子性执行——它只是把脚本发给 Redis,但没处理连接中断、超时、集群重定向等异常场景。一旦网络抖动或节点故障,脚本可能只在部分节点执行,或者根本没执行却返回了成功假象。
常见错误现象:JedisConnectionException 后脚本状态不可知;集群模式下报 MOVED 或 ASK 错误却没重试;脚本里用了 redis.call("set", ...) 但 key 不在当前 slot,直接失败。
- 必须用
DefaultRedisScript包装脚本,并显式指定resultType,否则返回值类型错乱(比如期望Long却拿到List) - 脚本里所有 key 必须通过
KEYS[1]等方式传入,不能硬编码;Spring 不会自动做 key hash 拆分,靠你自己确保所有 key 落在同一 slot(单机/哨兵可忽略,集群必须守规矩) - 避免在脚本里调用耗时命令如
KEYS *或大集合遍历,Redis 是单线程,卡住整个实例
怎么让 Lua 脚本在集群和哨兵下都跑稳
Spring Data Redis 对集群模式的 Lua 支持很弱:它默认把脚本发给任意节点,而集群要求所有 key 必须路由到同一分片。直接调 execute() 很大概率触发 ClusterRedirectException。
正确做法是绕过自动路由,手动定位节点:
- 用
RedisClusterConnection的clusterGetNodeForSlot()先算出第一个 key 的 slot,再获取对应节点连接 - 或更简单:改用
RedisOperations.execute(RedisScript, List, Object...)重载方法,传入keys列表(不是Collection),Spring 内部会尝试做 slot 对齐——但仅限所有 key 确实属于同一 slot 时才有效 - 哨兵模式下反而简单,只要配置了
RedisSentinelConfiguration,execute()会自动 failover 到新主节点,无需额外处理
示例:
String script = "return redis.call('incr', KEYS[1])";<br>DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();<br>redisScript.setScriptText(script);<br>redisScript.setResultType(Long.class);<br>Long result = redisTemplate.execute(redisScript, Arrays.asList("counter:123"));
如何捕获 Lua 执行失败的真实原因
Redis 返回的错误在 Spring 里常被吞成泛化的 RuntimeException,比如脚本语法错、redis.call() 抛异常、超时,全变成 org.springframework.dao.DataAccessException,根本看不出是 Lua 问题还是网络问题。
关键点在于:Lua 脚本内部的 error() 会被 Redis 封装为 ERR 响应,Spring 默认不透出原始错误信息。
- 启用
RedisTemplate.setExposeNativeConnection(true),然后用原生连接(如Jedis)调eval()并 catchJedisDataException,里面getMessage()才是真实 Lua 错误 - 脚本里别裸写
error("xxx"),改用return {err="xxx"},让 Java 层能区分业务错误和系统异常 - 加日志埋点:在 execute 前打上脚本 SHA 和 key 列表,出错时能快速比对是否脚本变更或 key 冲突
事务 + Lua 组合时最易踩的坑
别在 Lua 脚本里混用 multi/exec —— Redis 明确禁止:Lua 执行期间不允许开启新事务,否则直接报 ERR Error running script (call to f_...): ERR MULTI calls can not be nested。
如果你需要“先查后改”的原子性,正确姿势是全部逻辑写进 Lua,而不是在 Java 层用 multi() 包一层再 call script。
- 想实现带条件的 setnx + expire?写成一个脚本:
if redis.call("exists", KEYS[1]) == 0 then redis.call("setex", KEYS[1], ARGV[1], ARGV[2]) return 1 else return 0 end - 不要在脚本里调
redis.call("multi"),也没必要——Lua 本身就是原子的 - 注意
evalsha缓存:首次用eval加载脚本后,后续可用 SHA 调用,但若 Redis 重启或 flushdb,SHA 失效,需 fallback 到完整脚本
复杂点在于:脚本逻辑一长,调试成本高,出错时只能靠日志+手动 eval 测试,没有断点。上线前务必用真实数据压测边界 case,比如空 key、超长 value、并发争抢——这些在单元测试里很难覆盖全。

















