Redis使用set设计一个抽奖系统

ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花

一、设计思路

Redis 的 Set(集合)具有元素唯一、支持随机获取的特性,完美契合抽奖系统的核心需求:

  1. 去重参与:Set 自动去重,避免用户重复抽奖;
  2. 随机抽奖:通过 SPOP/SRANDMEMBER 命令随机抽取中奖者;
  3. 高效统计:SCARD 命令快速获取参与人数;
  4. 结果存储:不同奖项的中奖名单可存入不同 Set,便于管理。

核心命令说明:

SADD key uId

添加用户到抽奖集合(重复添加返回 0)

用户参与抽奖

SCARD key

获取集合元素数量

统计参与人数

SPOP key N

随机移除并返回 N 个元素(不可重复中奖)

正式抽奖(中奖后剔除)

SRANDMEMBER key N

随机返回 N 个元素(不移除)

测试/预览中奖名单

SMEMBERS key

获取集合所有元素

查询参与/中奖名单

二、完整代码实现(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
    

四、代码关键解释

  1. 初始化连接:封装 Jedis 连接,支持密码认证和数据库选择,添加异常处理,确保连接有效性,最后需调用 close() 关闭连接;
  2. 用户参与抽奖:调用 sadd() 方法,利用 Redis Set 唯一性,避免用户重复参与;
  3. 核心抽奖逻辑:先校验参与人数,再通过 spop() 方法随机抽取中奖者,同时将中奖者存入对应奖项集合,保证不重复中奖;
  4. 查询功能:getWinners() 调用 smembers() 获取所有中奖者,isUserWon() 调用 sismember() 验证用户中奖状态;
  5. 数据清理: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
    

六、总结

  1. 核心特性:Redis Set 的「唯一性」保证用户仅能参与一次抽奖,「随机获取」特性适配抽奖的随机性需求,且命令执行高效,适合高并发场景;
  2. 实现方式对比:Java 代码适用于工程化、可复用的抽奖场景,支持集成到系统中;纯 Redis 命令适用于简单、临时的抽奖需求,无需开发代码,操作便捷;
  3. 关键命令:正式抽奖用 SPOP(移除元素,避免重复中奖),测试/预览用 SRANDMEMBER(不移除);不同奖项建议用独立 Set 存储,便于查询和管理。

ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花

Redis常用的数据结构 文章被收录于专栏

Redis 作为高性能键值数据库,核心在于丰富的数据结构。本专栏聚焦String、Hash、List、Set、ZSet、Bitmap、HyperLogLog 等常用类型,从底层原理、使用场景到实战示例,清晰讲解每种结构的优缺点与最佳实践。帮你快速掌握如何用对数据结构,提升缓存、限流、排行榜、消息队列等业务场景的开发效率,写出更稳定、高效的 Redis 应用。

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务