记录一次基于redis抽奖固定奖品数量奖品分批次投放,超过奖品数量的客户直接返回幸运奖

需求

最近公司搞了个问卷调查的活动,用户填完问卷就能获得一次抽奖的就会,本来抽奖都是按概率来抽奖的,这种按概率的晚上挺多的相关实现方式,但是我们这个有个特定要求,就是奖品是固定的,抽奖分为三个阶段,第一阶段奖品固定个数,二等奖1个,三等奖2个,四等奖2个,五等奖15个,幸运奖180个。保证参与人数中奖率为100%。第二阶段,第三阶段我就不列出来啦,我把产品那边的需求文档放上来。


在这里插入图片描述

产品需求放在这,之前我也没做过此类抽奖的项目,去网上查找资料,基本全是按照概率来抽奖的,没有我们的这么操蛋。

我的实现思路

既然是抽奖,而且奖品固定,我想到的就是将所有的奖品都放到一个大容器中,用户抽奖时,抽到哪个就将相应的奖品数量从容器中移除掉一个。于是我开始对各个奖项设置为了各自抽奖编码,一等奖抽奖编码是 99999 二等奖是88888,三等奖是77777 一次类推,我做了下面这个表格方便查看。

  • 奖项数量及中奖代码
奖项 一等奖 二等奖 三等奖 四等奖 五等奖 幸运奖
物品 华为P30 kindle阅读器 蓝牙耳机 迷你风扇 定制小礼品 代金券或红包
数量 1 3 5 8 40 513
代码 99999 88888 77777 66666 55555 44444

数量和奖品代码搞清楚了,现在就在想那什么容器来装这些奖品代码呢?当时想过存到数据库中,现在奖品已经放到容器了,那第二个问题来了,我该如何保证这些奖品确保指定的奖品在固定的阶段被抽走呢?一共分为三阶段 0 - 200201 - 400401 - 570 超过570位参与者后,奖品全部都为幸运奖啦,准确的来分的话,可以分为前面三个阶段加上570 - ∞,这里就需要一个计数器来累计用户的参与人数,从而来判断该从第几阶段中抽奖, 于是想到redis中有个计数器的累加函数,想到这里同时想到redis中能存list类型的数据,所以打算放弃将数据存到数据库中,而是存到了reids中,既然分为四个阶段,我就将前面三个阶段的奖品以三个不同的key存储到redis中,以PRIZE_COUNT_200存放第一阶段的奖品,PRIZE_COUNT_400存放第二阶段的奖品,PRIZE_COUNT_570存放第三阶段的奖品。每次用户抽奖前,执行redis的自增函数,该函数在返回的时候回返回当前计数器的值,然后通过该值去判断该去哪个list中获取奖品,判断当前list的大小,并通过随机数生成一个该大小之间的一个整数,然后以该整数为下标去当前list中获取中奖码,拿到中奖码去数据库匹配当前奖品的余额,如果还有余额,将余额减1,并返回中奖结果。

  • 第一阶段奖品(PRIZE_COUNT_200)
奖项 一等奖 二等奖 三等奖 四等奖 五等奖 幸运奖
物品 华为P30 kindle阅读器 蓝牙耳机 迷你风扇 定制小礼品 代金券或红包
代码 99999 88888 77777 66666 55555 44444
数量 0 1 2 2 15 180
  • 第二阶段奖品(PRIZE_COUNT_400)
奖项 一等奖 二等奖 三等奖 四等奖 五等奖 幸运奖
物品 华为P30 kindle阅读器 蓝牙耳机 迷你风扇 定制小礼品 代金券或红包
代码 99999 88888 77777 66666 55555 44444
数量 0 1 2 2 15 180
  • 第三阶段奖品(PRIZE_COUNT_570)
奖项 一等奖 二等奖 三等奖 四等奖 五等奖 幸运奖
物品 华为P30 kindle阅读器 蓝牙耳机 迷你风扇 定制小礼品 代金券或红包
代码 99999 88888 77777 66666 55555 44444
数量 1 1 1 4 10 153

初始化奖品

这里我们先将抽奖代码放到各自对应的list当中,代码如下:

        //计数器初始化为0,并将三个list钟的数据全部清空
        listTools.initIncreCount(RedisKeyConstant.PRIZE_COUNT);
        listTools.delete(RedisKeyConstant.PRIZE_COUNT_200);
        listTools.delete(RedisKeyConstant.PRIZE_COUNT_400);
        listTools.delete(RedisKeyConstant.PRIZE_COUNT_570);



        //获取一个200以内随机数,将重要的奖品放到幸运奖中间位置
        int temp = new Random().nextInt(170);
        /****************第一阶段*************************/
        List list200 = new ArrayList<>();

        //三等奖 2个
        list200.add(LuckCodeEnum.GRADE_THREE.getCode());
        //四等奖 2个
        list200.add(LuckCodeEnum.GRADE_FOUR.getCode());

        //五等奖 15个
        for (int i = 0; i< 15; i++) {
            list200.add(LuckCodeEnum.GRADE_FIVE.getCode());
        }

        //幸运奖
        for (int j = 0; j < 180; j++) {
            list200.add(LuckCodeEnum.GRADE_LUCK.getCode());
            //放二等奖进去
            if (j == temp) {
                //二等奖 1个
                list200.add(LuckCodeEnum.GRADE_TWO.getCode());
                list200.add(LuckCodeEnum.GRADE_THREE.getCode());
                list200.add(LuckCodeEnum.GRADE_FOUR.getCode());
            }
        }
        /***********************************************/

        /*****************第二阶段***********************/
        List list400 = new ArrayList<>();
        //二等奖 1个
        list400.add(LuckCodeEnum.GRADE_TWO.getCode());
        //三等奖 2个
        list400.add(LuckCodeEnum.GRADE_THREE.getCode());
        list400.add(LuckCodeEnum.GRADE_THREE.getCode());
        //四等奖 2个
        list400.add(LuckCodeEnum.GRADE_FOUR.getCode());
        list400.add(LuckCodeEnum.GRADE_FOUR.getCode());

        //五等奖 15个
        for (int i = 0; i< 15; i++) {
            list400.add(LuckCodeEnum.GRADE_FIVE.getCode());
        }

        //幸运奖
        for (int j = 0; j < 180; j++) {
            list400.add(LuckCodeEnum.GRADE_LUCK.getCode());
        }
        /**********************************************/

        /*****************第三阶段***********************/
        List list570 = new ArrayList<>();

        //三等奖 2个
        list570.add(LuckCodeEnum.GRADE_THREE.getCode());
        //四等奖 4个
        list570.add(LuckCodeEnum.GRADE_FOUR.getCode());


        //五等奖 15个
        for (int i = 0; i< 10; i++) {
            list570.add(LuckCodeEnum.GRADE_FIVE.getCode());
        }

        int index = new Random().nextInt(130);
        //幸运奖
        for (int j = 0; j < 153; j++) {
            list570.add(LuckCodeEnum.GRADE_LUCK.getCode());
            if (index == j) {
                list570.add(LuckCodeEnum.GRADE_ONE.getCode());
                list570.add(LuckCodeEnum.GRADE_FOUR.getCode());

            }
            if (index +20 == j) {
                //二等奖 1个
                list570.add(LuckCodeEnum.GRADE_TWO.getCode());
                list570.add(LuckCodeEnum.GRADE_FOUR.getCode());
                list570.add(LuckCodeEnum.GRADE_FOUR.getCode());
            }
        }

        listTools.leftPushList(RedisKeyConstant.PRIZE_COUNT_200,list200);
        listTools.leftPushList(RedisKeyConstant.PRIZE_COUNT_400,list400);
        listTools.leftPushList(RedisKeyConstant.PRIZE_COUNT_570,list570);
        /**********************************************/

奖品已经放到了三个容器当前,现在就是将奖品数量持久化,放到数据库当中。表结构以及数量如下,这里幸运奖设置为9999是因为来抽奖的用户是未知的,第570位以后的用户,全部为幸运奖。


在这里插入图片描述

开始抽奖

假如当前用户为第208位来,当前计数器为 208 我们就该去PRIZE_COUNT_400该集合中获取奖品,但是奖品该怎么拿,并且确保每个奖品都被抽光呢?我个人的想法是,首先拿到PRIZE_COUNT_400该值得集合的size,然后通过随机数生成一个size以内的int类型的数,然后通过集合下标获取到中奖的号码。

//通过redis拿到当前的prize_count的大小
Long prizeCount = listTools.increCount(RedisKeyConstant.PRIZE_COUNT);
//判断该去哪个容器中获取奖品
if (prizeCount <= 570) {
    String prizeCodeKey = null;
    if (prizeCount <= 200) {
        prizeCodeKey = RedisKeyConstant.PRIZE_COUNT_200;
    } else if (prizeCount <= 400) {
        prizeCodeKey = RedisKeyConstant.PRIZE_COUNT_400;
    } else if (prizeCount <= 570) {
        prizeCodeKey = RedisKeyConstant.PRIZE_COUNT_570;
    }
 //获取到集合的大小
Long size = listTools.getListSize(prizeCodeKey);
 // 随机算法,获取随机下标并取出对应的奖品编码
 int index = new Random().nextInt(size.intValue());
 //获取到的中奖号码
 final String code = listTools.getIndexValue(prizeCodeKey,index);

通过获取到的中奖code,我们假如这里是88888去数据库查询当前奖品的余额数量, 如果余额大于0,则让该奖品数量减少1份,然后把redis中PRIZE_COUNT_400list中通过listOperations.remove(key,1,value);移除掉第一个遇到的88888,这样当前list的size就会减少1,按照该方法一直抽下去,list的size会越来越小,抽奖人数计数器等于400的时候,该list中的奖品全部被抽完。

抽奖核心代码

       //判断该用户是否已经中奖
        boolean flag = userService.isCanLuckDraw(name);
        if (!flag) {
            log.warn("{},已经抽过奖,无需重复抽奖",name);
            throw new LuckDrawException(ResultCode.LUCK_DRAW_AGAIN);
        }
        boolean isOk = false;
        //通过redis拿到当前的prize_count的大小
        Long prizeCount = listTools.increCount(RedisKeyConstant.PRIZE_COUNT);

        //默认幸运奖
        String luckCode = LuckCodeEnum.GRADE_LUCK.getCode();
        if (prizeCount <= 570) {
            String prizeCodeKey = null;
            if (prizeCount <= 200) {
                prizeCodeKey = RedisKeyConstant.PRIZE_COUNT_200;
            } else if (prizeCount <= 400) {
                prizeCodeKey = RedisKeyConstant.PRIZE_COUNT_400;
            } else if (prizeCount <= 570) {
                prizeCodeKey = RedisKeyConstant.PRIZE_COUNT_570;
            }

            //这里写循环是为了补偿那些拿到数据库没有的库存的奖品
            synchronized (PrizeController.class) {
                for (int i = 0; i < 3; i++) {
                    Long size = listTools.getListSize(prizeCodeKey);
                    // 随机算法,获取随机下标并取出对应的奖品编码
                    int index = new Random().nextInt(size.intValue());
                    //获取到的中奖号码
                    final String code = listTools.getIndexValue(prizeCodeKey,index);
                    //将中奖号码赋予给外层的中奖号码
                    luckCode = code;
                    //执行的结果,返回true表示数据库还有余额
                    //取数据库核对该奖品是否还有余额,有自减 没有重新进行随机算法 最后将中奖信息插入数据库
                    isOk = prizeService.resultTrue(name,code);
                    if (isOk) {
                        //移除redis中对应list中的一个奖项
                        listTools.removeFirstValue(prizeCodeKey,code);
                        break;
                    }
                    log.error("{},获取{}奖品,库存余额不足 这是第几次获取:{}",name,luckCode,i+1);
                }
            }
        }
        //如果该代码执行了说明拿出的奖品数据库余额不足,直接返回幸运奖
        if (!isOk) {
            if (prizeCount > 570)
                log.warn("{},奖品已经全部发放完毕,直接返回幸运奖",name);
            else
                log.error("{},拿到的奖品数据库余额不足,直接返回幸运奖",name);
            prizeService.resultTrue(name, luckCode);
        }
        return ResultUtils.success("您中的奖为 ["+luckCode+"]");

这就是我自己的自创该次抽奖的想法,有什么说的不对的地方请各位能够提出来一起讨论一下。

项目源码:https://github.com/niezhiliang/luck-draw

你可能感兴趣的:(记录一次基于redis抽奖固定奖品数量奖品分批次投放,超过奖品数量的客户直接返回幸运奖)