需求
最近公司搞了個問卷調(diào)查的活動,用戶填完問卷就能獲得一次抽獎的就會谍肤,本來抽獎都是按概率來抽獎的啦租,這種按概率的晚上挺多的相關(guān)實現(xiàn)方式,但是我們這個有個特定要求荒揣,就是獎品是固定的篷角,抽獎分為三個階段,第一階段獎品固定個數(shù)系任,二等獎1個恳蹲,三等獎2個,四等獎2個赋除,五等獎15個阱缓,幸運獎180個。保證參與人數(shù)中獎率為100%举农。第二階段,第三階段我就不列出來啦敞嗡,我把產(chǎn)品那邊的需求文檔放上來颁糟。
產(chǎn)品需求放在這,之前我也沒做過此類抽獎的項目喉悴,去網(wǎng)上查找資料棱貌,基本全是按照概率來抽獎的,沒有我們的這么操蛋箕肃。
我的實現(xiàn)思路
既然是抽獎婚脱,而且獎品固定,我想到的就是將所有的獎品都放到一個大容器中勺像,用戶抽獎時障贸,抽到哪個就將相應(yīng)的獎品數(shù)量從容器中移除掉一個。于是我開始對各個獎項設(shè)置為了各自抽獎編碼吟宦,一等獎抽獎編碼是 99999
二等獎是88888
,三等獎是77777
一次類推篮洁,我做了下面這個表格方便查看。
- 獎項數(shù)量及中獎代碼
獎項 | 一等獎 | 二等獎 | 三等獎 | 四等獎 | 五等獎 | 幸運獎 |
---|---|---|---|---|---|---|
物品 | 華為P30 | kindle閱讀器 | 藍(lán)牙耳機 | 迷你風(fēng)扇 | 定制小禮品 | 代金券或紅包 |
數(shù)量 | 1 | 3 | 5 | 8 | 40 | 513 |
代碼 | 99999 | 88888 | 77777 | 66666 | 55555 | 44444 |
數(shù)量和獎品代碼搞清楚了殃姓,現(xiàn)在就在想那什么容器來裝這些獎品代碼呢袁波?當(dāng)時想過存到數(shù)據(jù)庫中,現(xiàn)在獎品已經(jīng)放到容器了蜗侈,那第二個問題來了篷牌,我該如何保證這些獎品確保指定的獎品在固定的階段被抽走呢?一共分為三階段 0 - 200
踏幻、201 - 400
枷颊、401 - 570
超過570位參與者后,獎品全部都為幸運獎啦,準(zhǔn)確的來分的話偷卧,可以分為前面三個階段加上570 - ∞
豺瘤,這里就需要一個計數(shù)器來累計用戶的參與人數(shù),從而來判斷該從第幾階段中抽獎听诸, 于是想到redis中有個計數(shù)器的累加函數(shù)坐求,想到這里同時想到redis中能存list類型的數(shù)據(jù),所以打算放棄將數(shù)據(jù)存到數(shù)據(jù)庫中晌梨,而是存到了reids中桥嗤,既然分為四個階段,我就將前面三個階段的獎品以三個不同的key存儲到redis中仔蝌,以PRIZE_COUNT_200
存放第一階段的獎品泛领,PRIZE_COUNT_400
存放第二階段的獎品,PRIZE_COUNT_570
存放第三階段的獎品敛惊。每次用戶抽獎前渊鞋,執(zhí)行redis的自增函數(shù),該函數(shù)在返回的時候回返回當(dāng)前計數(shù)器的值瞧挤,然后通過該值去判斷該去哪個list中獲取獎品,判斷當(dāng)前l(fā)ist的大小锡宋,并通過隨機數(shù)生成一個該大小之間的一個整數(shù),然后以該整數(shù)為下標(biāo)去當(dāng)前l(fā)ist中獲取中獎碼特恬,拿到中獎碼去數(shù)據(jù)庫匹配當(dāng)前獎品的余額执俩,如果還有余額,將余額減1癌刽,并返回中獎結(jié)果役首。
- 第一階段獎品(PRIZE_COUNT_200)
獎項 | 一等獎 | 二等獎 | 三等獎 | 四等獎 | 五等獎 | 幸運獎 |
---|---|---|---|---|---|---|
物品 | 華為P30 | kindle閱讀器 | 藍(lán)牙耳機 | 迷你風(fēng)扇 | 定制小禮品 | 代金券或紅包 |
代碼 | 99999 | 88888 | 77777 | 66666 | 55555 | 44444 |
數(shù)量 | 0 | 1 | 2 | 2 | 15 | 180 |
- 第二階段獎品(PRIZE_COUNT_400)
獎項 | 一等獎 | 二等獎 | 三等獎 | 四等獎 | 五等獎 | 幸運獎 |
---|---|---|---|---|---|---|
物品 | 華為P30 | kindle閱讀器 | 藍(lán)牙耳機 | 迷你風(fēng)扇 | 定制小禮品 | 代金券或紅包 |
代碼 | 99999 | 88888 | 77777 | 66666 | 55555 | 44444 |
數(shù)量 | 0 | 1 | 2 | 2 | 15 | 180 |
- 第三階段獎品(PRIZE_COUNT_570)
獎項 | 一等獎 | 二等獎 | 三等獎 | 四等獎 | 五等獎 | 幸運獎 |
---|---|---|---|---|---|---|
物品 | 華為P30 | kindle閱讀器 | 藍(lán)牙耳機 | 迷你風(fēng)扇 | 定制小禮品 | 代金券或紅包 |
代碼 | 99999 | 88888 | 77777 | 66666 | 55555 | 44444 |
數(shù)量 | 1 | 1 | 1 | 4 | 10 | 153 |
初始化獎品
這里我們先將抽獎代碼放到各自對應(yīng)的list當(dāng)中,代碼如下:
//計數(shù)器初始化為0显拜,并將三個list鐘的數(shù)據(jù)全部清空
listTools.initIncreCount(RedisKeyConstant.PRIZE_COUNT);
listTools.delete(RedisKeyConstant.PRIZE_COUNT_200);
listTools.delete(RedisKeyConstant.PRIZE_COUNT_400);
listTools.delete(RedisKeyConstant.PRIZE_COUNT_570);
//獲取一個200以內(nèi)隨機數(shù)衡奥,將重要的獎品放到幸運獎中間位置
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());
//放二等獎進(jìn)去
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);
/**********************************************/
獎品已經(jīng)放到了三個容器當(dāng)前,現(xiàn)在就是將獎品數(shù)量持久化讼油,放到數(shù)據(jù)庫當(dāng)中杰赛。表結(jié)構(gòu)以及數(shù)量如下,這里幸運獎設(shè)置為9999是因為來抽獎的用戶是未知的矮台,第570位以后的用戶乏屯,全部為幸運獎。
開始抽獎
假如當(dāng)前用戶為第208位來瘦赫,當(dāng)前計數(shù)器為 208
我們就該去PRIZE_COUNT_400
該集合中獲取獎品辰晕,但是獎品該怎么拿,并且確保每個獎品都被抽光呢确虱?我個人的想法是含友,首先拿到PRIZE_COUNT_400
該值得集合的size
,然后通過隨機數(shù)生成一個size
以內(nèi)的int類型的數(shù),然后通過集合下標(biāo)獲取到中獎的號碼。
//通過redis拿到當(dāng)前的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);
// 隨機算法窘问,獲取隨機下標(biāo)并取出對應(yīng)的獎品編碼
int index = new Random().nextInt(size.intValue());
//獲取到的中獎號碼
final String code = listTools.getIndexValue(prizeCodeKey,index);
通過獲取到的中獎code
辆童,我們假如這里是88888
去數(shù)據(jù)庫查詢當(dāng)前獎品的余額數(shù)量, 如果余額大于0惠赫,則讓該獎品數(shù)量減少1份把鉴,然后把redis中PRIZE_COUNT_400
list中通過listOperations.remove(key,1,value);
移除掉第一個遇到的88888
,這樣當(dāng)前l(fā)ist的size就會減少1,按照該方法一直抽下去儿咱,list的size會越來越小庭砍,抽獎人數(shù)計數(shù)器等于400的時候,該list中的獎品全部被抽完混埠。
抽獎核心代碼
//判斷該用戶是否已經(jīng)中獎
boolean flag = userService.isCanLuckDraw(name);
if (!flag) {
log.warn("{},已經(jīng)抽過獎怠缸,無需重復(fù)抽獎",name);
throw new LuckDrawException(ResultCode.LUCK_DRAW_AGAIN);
}
boolean isOk = false;
//通過redis拿到當(dāng)前的prize_count的大小
Long prizeCount = listTools.increCount(RedisKeyConstant.PRIZE_COUNT);
//默認(rèn)幸運獎
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;
}
//這里寫循環(huán)是為了補償那些拿到數(shù)據(jù)庫沒有的庫存的獎品
synchronized (PrizeController.class) {
for (int i = 0; i < 3; i++) {
Long size = listTools.getListSize(prizeCodeKey);
// 隨機算法,獲取隨機下標(biāo)并取出對應(yīng)的獎品編碼
int index = new Random().nextInt(size.intValue());
//獲取到的中獎號碼
final String code = listTools.getIndexValue(prizeCodeKey,index);
//將中獎號碼賦予給外層的中獎號碼
luckCode = code;
//執(zhí)行的結(jié)果钳宪,返回true表示數(shù)據(jù)庫還有余額
//取數(shù)據(jù)庫核對該獎品是否還有余額揭北,有自減 沒有重新進(jìn)行隨機算法 最后將中獎信息插入數(shù)據(jù)庫
isOk = prizeService.resultTrue(name,code);
if (isOk) {
//移除redis中對應(yīng)list中的一個獎項
listTools.removeFirstValue(prizeCodeKey,code);
break;
}
log.error("{},獲取{}獎品,庫存余額不足 這是第幾次獲取:{}",name,luckCode,i+1);
}
}
}
//如果該代碼執(zhí)行了說明拿出的獎品數(shù)據(jù)庫余額不足使套,直接返回幸運獎
if (!isOk) {
if (prizeCount > 570)
log.warn("{}罐呼,獎品已經(jīng)全部發(fā)放完畢,直接返回幸運獎",name);
else
log.error("{}侦高,拿到的獎品數(shù)據(jù)庫余額不足,直接返回幸運獎",name);
prizeService.resultTrue(name, luckCode);
}
return ResultUtils.success("您中的獎為 ["+luckCode+"]");
這就是我自己的自創(chuàng)該次抽獎的想法厌杜,有什么說的不對的地方請各位能夠提出來一起討論一下奉呛。