Redis Lua禁用math.random以保证主从一致性,需用redis.call('TIME')或ARGV[1]生成种子,配合LCG算法实现确定性随机;加权抽奖须预计算整数累积权重并二分/顺序查找;中奖判重与写入必须原子执行,避免竞态;权重变更应预构建优化结构,禁用JSON解析。

Redis Lua里不能用math.random,必须自己造随机源
直接调math.random()会报attempt to call a nil value (field 'random')——这不是你写错了,是Redis主动禁用了它,为的是保证主从同步和AOF重放时结果完全一致。所以所有依赖随机数的逻辑,都得绕开这个限制。
可行方案只有两个:用redis.call('TIME')拿当前秒+微秒拼成种子,或用客户端传入的ARGV[1]作为盐值。别碰os.time(),Lua沙箱里不可用;也别用KEYS长度当种子,太容易碰撞,尤其当奖池结构固定时。
-
redis.call('TIME')返回两个整数,比如{1729348234, 123456},可组合为seed = (t1 * 1000000 + t2) % 2147483647 - LCG伪随机函数推荐用
(seed * 1103515245 + 12345) % 2147483647,轻量、确定、周期够长 - 每次抽奖必须用新种子,不能复用上一轮输出值——否则多轮结果会线性相关,权重分布失真
加权随机不能靠展开列表,SRANDMEMBER只适合等概率场景
看到“按权重抽”就本能想把item:5展开成5个元素再SRANDMEMBER?停手。一旦权重总和超过几千,Lua脚本内存立刻爆:command not allowed when used memory > 'maxmemory'。而且Redis不支持在Lua里动态创建临时key,所谓“先塞进__temp再抽”纯属纸上谈兵。
正确做法是预计算累积权重,然后用LCG生成一个[1, total_weight]范围内的整数,再顺序或二分查找它落在哪个区间。顺序查找简单可靠,二分适合权重项特别多(>100)且分布均匀的场景;但注意:如果权重“头重脚轻”(比如前两项占99%),二分反而比顺序慢。
- 累积权重数组必须在Lua里实时构建,不能靠客户端传入——网络传输可能截断大数组
- 浮点权重直接放弃,Redis Lua的double精度会导致边界判断漂移,只接受整数权重
- 总权重不能为0,得在脚本开头加
if total_weight == 0 then return nil end兜底
高并发下SRANDMEMBER不防重复,中奖判重必须进Lua原子块
SRANDMEMBER users 1抽出来一个用户ID,接着去SISMEMBER winners id查是否已中奖?这中间有竞态窗口。两个请求几乎同时抽到同一人,都查到“还没中”,然后都写进中奖名单——重复发奖就发生了。
真正安全的做法,是把“抽→查→写”三步全塞进一个Lua脚本:先SRANDMEMBER拿到候选ID,再SISMEMBER检查,仅当未中奖时才SADD winners id并返回该ID。Redis执行整个脚本是原子的,不会被中断。
- 别省那几行代码,漏掉
SISMEMBER校验等于默认允许重复中奖 - 如果中奖后要从原集合移除(如实体奖品有限),改用
SPOP更干脆,天然无放回 - 用
SCARD winners实时校验中奖总数上限,避免超发——这个判断也得在Lua里做,不能靠业务层
权重变更时不能热更新,必须重建奖池结构
很多人想让运营后台改个权重数值,Lua脚本就自动生效。现实是:Lua脚本读的是Redis里存的原始数据(比如HGETALL prize_config),它每次执行都重新拉取、解析、建累积数组。所以权重变了,只要数据已落库,下次抽奖自然生效——听起来很美,但隐患在“解析”环节。
如果奖池有100个奖品,每次抽奖都HGETALL再循环parse,CPU消耗陡增;更糟的是,若某次配置写错(比如权重字段缺失),脚本直接nil报错,整批请求失败。生产环境必须规避这种脆弱性。
- 上线前用
redis-cli --eval手动跑一遍脚本,验证解析逻辑能否扛住空值/非法格式 - 权重变更应走发布-订阅通知,由后台服务异步重建预计算结构(如
zset存累积权重),Lua脚本只读这个优化后的结构 - 永远不要在Lua里做JSON解析——Redis没内置JSON库,自己写解析器既慢又易出错











