游乐游手机版
首页/数据库/文章详情

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

时间:2026-07-28 22:30
基于Redis集合的高性能抽奖系统,利用SADD、SPOP、SRANDMEMBER命令分别实现奖池加入、大奖不放回随机抽取和阳光普照奖可重复抽取。Set天然去重防止重复报名,O(1)复杂度支撑高并发,具有高效稳定的特点,可应对大规模用户的抽奖需求。

基于Redis Set轻松实现高并发抽奖系统(Spring Boot实战详解)

想从零手搓一个能扛住高并发的抽奖系统?Redis 的 Set(集合)数据结构,可以说是最趁手的工具之一。它的去重特性跟 Java 里的 HashSet 如出一辙——无论用户手速多快、疯狂点击多少次参与,奖池里永远只有他一个名字,重复报名的问题天然被化解。更关键的是,随机弹出元素的时间复杂度仅为 O(1),哪怕海量用户同时开抽,系统也能稳稳接住。

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

用 Set 实现抽奖的核心逻辑非常轻量,熟练拿捏下面三个命令就够了:

  • SADD key member1 member2 ... —— 把参与者扔进奖池,自动去重。
  • SPOP key count —— 从奖池里随机抽人,并且直接移除。这非常适合“一等奖”“二等奖”这类不允许重复中奖的高并发抽奖场景。
  • SRANDMEMBER key count —— 随机抽人,但不挪走元素。适合“阳光普照奖”“参与奖”这种允许重复中奖的玩法。

核心代码实现与接口设计

下面用 Java (Spring Boot) + Redis 把抽奖系统落地。先看 Controller 层,定义了加入奖池、抽大奖(不放回)、抽阳光奖(可放回)三个接口。

1. Controller 层:定义抽奖接口

package com.example.redissetrandomget.lottery;

import org.springframework.web.bind.annotation.*;
import ja va.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 drawGrand(@RequestParam String activityId, @RequestParam long count) {
        return lotteryService.drawGrandPrize(activityId, count);
    }

    // 抽幸运参与奖(抽完保留在奖池,下次还有机会)
    @GetMapping("/drawSunshine")
    public List 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 交互,顺带做了参数校验和用户 ID 的清洗,保证数据干净可靠。

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 ja va.util.Arrays;
import ja va.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 drawGrandPrize(String activityId, long count) {
        validateCount(count);
        List winners = redisTemplate.opsForSet().pop(buildKey(activityId), count);
        return winners != null ? winners : List.of();
    }

    // 使用 randomMembers:随机抽取但不移除(适用于阳光普照奖)
    public List drawSunshinePrize(String activityId, long count) {
        validateCount(count);
        List 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 drawWithoutRepeat(String activityId, long count) {
        return drawGrandPrize(activityId, count);
    }

    public List 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 名测试用户。用 SCARD lottery:activity:2026 命令验证一下,确认 5 人已经成功入场。

测试一:抽取大奖(不放回)

先试试抽取 2 名一等奖。调用 drawGrand 接口:

GET https://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 接口:

GET https://localhost:8080/api/lottery/drawSunshine?activityId=2026&count=2

从返回结果可以看到,2 号用户被抽中了两次!这正是 SRANDMEMBER 的魔力——只随机抽取,不删除元素,所以同一个用户在同一轮或不同轮次中都有可能重复中奖。

HTTP/1.1 200
Content-Type: application/json
Date: Fri, 13 Mar 2026 08:56:28 GMT

[
  "2",
  "2"
]

总结

一套简单的抽奖系统,借助 Redis Set 的两个命令——SPOP 和 SRANDMEMBER,就能区分“大奖不放回”和“阳光普照可重复”两种核心场景。代码量不大,性能却足够应对高并发。如果你正在设计类似的抽奖活动,不妨直接拿来用。

来源:https://www.jb51.net/database/360450hzs.htm
上一篇Redis缓存机制原理详解及完整使用配置说明 下一篇RedisTemplate获取值失败的常见原因与解决方法
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Redis是什么:核心特性、架构与应用场景解析
数据库 · 2026-09-01

Redis是什么:核心特性、架构与应用场景解析

Redis是一款基于内存的键值型NoSQL数据库,以超高读写速度和丰富的数据结构著称。本文系统梳理Redis的核心特性、架构组成、性能优势及典型应用场景,并通过与Memcached、MySQL、MongoDB的对比,帮助开发者快速判断Redis是否适合当前业务需求。

Windows 安装 MongoDB 完整图文教程
数据库 · 2026-09-01

Windows 安装 MongoDB 完整图文教程

本文详细介绍在 Windows 系统上安装 MongoDB 的完整流程。从官网下载 MSI 安装包开始,逐步演示自定义安装路径、配置 Windows 服务、跳过 MongoDB Compass 等关键选项,并提供通过系统服务列表验证安装是否成功的方法,帮助开发者快速搭建本地 MongoDB 环境。

Linux 安装 MongoDB 完整指南:依赖配置、环境变量与服务启动
数据库 · 2026-09-01

Linux 安装 MongoDB 完整指南:依赖配置、环境变量与服务启动

本文详解在 Linux 系统下安装 MongoDB 的完整流程,涵盖依赖包安装、二进制包下载解压、环境变量配置、数据与日志目录创建及服务启动验证。通过标准化命令与路径说明,帮助开发者快速完成部署并确认服务状态。

MacOS安装MongoDB完整教程
数据库 · 2026-09-01

MacOS安装MongoDB完整教程

本文介绍在MacOS系统下安装MongoDB的完整流程,涵盖下载、解压、目录配置、环境变量设置及服务启动。通过明确的命令与参数说明,帮助开发者快速完成环境搭建并验证安装结果。

Ubuntu系统安装与配置Redis完整指南
数据库 · 2026-09-01

Ubuntu系统安装与配置Redis完整指南

本文详解在Ubuntu系统中安装Redis的两种主流方式:apt在线安装与源码编译安装。涵盖版本选择逻辑、服务启停与状态检查、连接验证方法,以及在线练习工具与桌面GUI客户端的对比与使用建议,帮助开发者快速搭建并验证Redis运行环境。