记录一次基于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<String> 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<String> 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<String> 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,176评论 5 469
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,190评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,232评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,953评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,879评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,177评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,626评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,295评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,436评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,365评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,414评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,096评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,685评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,771评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,987评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,438评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,032评论 2 341

推荐阅读更多精彩内容

  • 抽奖活动API 获取参与抽奖名单 分页获取参与抽奖名单 导出参与名单的报表模板 导入参与名单 内定中奖名单列表 获...
    kingsonCai阅读 2,501评论 1 1
  • 一般的抽奖管理功能,基本是在一个奖池中放一堆奖品,分别给它们设置不同的数量和概率,在奖品没有发完的情况下,...
    wwking阅读 10,140评论 3 16
  • 一般的抽奖管理功能,基本是在一个奖池中放一堆奖品,分别给它们设置不同的数量和概率,在奖品没有发完的情况下,概...
    wwking02阅读 3,830评论 1 4
  • /* *经典的概率算法, *$proArr是一个预先设置的数组, *假设数组为:array(100,200,300...
    抬头纹小鑫阅读 841评论 0 2
  • 小组在做一个抽奖系统,现在给我分配到了抽奖逻辑这方面的实现。EMMM,拿到分配的时候是懵B的。 追加: 没有多余讲...
    娶个好听的昵称阅读 5,385评论 1 5