redis lua脚本在单节点原子执行,但主从/集群中无法保证一致性,因脚本复制依赖确定性,非确定命令(如time、randomkey)或分支逻辑会导致主从状态分裂,集群还受限于slot约束与evalsha不共享等问题。

Redis Lua 脚本在单节点上是原子执行的,但**不能保证多节点(主从/集群)间的数据一致性**,根本原因不是脚本写得不对,而是复制机制与脚本执行模型存在底层冲突。
为什么主从复制会因 Lua 脚本分裂数据
Redis 默认采用「脚本复制模式(script replication)」:整个 Lua 脚本字符串被原样记录进 AOF 并发送给从节点,从节点再重新执行一遍。这要求脚本必须是「确定性」的——同一脚本、相同输入,在主从上必须产生完全相同的写命令序列。
一旦脚本里出现 RANDOMKEY、TIME、SRANDMEMBER(未排序)、math.random()(未调 math.randomseed)等非确定性命令,主库执行生成 SET a 1,从库重放时却可能生成 SET b 2,结果就是主从 key 集合、值内容、甚至数量都对不上。
常见错误现象:
(error) ERR Error running script (call to f_): @user_script:3: Write command attempted after non deterministic command- 从库日志反复出现
Timeout while executing script,CPU 持续飙高 - 业务侧观察到“同一请求在不同从节点读出不同结果”,且无法复现
replicate_commands() 看似能绕过,实则埋雷
调用 redis.replicate_commands() 可切换为「效果复制模式(effects replication)」:只把脚本内实际执行的 redis.call("SET", ...) 等写命令提取出来,包装成 MULTI/EXEC 发送给从节点和写入 AOF。这确实规避了非确定性命令的问题,但带来新风险:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 必须在第一个写命令前调用,否则已执行的写操作不会被复制,主从直接失联
-
redis.set_repl(...)配置若设为REPL_SLAVE或REPL_NONE,AOF 就不落盘,宕机即丢数据 - 脚本中若有条件分支(如
if redis.call("EXISTS", k) == 1 then ...),不同分支产生的写命令序列长度/顺序不同,从节点仍可能因分支跳转差异导致状态错位
集群模式下,脚本连执行资格都没有
Redis Cluster 要求 Lua 脚本所有操作的 key 必须落在同一 slot,否则直接报错:CROSSSLOT Keys in request don't hash to the same slot。这不是一致性问题,是执行门槛——脚本根本跑不起来。
更隐蔽的问题是:EVALSHA 在集群中不跨节点共享。你在节点 A SCRIPT LOAD 的脚本,节点 B 不知道它的 SHA1,调 EVALSHA 就返回 NOSCRIPT No matching script。强行用 EVAL 发送完整脚本,又触发 too long 错误或超时。
性能影响也很现实:
- 脚本执行期间阻塞整个 Redis 单线程,集群中一个节点卡住,对应 slot 的所有读写全挂
- 从节点重放长脚本时,复制延迟(
slave_repl_offset差值)会飙升,监控告警频繁触发
真正容易被忽略的是:哪怕脚本语法全对、没报错、也成功返回,只要它依赖了任何外部不可控因素(系统时间、客户端传参顺序、key 扫描顺序),就已在一致性边缘行走。生产环境别赌“这次应该没问题”。










