抽奖活动是电商平台、社交媒体和各类营销活动中常见的互动方式。本文将介绍如何利用Redis的Set数据结构实现一个高性能的抽奖系统。

1、Redis Set数据结构简介

Redis Set是一个无序的字符串集合,它支持高效的添加、删除和查询操作,并且提供了丰富的集合运算能力。这些特性使其非常适合实现抽奖功能:

  • 元素唯一性:确保用户不会重复参与抽奖

  • 无序性:符合随机抽奖的需求

  • 高性能:所有操作的时间复杂度都是O(1)

2、Redis Set相关命令

2.1、SADD

SADD key member [member ...]

将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。

假如 key 不存在,则创建一个只包含 member 元素作成员的集合。

当 key 不是集合类型时,返回一个错误。

时间复杂度: O(N), N 是被添加的元素的数量。

返回值: 被添加到集合中的新元素的数量,不包括被忽略的元素。

示例:

127.0.0.1:6379> sadd luckydraw 111 222 333 444 555 666 777 888 999 000

(integer) 10

2.2、SMEMBERS

SMEMBERS key

返回集合 key 中的所有成员。不存在的 key 被视为空集合。

时间复杂度:O(N), N 为集合的基数。

返回值: 集合中的所有成员。

示例:

127.0.0.1:6379> smembers luckydraw
1) "777"
2) "222"
3) "666"
4) "555"
5) "000"
6) "888"
7) "333"
8) "111"
9) "444"
10) "999"

2.3、SRANDMEMBER

SRANDMEMBER key [count]

随机返回集合中的count个参数,count为可选参数,没有提供的话,只返回集合中的一个随机元素。

时间复杂度:

只提供 key 参数时为 O(1) 。

如果提供了 count 参数,那么为 O(N) ,N 为返回数组的元素个数。

返回值:

只提供 key 参数时,返回一个元素;如果集合为空,返回 nil 。

如果提供了 count 参数,那么返回一个数组;如果集合为空,返回空数组。

示例:

127.0.0.1:6379> sadd luckydraw 111 222 333 444 555 666 777 888 999 000
(integer) 10
127.0.0.1:6379> srandmember luckydraw
"999"
127.0.0.1:6379> srandmember luckydraw 2
1) "333"
2) "111"
127.0.0.1:6379> srandmember luckydraw 3
1) "222"
2) "777"
3) "333"
127.0.0.1:6379> srandmember luckydraw 4
1) "222"
2) "777"
3) "666"
4) "333"

2.4、SPOP

SPOP key [count]

移除并返回集合中的count个随机元素。count为可选参数,没有提供的话,只移除并返回集合中的一个随机元素。

如果只想获取随机元素,不想该元素从集合中被移除,可以使用 SRANDMEMBER 命令。

时间复杂度: O(1)

返回值:

被移除的随机元素。

当 key 不存在或 key 是空集时,返回 nil 。

示例:

127.0.0.1:6379> spop luckydraw 1
1) "444"
127.0.0.1:6379> spop luckydraw 2
1) "666"
2) "777"
127.0.0.1:6379> spop luckydraw 3
1) "111"
2) "000"
3) "333"
127.0.0.1:6379> spop luckydraw 4
1) "222"
2) "555"
3) "888"
4) "999"
127.0.0.1:6379> spop luckydraw
(nil)

3、使用redis set实现抽奖小程序

3.1、抽奖需要使用的redis命令

将参与抽奖的人员加入集合:

SADD key {userId}

查看参与抽奖的人员:

SMEMBERS key

获取中奖人员:

SRANDMEMBER key [count]

SPOP key [count]

像微信抽奖小程序中抽取单个奖品可以使用SRANDMEMBER;如果是年会中的抽奖分一等奖、二等奖、三等奖...抽了三等奖的就不允许再抽其他奖,这个时候就需要使用SPOP,抽中了某个奖品后从人员集合中删除。

3.2、使用Spring中的RedisTemplate

首先确保你的Spring Boot项目中已经集成了Redis:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置application.yml:

spring:
  redis:
    host: localhost
    port: 6379
    database: 0

3.3、核心服务实现

接口:

public interface LotteryService {
    /**
     * 创建抽奖活动
     */
    boolean createLottery(String activityId);
    
    /**
     * 用户参与抽奖
     */
    boolean participate(String activityId, String userId);
    
    /**
     * 随机抽取获奖者(不删除)
     */
    Set<String> drawWinners(String activityId, int count);
    
    /**
     * 随机抽取获奖者并移除
     */
    Set<String> drawAndRemoveWinners(String activityId, int count);
    
    /**
     * 获取所有参与者
     */
    Set<String> getParticipants(String activityId);
    
    /**
     * 获取参与者数量
     */
    Long getParticipantCount(String activityId);
    
    /**
     * 检查用户是否参与
     */
    boolean isParticipated(String activityId, String userId);
    
    /**
     * 重置抽奖活动
     */
    void resetLottery(String activityId);
}

实现:

@Service
public class LotteryServiceImpl implements LotteryService {
    
    private final RedisTemplate<String, String> redisTemplate;
    
    // 使用构造器注入
    public LotteryServiceImpl(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    @Override
    public boolean createLottery(String activityId) {
        String key = getKey(activityId);
        // 检查是否已存在
        if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
            return false;
        }
        return true;
    }
    
    @Override
    public boolean participate(String activityId, String userId) {
        String key = getKey(activityId);
        // 使用SADD添加用户,返回添加成功的数量
        Long result = redisTemplate.opsForSet().add(key, userId);
        return result != null && result > 0;
    }
    
    @Override
    public Set<String> drawWinners(String activityId, int count) {
        String key = getKey(activityId);
        // 使用SRANDMEMBER随机抽取(不删除元素)
        return redisTemplate.opsForSet().distinctRandomMembers(key, count);
    }
    
    @Override
    public Set<String> drawAndRemoveWinners(String activityId, int count) {
        String key = getKey(activityId);
        // 使用SPOP随机抽取并删除(防止重复获奖)
        return redisTemplate.opsForSet().pop(key, count);
    }
    
    @Override
    public Set<String> getParticipants(String activityId) {
        String key = getKey(activityId);
        return redisTemplate.opsForSet().members(key);
    }
    
    @Override
    public Long getParticipantCount(String activityId) {
        String key = getKey(activityId);
        return redisTemplate.opsForSet().size(key);
    }
    
    @Override
    public boolean isParticipated(String activityId, String userId) {
        String key = getKey(activityId);
        return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, userId));
    }
    
    @Override
    public void resetLottery(String activityId) {
        String key = getKey(activityId);
        redisTemplate.delete(key);
    }
    
    private String getKey(String activityId) {
        return "lottery:" + activityId;
    }
}

4、总结

利用Redis Set实现抽奖程序具有以下优势:

  1. 高性能:所有操作在内存中完成,响应迅速

  2. 简单可靠:利用Redis内置特性,代码简洁且稳定

  3. 可扩展:轻松支持从几百到数百万用户规模的抽奖活动

  4. 功能丰富:基于Set数据结构可轻松实现去重、随机抽取等核心功能

这种实现方式特别适合需要高并发、低延迟的抽奖场景,如电商大促、直播互动、社交媒体活动等。

redis命令参考:Commands | Docs