Redis使用set设计一个抽奖系统
ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花
一、设计思路
Redis 的 Set(集合)具有元素唯一、支持随机获取的特性,完美契合抽奖系统的核心需求:
- 去重参与:Set 自动去重,避免用户重复抽奖;
- 随机抽奖:通过 SPOP/SRANDMEMBER 命令随机抽取中奖者;
- 高效统计:SCARD 命令快速获取参与人数;
- 结果存储:不同奖项的中奖名单可存入不同 Set,便于管理。
核心命令说明:
| 添加用户到抽奖集合(重复添加返回 0) | 用户参与抽奖 |
| 获取集合元素数量 | 统计参与人数 |
| 随机移除并返回 N 个元素(不可重复中奖) | 正式抽奖(中奖后剔除) |
| 随机返回 N 个元素(不移除) | 测试/预览中奖名单 |
| 获取集合所有元素 | 查询参与/中奖名单 |
二、完整代码实现(Java)
以下是基于 Java + Redis 的抽奖系统,采用 Jedis 客户端,包含「用户参与、抽奖、查询、清空」核心功能,代码可直接运行。
1. 前置条件
- 添加 Jedis 依赖(Maven):
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.4.6</version> <!-- 稳定版本,可根据需求调整 -->
</dependency>
- 本地已启动 Redis 服务(默认端口 6379),若有密码需在代码中修改配置。
2. 代码实现
import redis.clients.jedis.Jedis;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class RedisLotterySystem {
private final Jedis jedis;
/**
* 初始化 Redis 连接
* @param host Redis 主机地址
* @param port Redis 端口
* @param db 数据库索引
* @param password Redis 密码(无密码传 null)
*/
public RedisLotterySystem(String host, int port, int db, String password) {
try {
// 初始化 Jedis 连接
this.jedis = new Jedis(host, port);
// 若有密码,执行认证
if (password != null && !password.isEmpty()) {
jedis.auth(password);
}
// 选择数据库
jedis.select(db);
// 测试连接
jedis.ping();
System.out.println("Redis 连接成功!");
} catch (Exception e) {
throw new RuntimeException("Redis 连接失败:" + e.getMessage());
}
}
/**
* 用户参与抽奖
* @param activityKey 抽奖活动唯一标识(如 "lottery_spring_2026")
* @param userId 用户ID(如 "user_1001")
* @return true-参与成功,false-已参与过
*/
public boolean userJoinLottery(String activityKey, String userId) {
// SADD 返回 1 表示添加成功,0 表示已存在(去重)
Long result = jedis.sadd(activityKey, userId);
return result == 1;
}
/**
* 获取参与抽奖的总人数
* @param activityKey 抽奖活动唯一标识
* @return 参与人数
*/
public long getParticipantCount(String activityKey) {
// SCARD 命令获取集合元素数量
return jedis.scard(activityKey);
}
/**
* 抽取中奖者(正式抽奖,中奖后从参与集合移除,避免重复中奖)
* @param activityKey 参与集合的 key
* @param prizeKey 中奖名单存储的 key(如 "lottery_spring_2026_first")
* @param prizeCount 中奖人数
* @return 中奖用户ID列表
*/
public List<String> drawLottery(String activityKey, String prizeKey, int prizeCount) {
long participantCount = getParticipantCount(activityKey);
// 校验参与人数
if (participantCount == 0) {
throw new IllegalArgumentException("暂无用户参与抽奖!");
}
if (prizeCount > participantCount) {
throw new IllegalArgumentException(String.format("参与人数(%d)少于中奖人数(%d)!", participantCount, prizeCount));
}
// SPOP 随机移除并返回 N 个元素,保证不重复中奖
Set<String> winners = jedis.spop(activityKey, prizeCount);
// 将中奖者存入对应奖项的 Set 集合
if (winners != null && !winners.isEmpty()) {
jedis.sadd(prizeKey, winners.toArray(new String[0]));
}
// 转换为 List 返回,统一返回格式
return new ArrayList<>(winners != null ? winners : Set.of());
}
/**
* 查询某奖项的中奖名单
* @param prizeKey 奖项对应的 key
* @return 中奖用户ID列表
*/
public List<String> getWinners(String prizeKey) {
// SMEMBERS 获取集合所有元素,转换为 List
Set<String> winners = jedis.smembers(prizeKey);
return new ArrayList<>(winners);
}
/**
* 判断用户是否中某奖项
* @param prizeKey 奖项对应的 key
* @param userId 用户ID
* @return true-中奖,false-未中奖
*/
public boolean isUserWon(String prizeKey, String userId) {
// SISMEMBER 判断元素是否在集合中
return jedis.sismember(prizeKey, userId);
}
/**
* 清空抽奖数据(活动结束后调用)
* @param activityKey 参与集合的 key
* @param prizeKeys 所有奖项的 key(可变参数)
*/
public void clearLotteryData(String activityKey, String... prizeKeys) {
// 删除参与集合
jedis.del(activityKey);
// 删除所有奖项的中奖集合
if (prizeKeys != null && prizeKeys.length > 0) {
jedis.del(prizeKeys);
}
System.out.println("抽奖数据已清空!");
}
// 关闭 Redis 连接
public void close() {
if (jedis != null) {
jedis.close();
}
}
// ------------------- 测试示例 -------------------
public static void main(String[] args) {
// 初始化抽奖系统(无密码传 null,根据实际 Redis 配置调整)
RedisLotterySystem lottery = new RedisLotterySystem("localhost", 6379, 0, null);
// 定义活动和奖项的 key
String ACTIVITY_KEY = "lottery_spring_2026"; // 参与集合 key
String FIRST_PRIZE_KEY = "lottery_spring_2026_first"; // 一等奖中奖名单
String SECOND_PRIZE_KEY = "lottery_spring_2026_second";// 二等奖中奖名单
// 1. 模拟用户参与抽奖
String[] users = {"user_1001", "user_1002", "user_1003", "user_1004", "user_1005",
"user_1006", "user_1007", "user_1008", "user_1009", "user_1010"};
for (String user : users) {
if (lottery.userJoinLottery(ACTIVITY_KEY, user)) {
System.out.printf("用户 %s 参与抽奖成功%n", user);
} else {
System.out.printf("用户 %s 已参与过抽奖%n", user);
}
}
// 2. 统计参与人数
long total = lottery.getParticipantCount(ACTIVITY_KEY);
System.out.printf("%n当前参与抽奖总人数:%d%n", total);
// 3. 抽取中奖者(一等奖1名,二等奖3名)
List<String> firstWinners = lottery.drawLottery(ACTIVITY_KEY, FIRST_PRIZE_KEY, 1);
List<String> secondWinners = lottery.drawLottery(ACTIVITY_KEY, SECOND_PRIZE_KEY, 3);
System.out.printf("%n一等奖中奖名单:%s%n", firstWinners);
System.out.printf("二等奖中奖名单:%s%n", secondWinners);
// 4. 查询中奖名单
System.out.printf("%n一等奖完整中奖名单:%s%n", lottery.getWinners(FIRST_PRIZE_KEY));
System.out.printf("二等奖完整中奖名单:%s%n", lottery.getWinners(SECOND_PRIZE_KEY));
// 5. 验证用户是否中奖
boolean isWon = lottery.isUserWon(FIRST_PRIZE_KEY, "user_1001");
System.out.printf("%n用户 user_1001 是否中一等奖:%b%n", isWon);
// 6. 活动结束后清空数据(可选,注释后可保留数据)
// lottery.clearLotteryData(ACTIVITY_KEY, FIRST_PRIZE_KEY, SECOND_PRIZE_KEY);
// 关闭连接
lottery.close();
}
}
三、纯Redis命令实现(无编程语言)
无需借助任何编程语言,直接通过 Redis 命令行(或 Redis 客户端)即可完成抽奖全流程,适用于简单场景、临时抽奖,步骤如下(对应上述代码功能):
1. 初始化抽奖活动(定义集合key)
无需额外初始化,直接通过 SADD 命令创建参与集合(Redis 集合不存在时会自动创建),建议统一命名规范,例如:
- 参与集合 key:lottery_spring_2026(活动唯一标识)
- 一等奖中奖集合 key:lottery_spring_2026_first
- 二等奖中奖集合 key:lottery_spring_2026_second
2. 用户参与抽奖(去重)
执行 SADD 命令,重复添加同一用户会返回 0,自动去重:
# 单个用户参与:SADD 参与集合key 用户ID
SADD lottery_spring_2026 user_1001
# 多个用户批量参与(空格分隔)
SADD lottery_spring_2026 user_1002 user_1003 user_1004
3. 统计参与人数
执行 SCARD 命令,快速获取参与总人数:
SCARD lottery_spring_2026 # 返回结果为参与人数(如 10)
4. 正式抽奖(避免重复中奖)
执行 SPOP 命令,随机抽取指定数量的中奖者,同时将其从参与集合中移除(避免重复中奖),并将中奖者存入对应奖项集合:
# 1. 抽取一等奖1名,返回中奖用户ID
SPOP lottery_spring_2026 1
# 2. 将中奖者存入一等奖集合(替换为上一步返回的用户ID)
SADD lottery_spring_2026_first user_1005
# 3. 抽取二等奖3名,返回中奖用户ID列表
SPOP lottery_spring_2026 3
# 4. 将中奖者批量存入二等奖集合(替换为上一步返回的用户ID)
SADD lottery_spring_2026_second user_1002 user_1008 user_1001
5. 测试/预览中奖名单(不剔除用户)
若仅需预览中奖名单,不剔除参与用户,使用 SRANDMEMBER 命令(不移除元素):
# 预览一等奖1名、二等奖3名(仅预览,不影响参与集合) SRANDMEMBER lottery_spring_2026 1 SRANDMEMBER lottery_spring_2026 3
6. 查询中奖名单/验证用户是否中奖
# 1. 查询某奖项所有中奖者
SMEMBERS lottery_spring_2026_first # 查询一等奖名单
SMEMBERS lottery_spring_2026_second # 查询二等奖名单
# 2. 验证单个用户是否中某奖项(返回 1 表示中奖,0 表示未中奖)
SISMEMBER lottery_spring_2026_first user_1001
7. 活动结束,清空数据
执行 DEL 命令,删除参与集合和所有奖项集合,清理数据:
# 批量删除参与集合和所有奖项集合(空格分隔)
DEL lottery_spring_2026 lottery_spring_2026_first lottery_spring_2026_second
四、代码关键解释
- 初始化连接:封装 Jedis 连接,支持密码认证和数据库选择,添加异常处理,确保连接有效性,最后需调用 close() 关闭连接;
- 用户参与抽奖:调用 sadd() 方法,利用 Redis Set 唯一性,避免用户重复参与;
- 核心抽奖逻辑:先校验参与人数,再通过 spop() 方法随机抽取中奖者,同时将中奖者存入对应奖项集合,保证不重复中奖;
- 查询功能:getWinners() 调用 smembers() 获取所有中奖者,isUserWon() 调用 sismember() 验证用户中奖状态;
- 数据清理:clearLotteryData() 调用 del() 方法,批量删除参与集合和奖项集合,适用于活动结束后清理。
五、测试输出示例
Redis 连接成功!
用户 user_1001 参与抽奖成功
用户 user_1002 参与抽奖成功
用户 user_1003 参与抽奖成功
用户 user_1004 参与抽奖成功
用户 user_1005 参与抽奖成功
用户 user_1006 参与抽奖成功
用户 user_1007 参与抽奖成功
用户 user_1008 参与抽奖成功
用户 user_1009 参与抽奖成功
用户 user_1010 参与抽奖成功
当前参与抽奖总人数:10
一等奖中奖名单:[user_1005]
二等奖中奖名单:[user_1002, user_1008, user_1001]
一等奖完整中奖名单:[user_1005]
二等奖完整中奖名单:[user_1001, user_1002, user_1008]
用户 user_1001 是否中一等奖:false
六、总结
- 核心特性:Redis Set 的「唯一性」保证用户仅能参与一次抽奖,「随机获取」特性适配抽奖的随机性需求,且命令执行高效,适合高并发场景;
- 实现方式对比:Java 代码适用于工程化、可复用的抽奖场景,支持集成到系统中;纯 Redis 命令适用于简单、临时的抽奖需求,无需开发代码,操作便捷;
- 关键命令:正式抽奖用 SPOP(移除元素,避免重复中奖),测试/预览用 SRANDMEMBER(不移除);不同奖项建议用独立 Set 存储,便于查询和管理。
ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花
Redis 作为高性能键值数据库,核心在于丰富的数据结构。本专栏聚焦String、Hash、List、Set、ZSet、Bitmap、HyperLogLog 等常用类型,从底层原理、使用场景到实战示例,清晰讲解每种结构的优缺点与最佳实践。帮你快速掌握如何用对数据结构,提升缓存、限流、排行榜、消息队列等业务场景的开发效率,写出更稳定、高效的 Redis 应用。
查看8道真题和解析