Redis Lua脚本中使用math.random()会导致主从不一致,因该函数在主从节点上生成不同随机数序列;应禁用math.random()、os.time()等非确定性函数,改用KEYS/ARGV等确定性输入实现哈希或打散逻辑。

Redis Lua 脚本里用了 math.random() 就会主从不一致
因为 math.random() 在主节点和从节点上生成的随机数序列不同,导致脚本执行结果不一致——这是 Redis 主从复制中极隐蔽但真实存在的数据不一致根源。
Redis 复制的是「命令流」,不是最终状态。当脚本含非确定性操作(如随机数、时间戳、redis.call("TIME")),主节点执行后记录的是脚本本身(EVAL ...),而从节点重新执行该脚本时,math.random() 会基于自身 Lua 状态重新采样,结果与主节点不一致,复制链就断了。
- 主节点执行
math.random(1,100)得到 42 → 写入 key A = "42" - 从节点重放同一脚本 →
math.random(1,100)可能返回 87 → key A = "87" - 后续所有依赖该值的逻辑(比如条件分支、计数器更新)全部错位
怎么检查脚本是否用了不可复现的函数
别靠肉眼扫,用 redis-cli --eval 加 --debug 模式或静态扫描更可靠。重点盯三类函数:
-
math.random()、math.randomseed():直接禁用,无例外 -
os.time()、redis.call("TIME"):返回秒级/微秒级时间,主从毫秒级偏差即导致分支不同 -
redis.call("CLIENT", "ID")、redis.call("CLIENT", "GETNAME"):客户端标识在主从间不一致,不能用于逻辑判断
示例错误写法:
local ts = os.time()if ts % 2 == 0 then redis.call("SET", KEYS[1], "even") else redis.call("SET", KEYS[1], "odd") end——主从几乎必然写入不同值。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
替代方案:用确定性输入模拟“随机”行为
如果业务真需要“打散”或“哈希路由”,必须用确定性算法,输入源只能是:KEYS、ARGV、已知常量、或 Redis 已存的确定性数据(如 redis.call("GET", "config:seed"))。
- 用
crc32(KEYS[1] .. ARGV[1]) % 100替代math.random(1,100) - 用
redis.call("INCRBY", "global:seq", 1)做单调递增 ID,而非靠时间戳拼接 - 若需“随机选一个 key”,先用
redis.call("KEYS", "prefix:*")拿全集,再用table.sort+ 固定 seed(如KEYS[1]的 hash)做稳定 shuffle
注意:KEYS 和 ARGV 是脚本执行时传入的,主从完全一致;而任何依赖运行时环境或系统状态的调用,都不可信。
上线前必须做的校验动作
哪怕脚本逻辑看起来“很安全”,只要没显式排除非确定性调用,就不能进生产。
- CI 阶段加正则扫描:
grep -E "(math\.random|os\.time|TIME|CLIENT)" script.lua - 本地启主从双节点,用
redis-benchmark -r 1000 -n 1000 -e "script load $(cat script.lua)"批量加载,再比对主从 key 值 diff - 监控项补充:
INFO replication中的master_repl_offset与从节点slave_repl_offset持续追平 ≠ 数据一致;要额外抽样验证关键 key 的值是否相同
最危险的不是报错,而是静默错——脚本成功返回,主从 key 都存在,但值不同,且这种差异可能数小时后才在业务侧暴露。

















