奖品数量奖品分批次投放,超过奖品数量的客户直接返回幸运奖"/>
记录一次基于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 - 200
、201 - 400
、401 - 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_400
list中通过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);elselog.error("{},拿到的奖品数据库余额不足,直接返回幸运奖",name);prizeService.resultTrue(name, luckCode);}return ResultUtils.success("您中的奖为 ["+luckCode+"]");
这就是我自己的自创该次抽奖的想法,有什么说的不对的地方请各位能够提出来一起讨论一下。
项目源码:
更多推荐
记录一次基于redis抽奖固定奖品数量奖品分批次投放,超过奖品数量的客户直接返回幸运奖
发布评论