基于Redis Set轻松实现简单的抽奖系统

 更新时间:2026年03月17日 11:07:35   作者:程序员牛奶  
Redis Set集合是无序且元素唯一的String类型数据结构,支持高效增删查改操作,包括获取所有值、判断包含关系、计算交并差集等,底层基于Hash表实现O(1)时间复杂度,这篇文章主要介绍了基于Redis Set轻松实现简单的抽奖系统的相关资料,需要的朋友可以参考下

基于 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抽奖系统内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • RedisJSON中JSON.SET的用法小结

    RedisJSON中JSON.SET的用法小结

    本文全面解析 RedisJSON 插件的核心指令JSON.SET,涵盖语法规则、典型用法、易错点和性能优化建议,感兴趣的可以了解一下
    2025-11-11
  • Redis内存碎片处理实例详解

    Redis内存碎片处理实例详解

    内存碎片是redis服务中分配器分配存储对象内存的时产生的,下面这篇文章主要给大家介绍了关于Redis内存碎片处理的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-05-05
  • Redis+threading实现多线程消息队列的使用示例

    Redis+threading实现多线程消息队列的使用示例

    Redis多线程消息队列是一种使用Redis作为存储后端的消息队列实现,它利用Redis的线程并发处理能力来提高消息队列的处理效率,本文主要介绍了Redis+threading实现多线程消息队列的使用示例,感兴趣的可以了解一下
    2023-12-12
  • redis客户端实现高可用读写分离的方式详解

    redis客户端实现高可用读写分离的方式详解

    基于sentienl 获取和动态感知 master、slaves节点信息的变化,我们的读写分离客户端就能具备高可用+动态扩容感知能力了,接下来通过本文给大家分享redis客户端实现高可用读写分离的方式,感兴趣的朋友一起看看吧
    2021-07-07
  • 使用Redis实现记录访问次数的三种方案

    使用Redis实现记录访问次数的三种方案

    这篇文章主要介绍了使用Redis实现记录访问次数的三种方案,文中通过代码示例和图文讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-09-09
  • Redis 数据类型Streams详解

    Redis 数据类型Streams详解

    Redis Streams是Redis 5.0新增的数据类型,提供了一种日志结构化数据存储方式,这种类型适合用于构建消息队列、事件日志和处理时间序列数据的应用,本文介绍Redis 数据类型Streams相关知识,感兴趣的朋友一起看看吧
    2024-10-10
  • springboot项目redis缓存异常实战案例详解(提供解决方案)

    springboot项目redis缓存异常实战案例详解(提供解决方案)

    redis基本上是高并发场景上会用到的一个高性能的key-value数据库,属于nosql类型,一般用作于缓存,一般是结合数据库一块使用的,但是在使用的过程中可能会出现异常的问题,这篇文章主要介绍了springboot项目redis缓存异常实战案例详解(提供解决方案),需要的朋友可以参考下
    2025-05-05
  • 基于Redis实现双加密Token的示例代码

    基于Redis实现双加密Token的示例代码

    在现代分布式系统中,Token管理是身份验证和授权的核心部分,本文将深入分析一个基于Redis的Token管理实现,探讨其设计思路、关键代码逻辑以及实现细节,通过对源码的逐层剖析,帮助读者更好地理解Token管理的实现原理,需要的朋友可以参考下
    2025-01-01
  • Redis设置开机自启动全过程

    Redis设置开机自启动全过程

    这篇文章主要介绍了Redis设置开机自启动全过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-10-10
  • 如何保证Redis与数据库的数据一致性

    如何保证Redis与数据库的数据一致性

    这篇文章主要介绍了如何保证Redis与数据库的数据一致性,文中举了两个场景例子介绍的非常详细,需要的朋友可以参考下
    2023-05-05

最新评论