微信紅包知多少

緣起

其實從2014年,微信推出微信紅包,就有很多人揣測其中的算法是怎樣的,知乎上很多人分析了很多。

我一直覺得,應該就是幾個簡單的約束,然後拍腦袋一個算法,不需要特別的要求。

但還是有很多人會替開發人員做一些考慮,比如“不能讓一個人運氣太差,連續搶到0.01,這會讓用戶體驗很差”之類的要求,我覺得這是不可能的。

爲什麼?

很明顯,微信紅包這種極其高併發的請求,算法應該越簡單越好,不可能有太複雜的人性化約束。另一方面,假設紅包金額是完全隨機的,那麼一個人連續搶到0.01的概率其實是特別低的。

對於微信紅包的建模和約束,很多人已經做過很好的分析了。

問題爲:金額爲M,分成N個紅包

約束其實就簡單的一條——

每個紅包的金額,都是精確到小數點後兩位,且至少爲0.01元。

沒圖沒真相,我寫的代碼的效果如下(10塊錢分成5個紅包):
搶紅包的結果

感覺結果還算正常,偶發太高的金額,這個可以通過控制最大的金額的上限,比如有人提出,微信內部的人員告知,最大的金額是剩餘金額的平均值的兩倍。這樣是比較合理的,因爲一旦一個人搶了太高的金額,後邊的人很可能就只能在0.01左右徘徊了。

怎麼辦?

就隨機而言,很容易想到正態分佈。那麼,均值選多少?方差選多少?

我的代碼主體是下面這個函數:

它的邏輯很簡單,每次把均值設爲剩下金額的平均數,標準差設置爲均值和最小金額(0.01)的差值,爲何這樣選取的理由其實是很明顯了。

程序員的重點

我覺得算法不應該是重點,重點是如何設計一個適合收發紅包的高併發架構!

以下觀點來自於知乎上看文章(https://www.zhihu.com/question/22625187),以及自己的一些想法,不一定是好的,因爲沒實踐過。

問題1:紅包金額是事先分配好,然後逐個拿嗎?

解釋一下這個問題,意思是,比如10塊錢分成3個,做法是分成2.00,3.00,5.00,然後第一個人來的時候佔第一個位置,拿到了2.00……諸如此類。

答案是:肯定不需要啊。
如果事先分配好,假設你分配1000個紅包,豈不是得用1000倍的空間來存放?
如果放在內存裏,一臺服務器豈不是很容易爆?

所以更好的方法應該是:
1. 類似查表的,給每個目前存活的紅包(每個紅包只能存活兩天)編號,放在一張內存的表裏;
2. 表的每一行存放紅包剩餘總金額、紅包個數、當前已經被領取的個數;

爲了抵抗紅包被領完,還有大量無效的搶紅包請求進入後臺,還可以在進入後臺之前使用cache,原子遞減剩餘紅包個數,爲0的話,就馬上可以返回,不用進入後臺了。

問題2:進入後臺的搶同一個紅包的多個請求,需要一個隊列來序列化請求嗎?

知乎上的人說不需要隊列,但我覺得,如果沒有請求隊列的話,那同時有多個請求進入後臺搶同一個紅包,會發生什麼?
同時調用同個函數!
但這個分配紅包金額的函數,很明顯是不可重入的,因爲它依賴於上一次調用的結果。

雖然我沒接觸過內存數據庫,但我覺得,應該有一種類似鎖的機制來調度。比如多個線程同時想對一個紅包(在內存數據庫裏就是一行)進行修改,鎖機制會自動安排順序,所以就不需要請求隊列來強制從請求的粒度來使得搶紅包有序進行。

這種對數據庫的請求粒度,比對後臺的請求粒度更細,直觀上更加有效!

問題3:搶到紅包之後發生了什麼?

首先需要明確的一點是,錢肯定會被轉發到每個搶到紅包的人的賬戶裏。
但是,轉賬是一個很耗時的操作,所以應該馬上返回搶紅包的結果,而轉賬則是異步的操作
所以零錢到賬會有延遲!

再者,每個人可以看到其他人搶紅包的結果(如果該紅包是羣紅包的話),這個結果是來自哪裏呢?
硬盤?
內存?
cache?

我覺得應該放到cache裏爲好,因爲只有追加(新搶到紅包的記錄),不會修改條目(搶到多少就是多少),而且查詢的人跟搶紅包的人的數量差不多。

e,還有一個手氣最佳
這個,我覺得……在客戶端統計就可以了,哈哈哈,後臺完全沒必要做這件事。

比如你點查看搶紅包的記錄時,後臺只需要返回所有目前關於該紅包被搶的數據,總數爲多少個,每一個的所有者、金額、時間點,然後客戶端判斷是否已經搶完了(只有搶完才能判斷誰是手氣最佳),若是則找出金額最大、時間最早的一個紅包所有者,將其標記爲“手氣最佳”。

po代碼

代碼寫得略醜:

#include <stdlib.h>
#include <ctime>
#include <cmath>
#include <iostream>
#include <iomanip>
using namespace std;

class RedPacket {
public:
    RedPacket(float total, int number)
    : m_total(total), m_number(number), m_countOfUsed(0), goodSettings(true) {
        if (number <= 0) {
            cout << "【錯誤】紅包數量應該爲正整數" << endl;
            goodSettings = false;
        } else {
            if (round2(total) != total) {
                cout << "【錯誤】總金額應該只精小數點後兩位" << endl;
                goodSettings = false;
            } else {
                float average = round2(total / number);
                if (average < 0.01) {
                    cout << "【錯誤】總金額太少,或者紅包數量太多" << endl;
                    goodSettings = false;
                }
            }
        }
    }

    bool nextRedPacket() {
        // 紅包搶完了,或者設置出錯,則立即返回失敗
        if (m_countOfUsed >= m_number || !goodSettings) {
            return false;
        } else {
            float result;
            ++m_countOfUsed;
            // 最後一個紅包直接拿剩下的全額
            if (m_countOfUsed == m_number) {
                result = m_total;
            } else {
                // 限定每個人至少都能拿到0.01
                float minMoney = 0.01;
                float maxMoney = round2(m_total - (m_number - m_countOfUsed) * minMoney);

                // 如果金額沒得選,就只能這個了
                if (maxMoney == minMoney) {
                    result = maxMoney;
                } else {
                    // 保證紅包金額是隨機的,且滿足正態分佈:均值爲剩下金額的平均值,標準差爲平均值減去最小金額
                    do {
                        float average = m_total / (m_number - m_countOfUsed + 1);
                        float sigma = average - minMoney;
                        result = round2(GaussRand(sigma, average));
                    } while (result < minMoney || result > maxMoney);
                }
            }

            m_total -= result;
            cout << "第" << m_countOfUsed << "個紅包的金額爲:" << setprecision(2) << fixed << result << endl;
            return true;
        }
    }


private:
    // Box-Muller算法,產生標準正態分佈
    double GaussRand() {
        static double v1, v2, s;
        static int phase  = 0;
        double x;

        if (0 == phase) {
            do {
                double u1 = (double)rand() / RAND_MAX;
                double u2 = (double)rand() / RAND_MAX;

                v1 = 2 * u1 - 1;
                v2 = 2 * u2 - 1;
                s = v1 * v1 + v2 * v2;
            } while ( 1 <= s || 0 == s);
            x = v1 * sqrt(-2 * log(s) / s);
        } else {
            x = v2 * sqrt(-2 * log(s) / s);
        }
        phase = 1 - phase;

        return x; 
    }

    // 產生標準差爲S、均值爲E的正態分佈
    double GaussRand(double S, double E) {
        return GaussRand() * S + E;
    }

    // 把浮點數x按四捨五入,精確到小數點後兩位
    float round2(float x) {
        int ix = x * 100.0;
        if (ix % 10 >= 5)
            ix += 5;
        return ix / 100.0;
    }


private:
    int m_countOfUsed, m_number;
    float m_total;
    bool goodSettings;
};

int main() {
    srand(time(NULL));

    float total;
    int number;
    cin >> total >> number;
    RedPacket rp(total, number);
    while (rp.nextRedPacket());

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