redis高并发抽奖
2019獨角獸企業重金招聘Python工程師標準>>>
redis高并發抽獎
? 代碼有點繚亂,沒時間整理,如果有誤還請留言斧正。現在進入正題。
一、思路
1.獎品:
? ? ? ? ? ? 獎品分為獎品id(編號)、count(數量)、pointVal(價值)、remainCount (剩余數量)分為四個參數組成。
2.概率規則:
? ? ? ? ? ? ? ? ? ?單個產品剩余數量/總產品剩余數量=單個產品概率*1000,這也就意味著每個獎品的概率都是隨著數量變化的,這時候計算概率對數據在時間維度要求必須一致(數據快照)。
? ? ? ? ? ? ? ? ? ?每個產品概率相加=1000,如果沒有等于1*概率相乘的數說明數據快照有問題(我這里是乘1000所以等于1000)。
3.設計思路:
? ? ? ? ? ? ? ? ? ?這個demo,我用的是取之于民用之于民的想法寫的,保證每一個人都會中獎。是不是很開心???。首先我們得有甲乙雙發,甲方就是抽獎系統的提供方,乙方就是我們這些抽獎的小百姓。甲方會先提供獎品出來,我這里是一,二,三,四獎項,有一個默認五等獎獎項(demo里面有一,二,三,四,五個獎項)。我會先計算甲方提供每個獎品的數量、價值然后計算所有獎品的總價值,然后用? 總價值/每次抽獎的分數=總抽獎次數。總抽獎次數 - 每個獎品的 = 默認五等獎次數。這樣我們就算出了所有獎品的數量(包括默認五等獎也就是安慰獎的次數)。沒當抽獎總次數==0的時候就會自動輪詢補充庫存開始新的一輪抽獎。
中獎公式 :
? ? ? ? ? ? ? ? 總抽獎消耗積分上限值/每次抽獎消耗固定積分 = 總抽獎次數?
? ? ? ? ? ? ? ? 總抽獎次數 - 已經抽獎數量 = 剩余抽獎次數?
? ? ? ? ? ? ? ? 獎品類型 * 獎品數量 = 獎品總數量
? ? ? ? ? ? ? ? 獎品總數量 - 已中獎數量 = 剩余獎品數量
? ? ? ? ? ? ? ? 剩余抽獎次數/剩余獎品數量 = 剩余獎品中獎概率
詳細算法可以看看這篇文章。
?
4.降級問題:
? ? ? ? ? ? ? ? ? ?這個一開始我也考慮過使用降級處理,以防服務器并發過大GG。不過我考慮到跟我的設計思路不符合,會影響到概率的公平性,我就把那部分去掉了(如果你們項目需要可以自己進行降級,限流,不過這會損失一部分概率的公平性,直接導致的結果就真正的獎品往往都是在最后面出現)。
5.注意事項:
? ? ? ? ? ? ? ? ? ?特別強調一點,每次輪詢的時候判斷數量一定要用 == 不用用 <=? 周末吃過這個虧,結果看輪詢數據快照的時候偶爾來個 -1,特蛋疼。花了很多時間排除問題。總結一點就是涉及的數量紅線變更的必須用精確判斷,不能范圍判斷。
6.技術難點:
? ? ? ? ? ? ? ? ? ?一、概率的計算
??? ? ? ? ? ? ? ? ?二、庫存變更
? ? ? ? ? ? ? ? ? ?三、輪詢策略
? ? ? ? ? ? ? ? ? ?基本就上面三個點,我這里是靈活使用redis 做緩存共享(也可以使用db的悲觀鎖、樂觀鎖、版本號),使用了其中管道技術做時間維度上的獎品數據快照解決概率計算的正確性;事物技術解決庫存變更;兩者結合解決了輪詢策略問題。做到了監控每個輪詢前每一個獎品數量,精確到每個輪詢每個用戶所中的獎品和順序。
二、代碼干貨
獎品類:
?
?public class Award {
/**編號*/
public String id;
/**數量(該類獎品數量)*/
public int count;
/**價值(該類獎品價值積分)*/
public int pointVal;
/**剩余數量(該類獎品剩余數量)*/
public int remainCount;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getPointVal() {
return pointVal;
}
public void setPointVal(int pointVal) {
this.pointVal = pointVal;
}
public int getRemainCount() {
return remainCount;
}
public void setRemainCount(int remainCount) {
this.remainCount = remainCount;
}
/**
*
* @param id *編號*
* @param count 數量(該類獎品數量)*
* @param pointVal 價值(該類獎品價值積分)*
* @param remainCount 剩余數量(該類獎品剩余數量)
*/
public Award( String id, int count, int pointVal, int remainCount) {
this.id = id;
this.count = count;
this.pointVal = pointVal;
this.remainCount = remainCount;
}
@Override
public String toString() {
return "Award [id=" + id + ", count=" + count + ", pointVal="
+ pointVal + ", remainCount=" + remainCount + "]";
}
}
概率計算:
?
詳細算法可以看看這篇文章。
?
?import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
/**
*
* @author XiongYC
* @date 2017年12月3日
*
*/
public class LotteryUtil {
public static String lottery(Jedis jedis) {
try {
/**
* 讀取redis 緩存參數(使用watch 確保數據準確性)
*/
Set<String> set = jedis.smembers(TestDemo.PRIZE_LIST); //獲取獎品列表(id)
List<String> list = new ArrayList<String>(set.size()+1); //獎品列表(有序)
//使用管道技術一次性獲取所有獎品數量確保數據完整性和概率計算的正確性
Pipeline p = jedis.pipelined();
for (String id : set) {
list.add(id); //增加獎品列表
p.get(id);//獲取換成獎品數量
}
list.add(TestDemo.RESIDUAL_QUANTITY);
p.get(TestDemo.RESIDUAL_QUANTITY);
List<Object> list1= p.syncAndReturnAll();//獲取所有獎品的剩余數量
int totailCount = Integer.valueOf(String.valueOf(list1.get(list1.size()-1))); //獲取剩余獎品總數
if (totailCount == 0) {
// 重置獎品
TestDemo.initData(jedis);
return "-1";
}
// 存儲每個獎品新的概率區間
List<Float> proSection = new ArrayList<Float>();
proSection.add(0f); //起始區間
float totalPro = 0f; // 總的概率區間
for (int i = 0; i < list1.size()-1; i++) {
// awardCount += Float.valueOf(jedis.get(id)); //計算獎品現有總數量
//彈性計算每個獎品的概率(剩余獎品數量/剩余總獎品數量) 每個概率區間為獎品概率乘以1000(把三位小數換為整)
totalPro += (Float.valueOf(String.valueOf(list1.get(i))) / Float.valueOf(String.valueOf(list1.get(list1.size()-1)))) * 1000;
proSection.add(totalPro);
}
// 獲取總的概率區間中的隨機數
Random random = new Random();
float randomPro = (float) random.nextInt((int) totalPro);
for (int i = 0, size = proSection.size(); i < size; i++) {
if (randomPro >= proSection.get(i) && randomPro < proSection.get(i + 1)) {
return list.get(i);
}
}
} catch (Exception e) {
System.err.println("概率之外計算錯誤" + e.getMessage());
return null;
}
return null;
}
}
?
庫存共享變更:
?
?import java.util.List;
import java.util.UUID;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
/**
*
* @author XiongYC
* @date 2017年12月3日
*
*/
public class MyRunnable1 implements Runnable {
private Jedis jedis = RedisPoolUtils.getJedis();
@Override
public void run() {
try {
// 查詢剩余獎品總數
String key = getPrize();
System.err.println("線程" + Thread.currentThread().getName() + "中獎獎品id為:" + key);
} catch (Exception e) {
System.err.println("算法計算異常:異常原因 = " + e.getMessage());
} finally {
RedisPoolUtils.returnResourceObject(jedis);
}
}
private String getPrize() {
String key = LotteryUtil.lottery(jedis); //獲取中獎獎品ID
jedis.watch(key,TestDemo.RESIDUAL_QUANTITY); //精確監控單個獎品剩余數
if("-1".equals(key) || "0".equals(jedis.get(key))){
jedis.unwatch();
key = getPrize();
}else{
// key = AvailablePrize(key);
Transaction tx = jedis.multi(); //開啟redis事物
tx.incrBy(TestDemo.RESIDUAL_QUANTITY, -1); //減少總庫存
tx.incrBy(key, -1); //減少中獎獎品總庫存
List<Object> listObj = tx.exec(); //提交事務,如果此時watch key被改動了,則返回null
if (listObj != null) { //多個進程同時 key>0 key相等時
// String useId = UUID.randomUUID().toString();
jedis.sadd("failuse", UUID.randomUUID().toString() + key);
System.out.println("用戶中獎成功!!!"); //中獎成功業務邏輯
} else {
key = getPrize(); //重新計算獎品
}
}
return key;
}
//是否是有效獎品
// private String AvailablePrize(String key) {
// int prizeNum = Integer.valueOf(jedis.get(key));
//
// //獎品無效重新計算驗證
// if(prizeNum <= 0){
// AvailablePrize(LotteryUtil.lottery(jedis));
// }
// return key;
// }
}
初始化:
?
?
?import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Transaction;
/**
*
* @author XiongYC
* @date 2017年12月3日
*
*/
public class TestDemo {
/**
* 安慰獎
*/
public static final String CONSOLATION_PRIZE_BY_ID= "2017112400005";
/**
* 緩存總次數 安慰獎+獎品
*/
public static final String RESIDUAL_QUANTITY= "residualQuantity";
/**
* 獎品列表(ID)
*/
public static final String PRIZE_LIST= "prizeList";
/**
* 每日抽獎所需積分
*/
public static final String POINTS_REQUIRED_FOR_THE_LOTTERY = "pointsRequiredForTheLlottery";
/**
* 輪詢成功次數
*/
public static final String POLLING_SUCCESS_NUM = "POLLING_SUCCESS_NUM";
/**
* 輪詢失敗次數
*/
public static final String POLLING_FAIL_NUM = "POLLING_FAIL_NUM";
/**
* 輪詢獎品數量快照
*/
public static final String SNAPSHOT_LIST = "SNAPSHOT_LIST";
/**
*中獎用戶記錄
*/
public static final String FAILUSE = "FAILUSE";
/**
* 從redis連接池獲取連接
*/
private static Jedis jedis = RedisPoolUtils.getJedis();
public static void main(String[] args) {
try {
// jedis.del(POLLING_SUCCESS_NUM,POLLING_FAIL_NUM,SNAPSHOT_LIST,FAILUSE);
// jedis.set(RESIDUAL_QUANTITY, "0");
jedis.flushAll();//清除數據庫
// 初始化獎品參數
initData(jedis);
ExecutorService executor = Executors.newFixedThreadPool(50);
for (int i = 0; i <2400; i++) {
executor.execute(new MyRunnable1());
}
executor.shutdown();
} catch (Exception e) {
System.err.println("ERROR_MSG = " + e.getMessage());
}finally{
if(jedis!=null){
RedisPoolUtils.returnResourceObject(jedis);//釋放redis資源
}
}
}
/**
* 初始化獎品參數
*/
public static void initData(Jedis jedis) {
Award award1 = new Award("2017112400001", 1, 2000, 1); //一等獎
Award award2 = new Award("2017112400002", 2, 1500, 2); //二等獎
Award award3 = new Award("2017112400003", 3, 1000, 3); //三等獎
Award award4 = new Award("2017112400004", 4, 500, 4); //四等獎
List<Award> list = new ArrayList<Award>();
list.add(award1);
list.add(award2);
list.add(award3);
list.add(award4);
int tailCount = 0; //總獎品數
int tailPoint = 0; //總獎品總積分值
/**
* 讀取redis 緩存參數(使用watch 確保數據準確性)
*/
Set<String> set = jedis.smembers(TestDemo.PRIZE_LIST); //獲取獎品列表(id)
//輪詢快照
Pipeline p = jedis.pipelined();
for (String id : set) {
p.get(id);//獲取換成獎品數量
}
p.get(RESIDUAL_QUANTITY);
List<Object> temp = p.syncAndReturnAll();//獲取所有獎品的剩余數量
//開始初始化緩存獎品數據
for (int i = 0; i < list.size(); i++) {
jedis.sadd(PRIZE_LIST, list.get(i).id); //緩存獎品列表
jedis.set(list.get(i).id, String.valueOf(list.get(i).count));//緩存獎品數量
tailCount +=list.get(i).count; //計算總獎品數
tailPoint += list.get(i).count* list.get(i).pointVal; //計算獎品總積分值
}
jedis.set(POINTS_REQUIRED_FOR_THE_LOTTERY, "500"); //每次抽獎所需積分
int residualQuantity = tailPoint / Integer.valueOf(jedis.get(POINTS_REQUIRED_FOR_THE_LOTTERY)); //計算未中獎次數(安慰獎)
int missesNum = residualQuantity - tailCount; //安慰劑次數
jedis.watch(RESIDUAL_QUANTITY);
int count = 0;
//判斷是否是初次輪詢
if(jedis.exists(RESIDUAL_QUANTITY)){
count= Integer.valueOf(jedis.get(RESIDUAL_QUANTITY));
}
if(count == 0){
Transaction tx = jedis.multi();
tx.set(CONSOLATION_PRIZE_BY_ID, String.valueOf(missesNum)); //緩存安慰獎次數
tx.sadd(PRIZE_LIST, CONSOLATION_PRIZE_BY_ID); //緩存安慰獎到獎品列表
tx.set(RESIDUAL_QUANTITY, String.valueOf(residualQuantity)); // 緩存總次數 安慰獎+獎品
List<Object> obj = tx.exec();
if (obj != null) { // 多個進程同時 key>0 key相等時
jedis.incrBy(POLLING_SUCCESS_NUM,1);
jedis.sadd(SNAPSHOT_LIST, jedis.get(POLLING_SUCCESS_NUM)+temp.toString());
System.out.println("初始化成功=============================》!!!");
} else {
jedis.incrBy(POLLING_FAIL_NUM,1);
System.err.println("初始化失敗=============================》!!!");
}
}else{
jedis.unwatch();
}
}
}
?
三、數據驗證
案例:根據以上demo我們可以看出有五個獎品(四個真正的獎品,一個安慰獎),我們根據設置的四個獎品可以得出 總積分價值為10000積分,每次抽獎500積分,一共需要抽20次完成一輪輪詢,其中真正的獎品為10個,其余的10為安慰獎個數。
論證:現在我們用50個線程跑2400個請求
預期效果:我們會有2400/20=120次輪詢,每次輪詢獎品剩余庫存數據快照為?輪詢次數+[0,0,0,0,0],每次輪詢抽獎結果為一等獎1個,二等獎2個,三等獎3個。四等獎4個,五等獎10個(安慰獎),共20個獎品;
?
實際效果:
? ? ? ? ? ? ? ? 輪詢次數120次。,--驗證通過
? ? ? ? ? ? ? ? 每次輪詢獎品剩余庫存數據快照
? ? ? ? ? ? ? ? 我截取了最好一部分數據快照,可以看出數據條數是可以對上輪詢次數的,快照數據也是沒問題的。--驗證通過
? ? ? ? ? ? ? ? 每次輪詢抽獎結果 :我隨機抽取了一個循環的獎品記錄
?初始化成功=============================》!!!
線程pool-1-thread-46中獎獎品id為:2017112400005
用戶中獎成功!!!
用戶中獎成功!!!
線程pool-1-thread-14中獎獎品id為:2017112400003
線程pool-1-thread-43中獎獎品id為:2017112400005
用戶中獎成功!!!
線程pool-1-thread-30中獎獎品id為:2017112400005用戶中獎成功!!!
線程pool-1-thread-36中獎獎品id為:2017112400001
用戶中獎成功!!!
線程pool-1-thread-13中獎獎品id為:2017112400002
用戶中獎成功!!!
線程pool-1-thread-49中獎獎品id為:2017112400005
用戶中獎成功!!!
線程pool-1-thread-5中獎獎品id為:2017112400005
用戶中獎成功!!!
線程pool-1-thread-25中獎獎品id為:2017112400005
用戶中獎成功!!!
線程pool-1-thread-35中獎獎品id為:2017112400003
用戶中獎成功!!!
線程pool-1-thread-18中獎獎品id為:2017112400003
用戶中獎成功!!!
用戶中獎成功!!!
線程pool-1-thread-25中獎獎品id為:2017112400004
用戶中獎成功!!!
線程pool-1-thread-38中獎獎品id為:2017112400005
用戶中獎成功!!!
線程pool-1-thread-15中獎獎品id為:2017112400004
用戶中獎成功!!!
線程pool-1-thread-48中獎獎品id為:2017112400004
用戶中獎成功!!!
線程pool-1-thread-7中獎獎品id為:2017112400005
用戶中獎成功!!!
線程pool-1-thread-37中獎獎品id為:2017112400002
用戶中獎成功!!!
線程pool-1-thread-12中獎獎品id為:2017112400004
線程pool-1-thread-31中獎獎品id為:2017112400005
用戶中獎成功!!!
用戶中獎成功!!!
線程pool-1-thread-1中獎獎品id為:2017112400005
?
后臺成功中獎記錄里面條數也對應上了2400請求(有的細心的小伙伴可能會直接去緩存里面按行數來去驗證獎品數量,我這里用的是集合,小伙伴們可以自行緩存list存儲驗證)。--驗證通過。
?
?
以上就是全部內容了,有點糙,還望見諒。
轉載于:https://my.oschina.net/demons99/blog/2231163
總結
以上是生活随笔為你收集整理的redis高并发抽奖的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vue-11-路由嵌套-参数传递-路由高
- 下一篇: 插入排序与希尔排序