SPOP key count 是高并发下最稳妥的一次性无放回抽取,原子性完成“随机选+删除”,避免重复和竞态;但需 Redis 6.2+ 支持批量,且 7.0 前为伪随机,抽完即删不可回滚。

大量成员下直接用 SMEMBERS + 应用层 shuffle 会爆内存
当 Set 有 10 万+ 成员时,SMEMBERS 会把全部元素一次性拉到客户端,不仅网络带宽吃紧,Java/Python 等语言还可能触发 GC 尖峰甚至 OOM。更危险的是,读完再删(比如用 SREM 逐个删)存在明显竞态窗口:两个请求几乎同时执行 SMEMBERS,拿到同一份名单,各自 shuffle 后抽到相同用户,都以为中奖成功。
SPOP key count 是高并发下最稳妥的一次性无放回抽取
它原子性完成“随机选 + 删除”,天然避免重复和竞态。但要注意几个硬性条件:
-
SPOP的count参数从 Redis 6.2 才正式支持批量;旧版本只能循环调用SPOP key,性能差且非原子 - Redis 7.0 前的
SPOP key N内部不是真随机,而是按哈希桶顺序伪随机——对抽奖公平性要求极高的场景需留意 - 抽完即删,不可回滚。如果业务需要保留原始名单(比如中奖后还要发短信、记审计日志),得提前用
SWAP或SMOVE把数据复制到备份 key
用 Lua 脚本封装 SRANDMEMBER + SREM 适合需保留原集合的场景
当你要从 50 万用户池里抽 100 人,又必须保留原始池子用于后续批次或审计,Lua 是更可控的选择。脚本本质是把“选”和“删”压进一次服务端原子操作:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
eval "local r = redis.call('SRANDMEMBER', KEYS[1], ARGV[1]); redis.call('SREM', KEYS[1], unpack(r)); return r" 1 prize_pool 100
但别忽略这个坑:如果 ARGV[1] 接近集合大小(比如从 1000 人里抽 990 人),SRANDMEMBER 可能反复碰撞、重试,实际耗时反而比 SPOP 高。建议只在 count << SCARD 时用此方案。
别信 SRANDMEMBER key N 能保证不重复
这是最常踩的坑。它的行为完全取决于 N 和集合基数的关系:
-
N为正数且 ≤ 集合大小:返回N个不重复元素,但每次调用都独立采样,多次调用之间毫无互斥 -
N为正数且 > 集合大小:Redis 自动降级为“有放回”,结果必然含重复 -
N为负数(如-10):强制有放回,哪怕集合很大也允许重复——这适合转盘类可复用抽奖,但绝不能用于“一人一中”场景
真正要防重复,核心不是“怎么随机”,而是“怎么确保被抽走的元素立刻不可见”。这点上,SPOP 和 Lua 封装是唯二靠谱路径,其余都是掩耳盗铃。

















