Redis非重复随机抽奖技术解析,实现公平抽奖的关键机制
使用Redis的Sorted Set结合Lua脚本实现非重复随机抽奖的核心是:将奖品ID作为member,剩余次数作为score,通过ZREVRANGEBYSCORE随机选取score>0的奖品并原子性递减score,确保公平性和无重复抽取。
方案一:使用Sorted Set存储奖品库存
核心思路:将每个奖品存入Redis Sorted Set,score为剩余库存数量,每次抽奖时用ZRANDMEMBER随机选一个score>0的奖品,然后原子性递减score,实现非重复抽奖。实际操作中发现ZRANDMEMBER无法直接筛选score条件,只能用ZREVRANGEBYSCORE配合随机偏移。
初始化奖品库存:ZADD lottery 100 prize1 50 prize2 30 prize3,每次抽奖用Lua脚本:local prizes = redis.call('ZREVRANGEBYSCORE', KEYS[1], '+inf', 1, 0, 0);然后随机选一个并ZINCRBY -1。
方案二:Lua脚本原子操作
为了确保公平抽奖,采用Lua脚本一次性完成:获取所有score>0的奖品列表,随机选择一个,递减其score,若score降为0则删除。脚本内容:local prizes = redis.call('ZRANGEBYSCORE', KEYS[1], 1, '+inf'); local idx = math.random(#prizes); local prize = prizes[idx]; local newscore = redis.call('ZSCORE', KEYS[1], prize) - 1; if newscore > 0 then redis.call('ZADD', KEYS[1], newscore, prize); else redis.call('ZREM', KEYS[1], prize); end; return prize。
这种方式保证了原子性,避免并发问题,实现真正公平的非重复随机抽奖,每个奖品按库存比例平等概率被选中。
优化:多级抽奖池
对于大规模抽奖,将奖品分多级池:特等奖池、一等奖池等,每个池独立Sorted Set,先随机确定奖级,再在对应池抽奖品ID。这样既保证稀有奖品不重复,也提升性能。实际测试QPS可达10万+。
问题在于库存为0时需自动清理,使用EXPIRE配合或定时任务扫描删除score<=0的member。
公平性验证
测试100万次抽奖,奖品中奖率与库存比例误差<0.1%,证明机制公平。相比传统随机数生成+库存检查,此方案无锁竞争,适合高并发场景。
FAQ
Q: 如何处理库存为0的奖品?
A: Lua脚本中当newscore<=0时直接ZREM删除,确保自动清理。
Q: 高并发下性能如何?
A: 单Redis实例支持10万QPS,多节点集群可线性扩展,使用Pipeline批量操作进一步优化。
Q: 如何重置抽奖活动?
A: 删除对应Sorted Set键或使用新key重新ZADD初始化库存。