直接用incr/decr无法满足抽奖概率扣减,因其仅支持固定整数增减,无法实现“查库存→判阈值→按动态权重扣减→返回结果”的原子流程;需用lua脚本统一放大浮点权重为整数、显式类型转换、结合setnx防重及预加载evalsha保障性能与一致性。

为什么直接用 INCR 或 DECR 无法满足抽奖概率扣减?
因为抽奖通常不是简单“扣1”,而是按权重随机选中后,需要原子性地:① 查当前剩余库存/权重值;② 判断是否足够;③ 若足够则扣减对应数值(可能是 1,也可能是 0.5、2、甚至动态计算的浮点权重);④ 同时返回中奖结果。多个客户端并发时,GET+SET 非原子,会超发;INCRBY 又无法做条件判断和分支返回。
EVAL 脚本里如何安全判断并扣减浮点型概率权重?
Lua 在 Redis 中不支持原生浮点比较(比如 if remaining >= weight then 可能因精度丢失误判),且 redis.call("GET", key) 返回的是字符串,需显式转数字。关键做法是:统一用整数放大(如 ×1000)存权重和库存,全部用 tonumber() 转换,用 math.floor 或 math.ceil 控制舍入方向,避免隐式转换误差。
示例逻辑:
local key = KEYS[1]
local weight = tonumber(ARGV[1]) * 1000 -- 放大1000倍
local current = tonumber(redis.call("GET", key) or "0")
if current >= weight then
redis.call("DECRBY", key, weight)
return 1 -- 中奖
else
return 0 -- 未中奖
end
并发抽奖时怎么防止同一份库存被多次扣减?
靠 Lua 脚本的原子执行还不够——如果不同奖品共用一个库存 key(比如总抽奖次数限制),而脚本没校验「本次抽奖是否已参与」,就会重复扣减。必须把用户标识(如 user_id)作为 key 的一部分,或额外用 SETNX 记录单次参与状态:
- 方案一(推荐):库存 key 设计为
"lottery:pool:<code>prize_id:stock",每次扣减只影响该奖品,互不干扰 - 方案二:在脚本内加一层防重,用
redis.call("SETNX", "lottery:used:"..KEYS[2], "1")(KEYS[2]是user_id),并设 TTL,失败则直接返回未中奖 - 注意:
SETNX+EXPIRE非原子,必须用SET的EX和NX参数合并:redis.call("SET", "lottery:used:"..KEYS[2], "1", "EX", 3600, "NX")
用 EVALSHA 代替 EVAL 有什么实际好处?
脚本内容不变时,EVALSHA 只传 SHA1 值(32 字符),比传完整 Lua 字符串(常 >200 字符)节省带宽;更重要的是,Redis 会缓存已加载脚本的 SHA,后续调用更快。但要注意两点:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
必须先用 SCRIPT LOAD 预热脚本(否则 EVALSHA 报错 NOSCRIPT);客户端要处理 NOSCRIPT 异常并 fallback 到 EVAL。
典型调用链:
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# → 返回 "4e81a7a6f3a9c7b2d..."
EVALSHA 4e81a7a6f3a9c7b2d... 1 lottery:stock 1
生产环境建议所有 Lua 脚本启动时预加载,避免运行时首次触发 EVAL 的延迟抖动。
真正难的不是写对脚本,而是库存 key 的粒度设计和用户参与状态的 TTL 管理——这两个点一旦疏忽,压测时才会暴露超发或漏奖。










