搶紅包問題隨想

    前些天看了一篇文章,內容是講述分佈式鎖的,舉的例子是搶紅包的例子,大意是說10w人分1億紅包。其大致的思想是通過分佈式鎖來控制紅包的金額,每來一個請求就按照當前紅包剩餘金額分配額度,直至紅包剩餘金額爲0爲止。文章的鏈接地址如下:https://mp.weixin.qq.com/s/NQuefbBGUIpOEgcxd7qUXQ

     這裏不討論分佈式鎖的問題,今天要討論的是:如果是我,我該如何解決10w人/10億人搶1億/10億紅包的問題?

     這個問題可以分爲兩步,第一步解決小規模的搶紅包問題。這個問題解決了,我們再解決第二步,大規模的搶紅包問題。

     平常我們都玩微信和QQ搶紅包,基本上都是幾十、幾百人或者一兩千人的人規模去搶一個小紅包,一般金額不會超過1000。那麼,這種小規模的搶紅包該如何設計呢?是像前面鏈接的文章中所說,每來一個請求就實時隨機生成一個金額分配出去嗎?這樣做也不是不可以,但還有其他方式,比如我事先先把紅包生成好,按照一定的數量,比如100個人搶200元隨機紅包(隨機成30個),那我們可以先生成30個紅包,再讓這100個人搶30個紅包即可。所以小規模的搶紅包問題,也變成了兩個問題:

     1、如何生成隨機紅包

     2、如何解決瞬時大併發情況下紅包被合理的搶出去,不超發,不超額。

   1)如何生成隨機紅包

     要解決這個問題,那我們在繼續縮小規模,比如10元紅包隨機生成7個。我們可以將這10元紅包看做一個只有10個元素的數組A,將數組A隨機分成7份,有兩種方式。

     1、跟將一根長度10的木棍分成七分類似,將木棍砍斷6次,就變成了7個小木棍。那麼,我們在數組A元素0與9之間,隨機生成6個截斷節點,每個兩個相鄰節點的差值就是紅包的金額了。

     2、將10個均等長度的短木棍變成7個不等長度的木棍,將這10個均等的小木棍隨機選出三個,跟其他剩餘7個木棍隨機組合(可以將這3個與其他7箇中的一個組合起來,也可以與7箇中的兩個或者三個組合起來)起來就變成了7個木棍了。那麼,我們在數組A的元素中隨機找三個節點,與這三個節點相鄰的節點間的差值就是紅包金額。

    如下圖所示:

     

策略1,就是隨機成成7個截斷節點,這七個幾點之間的差值就是紅包大小。

測略2,就是隨機生成3個合併節點,剩餘七個幾點之間的差值就是紅包大小。 

附代碼如下:

import java.util.concurrent.ThreadLocalRandom;

/**
 * 隨機紅包生成器
 * 
 * @author zhanglulu
 *
 */
public class RandomRedPackageGenerator {

	public static void main(String[] args) {
		long begin = System.currentTimeMillis();
		for (int j = 0; j < 1; j++) {
			int tempMoney = 0;
			int[] subMoney = generateRedPackage(0.04f, 3);
			for (int i = 0; i < subMoney.length; i++) {
				tempMoney += subMoney[i];
				System.out.println("sumMoney[" + (i + 1) + "] = " + String.format("%.2f", (subMoney[i] / 100f)) + "元");
			}
			System.out.println("tempMoney =" + String.format("%.2f", (tempMoney / 100f)) + "元");
		}
		System.out.println("cost:" + (System.currentTimeMillis() - begin) + "ms");

	}

	/**
	 * 用於產生隨機大小的紅包。邏輯思想如下: 將sumAmt元的紅包轉成sumAmt*100數組A
	 * (1)如果紅包數量小於A的長度的一半,在數組A的整個長度內生成subNum-1個節點。每個相鄰兩個節點的差值就是隨機紅包的大小
	 * (2)如果紅包數量大於A的長度的一半,在數組A中隨機生成(A的長度-紅包數量)個合併節點,剩下的節點之家的差值就是隨機紅包的大小
	 * 
	 * @param sumAmt 紅包的總額,單位:元
	 * @param subNum 隨機紅包的個數
	 * @return
	 */
	public static int[] generateRedPackage(float sumAmt, int subNum) {
		int[] subMoney = new int[subNum];
		int sumAmtFen = (int)(sumAmt * 100);
		if (subNum > sumAmtFen) {
			throw new RuntimeException("總金額" + sumAmt + "元,隨機紅包個數不得超過" + sumAmtFen + "個");
		}
		if (subNum == 1) {
			subMoney[0] = sumAmtFen;
			return subMoney;
		}
		if (subNum == sumAmtFen) {
			for (int i = 0; i < subMoney.length; i++) {
				subMoney[i] = 1;
			}
			return subMoney;
		}

		boolean[] sumAmtArr = new boolean[sumAmtFen];
		int maxRange = sumAmtArr.length - 1;
		//如果生成的紅包數量超過了整個紅包大小的半數,則反其道而行之。
		if (subNum > sumAmtFen / 2) {
			for (int i = sumAmtFen - subNum; i > 0; i--) {
				int index = ThreadLocalRandom.current().nextInt(0, maxRange);
				while (index < 0 || sumAmtArr[index]) {
					index = ThreadLocalRandom.current().nextInt(0, maxRange);
				}
				sumAmtArr[index] = true;
			}
			// 記錄sumAmtArr中元素爲false的元素下標
			int[] falseArray = new int[subNum];
			int index = 0;
			for (int i = 0; i < sumAmtArr.length; i++) {
				if (!sumAmtArr[i]) {
					falseArray[index] = i;
					index++;
				}
			}
			// falseArray中的元素就是sumAmtArr數組中爲false的元素下標,獲取相鄰的差值就可以得出隨機紅包的大小
			subMoney[0] = falseArray[0] + 1;
			for (int i = 1; i < falseArray.length; i++) {
				subMoney[i] = falseArray[i] - falseArray[i - 1];
			}
			return subMoney;

		} else {// subNum個人分,那麼只需要在數組中產生subNum-1個節點即可,將sumAmt元紅包轉換成sumAmt*100的bool數組,將隨機產生的下標所對應的元素置爲true
			int keyPoint = subNum - 1;
			for (int i = keyPoint; i > 0; i--) {
				int index = ThreadLocalRandom.current().nextInt(0, maxRange);
				while (index < 0 || sumAmtArr[index]) {
					index = ThreadLocalRandom.current().nextInt(0, maxRange);
				}
				sumAmtArr[index] = true;
			}

			// 將sumAmtArr數組中爲true的元素下標記錄在trueArray中
			int[] trueArray = new int[keyPoint];
			int index = 0;
			for (int i = 0; i < sumAmtArr.length; i++) {
				if (sumAmtArr[i]) {
					trueArray[index] = i;
					index++;
				}
			}
			// trueArray中的元素就是sumAmtArr數組中爲true的元素下標,獲取相鄰的差值就可以得出隨機紅包的大小
			subMoney[0] = trueArray[0] + 1;
			for (int i = 1; i < trueArray.length; i++) {
				subMoney[i] = trueArray[i] - trueArray[i - 1];
			}
			subMoney[subMoney.length - 1] = (sumAmtArr.length - 1) - trueArray[trueArray.length - 1];
			return subMoney;
		}
	}

}

2)如何解決打併發場景下紅包的分配問題

1、首先我們簡單的設計出我們的數據庫表結構梳理思路,只簡單設計關於紅包這一塊,至於用戶、訂單、支付等就不列出,否則就過於複雜 。設計了兩張表,一張用於存儲主紅包記錄(即:用於記錄發紅包的用戶以及紅包信息)、一張表用於存儲生成的隨機紅包的記錄。兩張表通過紅包名稱關聯,表結構以及關聯關係如下:

每一個發出去的紅包都需要在主紅包記錄中保存,同時在生成隨機紅包之後插入隨機紅包記錄,此時紅包屬主字段是沒有值的,只有在用戶搶到該紅包之後,才更新該值。

2、將隨機生成的紅包以隊列的形式存放於redis緩存中,每當有用戶搶紅包的請求抵達之後,直接從該隊列取出一個值即可,如果取到該值,則直接返回紅包搶成功,同時發送一條MQ消息告知訂單或者支付系統更新相應的信息,如果未取到值(隊列空了),則直接返回客戶端紅包已搶完。大體流程圖如下:

3、當然單一的系統是無法解決搶紅包的這種複雜的問題,需要一系列系統通力合作才能達到效果。下面是一個簡單的搶紅包的應用架構圖,這裏並未考慮網絡、系統架構層面的問題,比如:CDN、防火牆、服務分層、網絡分層等因素,僅做思路梳理之用

  1)、作爲一個面對互聯網系統,對外的驗證和防禦系統是必要的,用於抵禦Ddos、重複盜刷、惡意訪問等問題,在這裏可以攔截大部分流量

  2)、需要web站點,在站點層攔截大部分流量,通過身份驗證、黑白名單過濾、惡意重複提交過濾等手段攔截流量

  3)、紅包生成之後在redis緩存設置紅包總量(總個數),在每個搶紅包的請求抵達之後,通過redis原子遞減的方式限制紅包數量,服務端獲取到的那個值就是數據庫中要更新的紅包記錄索引,避免超發的同時也可以提供系統的吞吐量。

  4)、如果有大量的搶紅包的業務,比如像春節那種全國狂歡、紅包滿天飛的狀況,可以通過高可用MQ解耦,通過接收紅包搶成功的信息,在通過其他系統消費MQ消息解決落庫問題。

3)回到初衷

      回到當初所提出的問題:如何解決10w人/10億人搶1億/10億紅包的問題。這個問題是超大規模的問題,按照分治思想,解決大規模問題的關鍵就是降低規模。將這1億均衡的分佈到1000個集羣中去(能發10億紅包的企業應該不差錢的吧),每個集羣承受10萬的併發問題不大吧,同時將這10億紅包也均衡的分佈到這1000個集羣,相當於每個集羣的流量用戶搶100w的紅包。每個集羣內部搶紅包的邏輯其實都是一樣的。當然有人問,這10億紅包隨機成成1億個小紅包,不管有多牛逼的機器,等着一億隨機紅包生成完,黃花菜也涼了,這其實就是運營問題了,發出這種巨大紅包的人必然要充分吸引人們的眼球,賺足流量,就算是開搶也會讓人們憋足了勁兒,千呼萬盼之後纔開始的吧,想想阿里,每年春節的鉅額紅包都是恨不得提前一個月開始預熱,截止到某一天停止紅包發放名額,最後截止到除夕才真正揭曉你所能獲得紅包(事實上,可能這個紅包提前就已經完成分配了,說是搶,其實就是揭曉你能得到多大的紅包而已):。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章