基于 redis set 轻松搞定高并发抽奖系统
想要从零手搓一个高性能的抽奖系统?redis 的 set (集合)数据结构绝对是你的不二之选。
它的特性和 java 中的 hashset 极其相似,天生自带去重光环。这就意味着,无论一个用户手速多快、疯狂点击了多少次参与,抽奖池里也永远只有他的一个名字,完美避免了重复报名的问题。更棒的是,它底层随机弹出元素的时间复杂度仅为 o(1)o(1)o(1),即使面对海量用户的并发抽奖,也能轻松扛住压力。
利用 set 实现抽奖系统的核心逻辑非常轻量,熟练掌握以下三个命令即可:
sadd key member1 member2 ...:向奖池中添加一个或多个参与者。spop key count:随机从奖池中抽出并移除指定数量的元素。非常适合“一等奖”、“二等奖”这种不允许重复中奖的核心业务场景。srandmember key count:随机从奖池中获取指定数量的元素,但不移除它们。适合“阳光普照奖”、“参与奖”这种允许重复中奖的场景。
核心代码实现
下面我们结合 java (spring boot) 与 redis,来落地这个抽奖系统。
1. controller 层:定义抽奖接口
在这里我们定义了加入奖池、抽取大奖(不放回)以及抽取阳光奖(可放回)的 api。
package com.example.redissetrandomget.lottery;
import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.requestmapping;
import org.springframework.web.bind.annotation.requestmethod;
import org.springframework.web.bind.annotation.requestparam;
import org.springframework.web.bind.annotation.restcontroller;
import java.util.list;
@restcontroller
@requestmapping("/api/lottery")
public class lotterycontroller {
private final lotteryservice lotteryservice;
public lotterycontroller(lotteryservice lotteryservice) {
this.lotteryservice = lotteryservice;
}
// 加入抽奖者(支持批量)
@requestmapping(path = "/add", method = {requestmethod.get, requestmethod.post})
public string add(@requestparam string activityid, @requestparam string[] userids) {
lotteryservice.addparticipants(activityid, userids);
long remaincount = lotteryservice.getremaincount(activityid);
return "成功加入奖池!当前奖池总人数:" + remaincount;
}
// 抽核心大奖(抽完即踢出奖池,绝对不重复中奖)
@getmapping("/drawgrand")
public list<string> drawgrand(@requestparam string activityid, @requestparam long count) {
return lotteryservice.drawgrandprize(activityid, count);
}
// 抽幸运参与奖(抽完保留在奖池,下次还有机会)
@getmapping("/drawsunshine")
public list<string> drawsunshine(@requestparam string activityid, @requestparam long count) {
return lotteryservice.drawsunshineprize(activityid, count);
}
// 查询奖池剩余人数
@getmapping("/remain")
public long remain(@requestparam string activityid) {
return lotteryservice.getremaincount(activityid);
}
}
2. service 层:封装 redis 操作
service 层主要负责与 redis 进行交互,并做了一些基础的参数校验和清理工作,保证数据的健壮性。
package com.example.redissetrandomget.lottery;
import org.springframework.data.redis.core.stringredistemplate;
import org.springframework.stereotype.service;
import org.springframework.util.assert;
import org.springframework.util.stringutils;
import java.util.arrays;
import java.util.list;
@service
public class lotteryservice {
private static final string lottery_key_prefix = "lottery:activity:";
private final stringredistemplate redistemplate;
public lotteryservice(stringredistemplate redistemplate) {
this.redistemplate = redistemplate;
}
public void addparticipants(string activityid, string... userids) {
redistemplate.opsforset().add(buildkey(activityid), normalizeuserids(userids));
}
// 使用 pop:随机抽取并移除(适用于大奖)
public list<string> drawgrandprize(string activityid, long count) {
validatecount(count);
list<string> winners = redistemplate.opsforset().pop(buildkey(activityid), count);
return winners != null ? winners : list.of();
}
// 使用 randommembers:随机抽取但不移除(适用于阳光普照奖)
public list<string> drawsunshineprize(string activityid, long count) {
validatecount(count);
list<string> winners = redistemplate.opsforset().randommembers(buildkey(activityid), count);
return winners != null ? winners : list.of();
}
public long getremaincount(string activityid) {
long size = redistemplate.opsforset().size(buildkey(activityid));
return size != null ? size : 0l;
}
public void joinlottery(string activityid, string... userids) {
addparticipants(activityid, userids);
}
public list<string> drawwithoutrepeat(string activityid, long count) {
return drawgrandprize(activityid, count);
}
public list<string> drawwithrepeat(string activityid, long count) {
return drawsunshineprize(activityid, count);
}
public long participantcount(string activityid) {
return getremaincount(activityid);
}
// --- 私有辅助方法 ---
private void validatecount(long count) {
assert.istrue(count > 0, "抽奖人数必须大于 0");
}
private string buildkey(string activityid) {
assert.hastext(activityid, "活动 id 不能为空");
return lottery_key_prefix + activityid.trim();
}
private string[] normalizeuserids(string[] userids) {
assert.notempty(userids, "用户列表不能为空");
string[] normalizeduserids = arrays.stream(userids)
.filter(stringutils::hastext)
.map(string::trim)
.distinct()
.toarray(string[]::new);
assert.notempty(normalizeduserids, "过滤后没有合法的用户 id");
return normalizeduserids;
}
}
接口测试与验证
代码准备就绪,我们来模拟一次真实的抽奖流程。
首先,我们通过接口向活动 2026 的奖池中加入 5 名测试用户。你可以在 redis 客户端中使用 scard lottery:activity:2026 命令来验证奖池内的人数,确认 5 人已成功入场:
测试一:抽取大奖(不放回)
我们先来测试一下抽取 2 名一等奖用户。调用 drawgrand 接口:
http
get http://localhost:8080/api/lottery/drawgrand?activityid=2026&count=2
接口成功返回了 3 号和 5 号用户。由于使用的是 spop 命令,这两个幸运儿已经被移出奖池,后续的抽奖中绝不会再出现他们的身影。
http/1.1 200 content-type: application/json date: fri, 13 mar 2026 08:54:20 gmt [ "3", "5" ]
测试二:抽取幸运参与奖(可放回)
接下来,我们测试抽取 2 名阳光普照奖。调用 drawsunshine 接口:
http
get http://localhost:8080/api/lottery/drawsunshine?activityid=2026&count=2
查看返回结果,我们发现 2 号用户被抽中了两次!这正是 srandmember 的特性:随机抽取元素但保留在原集合中,因此同一个用户在同一轮或不同轮次中都有可能重复中奖。
json
http/1.1 200 content-type: application/json date: fri, 13 mar 2026 08:56:28 gmt [ "2", "2" ]
总结
到此这篇关于基于redis set轻松实现简单的抽奖系统的文章就介绍到这了,更多相关redis set抽奖系统内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论