現(xiàn)與調(diào)參實(shí)踐)
每年公司群里發(fā)開(kāi)工紅包都會(huì)有同一個(gè)畫(huà)面一個(gè)人吆喝著“拼手氣了”下一秒幾十個(gè)人同時(shí)點(diǎn)進(jìn)去有人到手十幾塊有人只有 0.01。作為一個(gè)寫(xiě)了好幾年后端、也翻過(guò)不少隨機(jī)算法代碼的開(kāi)發(fā)者我一直覺(jué)得“微信拼手氣紅包算法”這種看起來(lái)很小的功能其實(shí)把概率設(shè)計(jì)、數(shù)值精度和并發(fā)扣減都?jí)涸谝黄鹆?。今天想聊的是技術(shù)社區(qū)里流傳最廣、也最常被復(fù)現(xiàn)的“二倍平均值法”并給出一套可以直接運(yùn)行的實(shí)現(xiàn)示范代碼。如果你是剛?cè)腴T(mén)的學(xué)生、準(zhǔn)備面試的求職者或者要在自己產(chǎn)品里做拼手氣紅包的開(kāi)發(fā)者這篇應(yīng)該能幫你少走不少?gòu)澛贰?. 為什么二倍平均值法能成為拼手氣紅包的主流玩法1.1 拼手氣紅包的三個(gè)隱藏需求很多人第一反應(yīng)是“拼手氣紅包不就是隨機(jī)分錢(qián)嗎”真正上手做的時(shí)候才會(huì)發(fā)現(xiàn)產(chǎn)品側(cè)從來(lái)不是只要一個(gè)隨機(jī)函數(shù)。拆開(kāi)來(lái)看拼手氣紅包至少要同時(shí)滿足三個(gè)訴求。第一個(gè)訴求是“整體公平”。這里說(shuō)的公平不是每個(gè)人拿一模一樣而是每個(gè)人在沒(méi)打開(kāi)紅包之前的期望收益應(yīng)該接近一致。換句話講如果 100 塊錢(qián)發(fā)給 10 個(gè)人任何一個(gè)參與者在搶之前都不應(yīng)該明顯吃虧。二倍平均值法天然滿足這一點(diǎn)每個(gè)人的理論期望就是“剩余金額 ÷ 剩余人數(shù)”也就是當(dāng)前均值。第二個(gè)訴求是“有驚喜”。如果系統(tǒng)真的按平均值精確分配每個(gè)人都拿 10 塊那就不叫拼手氣叫工資。要讓同一個(gè)紅包里出現(xiàn)“大包”和“小包”就必須允許單個(gè)樣本偏離均值足夠遠(yuǎn)同時(shí)又要約束偏離方向不能出現(xiàn)第一個(gè)人把 99% 的錢(qián)全部卷走的極端情況。第三個(gè)訴求是“實(shí)現(xiàn)簡(jiǎn)單、性能可控”。紅包功能往往伴隨高并發(fā)用戶點(diǎn)開(kāi)瞬間就要返回金額算法不能太重也不能依賴復(fù)雜的全局排序或集中計(jì)算。二倍平均值法每次只需要知道兩個(gè)數(shù)剩余金額和剩余人數(shù)做一次隨機(jī)數(shù)計(jì)算這幾乎就是為高并發(fā)場(chǎng)景量身定制的。1.2 與平均分配、完全隨機(jī)的差異把三種方案放到一起對(duì)比能看得更清楚。方案期望是否等于均值波動(dòng)程度極端風(fēng)險(xiǎn)實(shí)現(xiàn)成本平均分配是無(wú)無(wú)驚喜最低完全隨機(jī)是很大第一人可能搶走近全部很低二倍平均值法是中等上限受限單個(gè)紅包最多接近兩倍均值低“完全隨機(jī)”聽(tīng)起來(lái)很爽但問(wèn)題在于它沒(méi)有邊界。假設(shè)總金額 100 元、10 個(gè)人如果第一個(gè)人直接從剩余金額里均勻取一個(gè)數(shù)他有概率一次拿走一半甚至更多后面的人拿到的金額會(huì)迅速趨近于 0。這不符合“拼手氣”的體驗(yàn)更像是在抽獎(jiǎng)。二倍平均值法把每個(gè)紅包的上限卡在“當(dāng)前剩余均值 × 2”上。這個(gè)約束很有意思它允許你去沖擊當(dāng)前均值兩倍的金額但不會(huì)讓你無(wú)限放大第二個(gè)人能不能拿大包則取決于第一個(gè)人到底拿走了多少。正因?yàn)橛辛诉@層動(dòng)態(tài)約束整個(gè)紅包序列才會(huì)出現(xiàn)“有高低、但整體不離譜”的體驗(yàn)。提示我們這里討論的算法是社區(qū)里常見(jiàn)的復(fù)現(xiàn)版本不代表任何官方實(shí)現(xiàn)。官方到底怎么算屬于未公開(kāi)的業(yè)務(wù)細(xì)節(jié)但從工程復(fù)現(xiàn)角度看二倍平均值法是最接近“拼手氣”體驗(yàn)的簡(jiǎn)單模型。2. 概率模型拆解從當(dāng)前均值到“先搶為王”的演變2.1 規(guī)則與數(shù)學(xué)期望先把這個(gè)算法的規(guī)則講準(zhǔn)確。假設(shè)當(dāng)前剩余總金額是S當(dāng)前剩余人數(shù)是n那么當(dāng)前平均值為avg S / n當(dāng)前這一個(gè)人可搶到的金額區(qū)間是[min_money, min(2 * avg, S - (n - 1) * min_money)]如果n 1則從區(qū)間內(nèi)按均勻分布隨機(jī)取一個(gè)金額當(dāng)剩余人數(shù)為 1 時(shí)直接拿走全部剩余金額。第一個(gè)人可搶金額是一個(gè)均勻隨機(jī)變量取值范圍的下限是 0.01上限是當(dāng)前均值的兩倍。均勻分布的期望正好是區(qū)間中點(diǎn)所以第一個(gè)人拿到的金額期望就是avg。這一點(diǎn)非常關(guān)鍵它說(shuō)明系統(tǒng)的數(shù)學(xué)期望不會(huì)騙人所有參與者在搶之前的“平均預(yù)期”是一樣的。但期望相同不意味著分布相同。均勻分布的方差公式是(b - a)^2 / 12二倍平均法里a和b之間的跨度大約是2 * avg所以這一輪隨機(jī)金額的標(biāo)準(zhǔn)差大約是avg / sqrt(3)接近均值的 0.577 倍。也就是說(shuō)如果你搶到一個(gè)“低于均值不少”的紅包完全正常偏差在概率預(yù)期之內(nèi)。2.2 每一次搶完之后下一輪均值怎么變這個(gè)算法的魅力在于每一輪結(jié)束后下一輪的均值會(huì)被重新計(jì)算而新的均值取決于上一輪誰(shuí)拿走了多少。舉例來(lái)說(shuō)100 元分給 10 個(gè)人初始均值 10 元第一個(gè)人最多能搶 20 元。如果第一個(gè)人搶了 15 元剩余 85 元剩余 9 人新均值變成 85 ÷ 9 ≈ 9.44 元。也就是說(shuō)第二個(gè)人面前的平均水位已經(jīng)下降了他能拿到的上限也從 20 元降到了約 18.89 元。如果第一個(gè)人只搶了 2 元剩余 98 元剩余 9 人新均值變成 98 ÷ 9 ≈ 10.89 元。第二個(gè)人反而拿到了一個(gè)比第一個(gè)人更高的期望上限約 21.78 元。這種動(dòng)態(tài)調(diào)整機(jī)制保證了系統(tǒng)永遠(yuǎn)“跟著現(xiàn)實(shí)走”前面的人如果手氣好后面的人均值就會(huì)被攤薄前面的人如果手氣差后面的人反而有機(jī)會(huì)撿漏。整體期望雖然一致但路徑上會(huì)有明顯的波動(dòng)傳導(dǎo)。也正因?yàn)槿绱宋覀冊(cè)谥庇X(jué)上會(huì)認(rèn)為“先搶的人更容易拿大包”。這個(gè)說(shuō)法部分正確如果所有人的隨機(jī)結(jié)果都恰好達(dá)到各自區(qū)間的上半部分那么先搶的人會(huì)在一個(gè)更大的絕對(duì)值區(qū)間里抽獎(jiǎng)但如果先搶的人抽到小包后面的人均值反而會(huì)上修。嚴(yán)格說(shuō)不是“先搶必賺”而是“先搶的人承擔(dān)了更多方差后搶的人更容易跟著前人的結(jié)果波動(dòng)”。2.3 最后一人為什么總是只能吃“尾”二倍平均值法里有一個(gè)接近確定性的結(jié)局最后一個(gè)紅包不參與隨機(jī)他直接拿走剩余的全部金額。這就會(huì)形成一種特殊的尾部效應(yīng)。如果前面所有人都不幸地只搶到小包剩余金額會(huì)比較大最后一個(gè)人會(huì)拿到一個(gè)“意外巨款”反過(guò)來(lái)如果前面有人搶走了接近上限的大包最后一個(gè)人可能只剩 0.01。所以在真實(shí)場(chǎng)景里最后一個(gè)紅包金額的方差甚至?xí)绕渌恢酶?。它不是均勻隨機(jī)而是前面所有隨機(jī)結(jié)果的“剩余殘差”。設(shè)計(jì)人員如果想讓結(jié)尾更有儀式感比如“最后一個(gè)紅包必定不會(huì)太小”就需要在算法里給最后一人預(yù)留一定金額而不能放任殘差隨意波動(dòng)。這屬于變體設(shè)計(jì)后面聊調(diào)參的時(shí)候會(huì)再提到。3. 示范代碼用 Python 實(shí)現(xiàn)二倍平均值法3.1 為什么先把“元”換成“分”寫(xiě)代碼之前最容易被忽略的是數(shù)值精度。人民幣紅包習(xí)慣保留到分也就是小數(shù)點(diǎn)后兩位。如果直接用浮點(diǎn)數(shù)計(jì)算money round(random.uniform(0.01, upper), 2)多次累加之后很可能會(huì)出現(xiàn)99.999999999或者0.010000000001這類結(jié)果。浮點(diǎn)數(shù)在二進(jìn)制里無(wú)法精確表示十進(jìn)制小數(shù)這是計(jì)算機(jī)基礎(chǔ)問(wèn)題不是 random 的鍋。更穩(wěn)妥的做法是在函數(shù)入口把“元”轉(zhuǎn)換為“分”全程用整數(shù)做隨機(jī)和加減最后再統(tǒng)一除以 100 返回展示結(jié)果。整數(shù)運(yùn)算沒(méi)有精度問(wèn)題random.randint的上下限也都是整數(shù)邏輯清晰且可驗(yàn)證。3.2 完整代碼split_red_packet 的逐段說(shuō)明下面這段代碼可以直接復(fù)制運(yùn)行我用 Python 3.8 的寫(xiě)法實(shí)現(xiàn)import random def split_red_packet(total_money: float, count: int, min_money: float 0.01): if count 0: raise ValueError(紅包個(gè)數(shù)必須大于0) total_fen int(round(total_money * 100)) min_fen int(round(min_money * 100)) if total_fen count * min_fen: raise ValueError(總金額不足無(wú)法保證每個(gè)紅包至少達(dá)到最低金額) result [] remain_fen total_fen remain_count count for _ in range(count - 1): avg_fen remain_fen // remain_count # 二倍平均值理論上限是當(dāng)前均值的 2 倍 upper_by_double avg_fen * 2 # 但還要保證如果這個(gè)人搶得太狠后面每個(gè)人還能拿到最低金額 upper_by_reserve remain_fen - (remain_count - 1) * min_fen upper min(upper_by_double, upper_by_reserve) lower min_fen # 極端情況下比如剩余金額剛夠分配時(shí)上下限可能收攏到同一個(gè)數(shù) if upper lower: upper lower money_fen random.randint(lower, upper) result.append(money_fen / 100) remain_fen - money_fen remain_count - 1 # 最后一個(gè)人不隨機(jī)直接收下剩余金額 result.append(remain_fen / 100) return result if __name__ __main__: packets split_red_packet(100.00, 10) print(packets) print(總金額:, round(sum(packets), 2))逐段解釋一下設(shè)計(jì)意圖。upper_by_double avg_fen * 2是這個(gè)算法的核心也就是二倍平均值的由來(lái)。它保證單個(gè)紅包最多不超過(guò)當(dāng)前均值的兩倍同時(shí)因?yàn)橄孪奘莔in_fen所以這個(gè)紅包的期望會(huì)穩(wěn)定在均值附近。upper_by_reserve remain_fen - (remain_count - 1) * min_fen則是工程實(shí)現(xiàn)里必須補(bǔ)上的護(hù)欄。如果沒(méi)有這行當(dāng)出現(xiàn)“當(dāng)前紅包隨機(jī)值很大、剩余金額不夠后面人分”的情況時(shí)后面就會(huì)出現(xiàn)金額為 0 的紅包。加入這行之后無(wú)論隨機(jī)數(shù)取到哪里后面每個(gè)人至少能保住最低金額。if upper lower的兜底邏輯平時(shí)不會(huì)觸發(fā)但當(dāng)剩余金額緊張、均值逼近最低金額時(shí)會(huì)存在區(qū)間收攏到一點(diǎn)的情況。比如剩余 0.03 元給 3 個(gè)人此時(shí)均值就是 1 分上限也是 1 分直接取 1 分即可。3.3 跑一次的結(jié)果與驗(yàn)證方法我本地跑了一次 100 元分 10 人某一次的返回值如下[12.55, 10.78, 9.32, 6.09, 18.42, 4.5, 8.31, 14.76, 11.26, 4.01]可以看到既有 18.42 這種接近大包的金額也有 4.01 這種小包整體波動(dòng)明顯但總和始終正好等于 100.00。多次運(yùn)行后我可以得出幾個(gè)經(jīng)驗(yàn)規(guī)律第一個(gè)紅包落在均值附近的概率最高但極差可達(dá)兩倍均值。金額序列基本看不出規(guī)律前一個(gè)紅包變大后一個(gè)期望就變小。最后一個(gè)紅包經(jīng)常出現(xiàn)極端值要么很小要么意外很大。驗(yàn)證算法是否正確的辦法很簡(jiǎn)單固定總金額和人數(shù)循環(huán)執(zhí)行一萬(wàn)次檢查每次結(jié)果總和是否等于總金額檢查是否有任何紅包小于最低金額檢查每個(gè)紅包是否都不超過(guò)當(dāng)時(shí)的二倍平均值。這三條都通過(guò)基本可以認(rèn)定實(shí)現(xiàn)沒(méi)有邏輯問(wèn)題。4. 上限的雙重約束金額精度與最低金額控制4.1 動(dòng)態(tài)上限不能只看二倍平均值如果只按“剩余均值 × 2”作為上限代碼能跑但邊界會(huì)出問(wèn)題。舉個(gè)例子100 元分 50 人平均每人 2 元二倍上限 4 元。假如前 40 個(gè)人運(yùn)氣逆天每個(gè)人都搶到接近 4 元錢(qián)立刻就不夠了后面的人連最低的 0.01 都拿不到。這就是我在代碼里特意寫(xiě)upper_by_reserve的原因。它的含義是當(dāng)前這個(gè)人最多只能拿走“剩余金額減去后面每個(gè)人最低金額之后”的部分。本質(zhì)上是給每一個(gè)紅包都留出了“保底空間”避免隨機(jī)數(shù)突破系統(tǒng)底線。upper min(upper_by_double, upper_by_reserve)的含義很直白兩個(gè)上限誰(shuí)更小就用誰(shuí)。大多數(shù)情況下剩余金額還比較充裕時(shí)二倍平均值的上限會(huì)先被命中預(yù)留上限只是安全鎖但當(dāng)剩余人數(shù)多、剩余金額少時(shí)預(yù)留上限會(huì)提前接管讓隨機(jī)區(qū)間變得越來(lái)越窄。4.2 最低金額校驗(yàn)與異常處理入門(mén)實(shí)現(xiàn)最容易漏的是參數(shù)校驗(yàn)。total_money可能傳了負(fù)數(shù)count可能傳了 0也可能出現(xiàn)“總金額 0.5 元分給 100 個(gè)人”這種根本不可能滿足最低金額的請(qǐng)求。所以函數(shù)開(kāi)頭必須先做兩件事校驗(yàn)紅包個(gè)數(shù)必須大于 0校驗(yàn)總金額分 人數(shù) × 最低金額分。第一條避免了除零錯(cuò)誤第二條避免系統(tǒng)生成出“0 元紅包”這種根本不合法的結(jié)果。實(shí)際項(xiàng)目里我還會(huì)額外加一個(gè)“單個(gè)紅包金額上限”的業(yè)務(wù)參數(shù)比如運(yùn)營(yíng)希望單個(gè)紅包最多不能超過(guò) 200 元那就是在upper上再加一個(gè)min(upper, max_fen)。4.3 從 0.01 到 0.10運(yùn)營(yíng)場(chǎng)景下的最低限額定制很多場(chǎng)景并不想讓紅包出現(xiàn) 0.01。比如公司做活動(dòng)的時(shí)候希望“人人有份且至少有 0.5 元”這時(shí)候只需要在調(diào)用函數(shù)時(shí)傳入min_money0.50。我在版本迭代里遇到過(guò)這樣的需求用戶每天簽到可以領(lǐng)拼手氣紅包但產(chǎn)品要求最低不低于 0.1 元否則用戶會(huì)覺(jué)得打發(fā)叫花子。把min_fen從 1 分改成 10 分之后區(qū)間下限抬高期望也隨之抬高總金額和人數(shù)不變的前提下系統(tǒng)會(huì)自動(dòng)加大上限因?yàn)閡pper_by_double 2 * avg_fen本身不會(huì)變但下限抬高后隨機(jī)數(shù)整體右移偶爾會(huì)出現(xiàn)單包金額更大的情況。這也說(shuō)明二倍平均值法不是一個(gè)固定不變的“二倍”min_money本質(zhì)上是你對(duì)分配曲線的一個(gè)調(diào)節(jié)旋鈕。調(diào)大最低金額曲線整體上移調(diào)大倍數(shù)系數(shù)曲線波動(dòng)加大。把握好這兩個(gè)旋鈕基本可以應(yīng)付大多數(shù)產(chǎn)品需求。5. 隨搶隨算 vs 一次性預(yù)生成兩種紅包生成思路對(duì)比5.1 預(yù)生成方案的優(yōu)點(diǎn)和隱患另一種常見(jiàn)思路是創(chuàng)建紅包時(shí)直接一次性把所有人的金額全部生成好存入列表或數(shù)據(jù)庫(kù)用戶搶的時(shí)候按順序取一個(gè)。這種方案的優(yōu)點(diǎn)有兩個(gè)。第一總額絕對(duì)可控創(chuàng)建紅包時(shí)就把所有金額生成完畢后期不需要再動(dòng)態(tài)計(jì)算第二金額序列可以預(yù)先做審計(jì)能提前看到每一個(gè)紅包的具體數(shù)額方便排查問(wèn)題。但它也有明顯的隱患。最直接的問(wèn)題是“你很難嚴(yán)格做到并發(fā)安全”。用戶同時(shí)搶紅包時(shí)如果是純內(nèi)存列表需要加鎖如果用數(shù)據(jù)庫(kù)行需要處理并發(fā) UPDATE 的原子性。更麻煩的是一旦紅包設(shè)定為“剩余金額動(dòng)態(tài)變化”預(yù)生成方案就完全沒(méi)法用。5.2 動(dòng)態(tài)計(jì)算方案為什么更適合二倍平均值法二倍平均值法天然迎合“隨搶隨算”的模式用戶搶到紅包的那一刻系統(tǒng)只需要知道當(dāng)前剩余金額和剩余人數(shù)然后執(zhí)行一次隨機(jī)數(shù)計(jì)算扣減剩余金額。維度預(yù)生成方案動(dòng)態(tài)計(jì)算方案創(chuàng)建紅包開(kāi)銷生成完整序列耗時(shí)較長(zhǎng)只存總額和人數(shù)幾乎為零存儲(chǔ)負(fù)擔(dān)要存 n 個(gè)金額只需存兩個(gè)數(shù)字并發(fā)保護(hù)需要鎖或游標(biāo)避免多人拿到同一個(gè)金額只需原子扣減當(dāng)前余額動(dòng)態(tài)調(diào)整不支持中途改人數(shù)、改金額靈活支持審計(jì)難度高能準(zhǔn)確回放低但可用日志復(fù)現(xiàn)在真實(shí)紅包系統(tǒng)里用戶創(chuàng)建紅包那一刻并不知道會(huì)有多少人最終參與拼手氣紅包常常出現(xiàn)“發(fā)了 10 個(gè)但只被搶走 3 個(gè)”的情況剩余 7 個(gè)的金額還掛在賬上等下一次被搶。動(dòng)態(tài)計(jì)算方案在這種場(chǎng)景下優(yōu)勢(shì)非常明顯每次只算當(dāng)前這一下不關(guān)心未來(lái)還有沒(méi)有人搶。而預(yù)生成方案遇到這種半路停止的情況就會(huì)殘留一堆“已生成但未被搶走”的金額處理起來(lái)非常別扭。5.3 需要警惕的性能誤區(qū)有人會(huì)說(shuō)隨機(jī)數(shù)生成在高并發(fā)下是不是也慢實(shí)際上不是。一次random.randint的開(kāi)銷是微秒級(jí)別的與一次數(shù)據(jù)庫(kù)或緩存 RTT 相比幾乎可以忽略。真正消耗性能的不是算法本身而是并發(fā)扣減時(shí)對(duì)共享數(shù)據(jù)的爭(zhēng)搶。在做一個(gè)紅包活動(dòng)時(shí)我會(huì)把“隨機(jī)分配金額”和“賬戶扣減”拆成兩個(gè)階段。前者只是一個(gè)純函數(shù)無(wú)狀態(tài)且可重入后者走數(shù)據(jù)庫(kù)或緩存的事務(wù)扣減確保同一個(gè)紅包不能被兩個(gè)人搶到同一次。這個(gè)設(shè)計(jì)思路比糾結(jié)于“二倍還是三倍”要重要得多。6. 常見(jiàn)實(shí)現(xiàn)錯(cuò)誤與調(diào)參經(jīng)驗(yàn)我從真實(shí)項(xiàng)目里踩過(guò)的坑6.1 最常見(jiàn)四個(gè)錯(cuò)誤我見(jiàn)過(guò)不少剛接觸這個(gè)算法的同學(xué)寫(xiě)出來(lái)的代碼有些問(wèn)題很有代表性這里列成清單方便自查。第一個(gè)錯(cuò)誤是最后一個(gè)紅包沒(méi)有單獨(dú)處理。直接在循環(huán)里讓每個(gè)人都隨機(jī)最后發(fā)現(xiàn)金額對(duì)不上。因?yàn)殡S機(jī)過(guò)程中的上限是不斷變化的最后一個(gè)人的金額是“剩余殘差”不是再隨機(jī)一次的結(jié)果。正確的做法是循環(huán)只跑count - 1次最后一次直接取剩余金額。第二個(gè)錯(cuò)誤是隨機(jī)區(qū)間下限寫(xiě)成 0。random.uniform(0, upper)看起來(lái)也能跑但它會(huì)生成 0 元紅包這在微信支付語(yǔ)境下非法支付平臺(tái)也不可能受理 0 元交易。所以下限必須至少是 1 分。第三個(gè)錯(cuò)誤是沒(méi)有預(yù)留后面人的最低金額。單純寫(xiě)upper avg * 2在紅包個(gè)數(shù)較多、總金額緊張時(shí)會(huì)出現(xiàn)在第 10 個(gè)人之后剩余金額不足 1 分的情況。我在前面反復(fù)強(qiáng)調(diào)的upper_by_reserve就是為了堵住這個(gè)漏洞。第四個(gè)錯(cuò)誤是對(duì)浮點(diǎn)數(shù)結(jié)果直接做round但在循環(huán)里持續(xù)減的是未取整的浮點(diǎn)數(shù)。比如第一次隨機(jī)得到 12.345round(12.35)返回 12.35但剩余金額扣的是 12.345第 5 次之后累計(jì)誤差就會(huì)浮出水面。用整數(shù)分計(jì)算可以從根上消滅這個(gè)坑。6.2 把“二倍”改成 1.5 倍或 3 倍會(huì)怎樣二倍平均值法里的“二倍”不是神圣不可動(dòng)的它實(shí)際上是一個(gè)可調(diào)系數(shù)我傾向于用k表示upper avg * k把k調(diào)小紅包分配會(huì)更“平穩(wěn)”。比如k 1.1時(shí)每個(gè)紅包最多只能比當(dāng)前均值高一成大家拿到手的金額會(huì)非常接近驚喜感會(huì)變?nèi)醯m合“求穩(wěn)不求爽”的場(chǎng)景比如公司福利補(bǔ)貼。把k調(diào)大比如k 3單紅包的上限更高大額紅包更容易出現(xiàn)但小金額紅包也會(huì)更加頻繁整體體驗(yàn)更刺激代價(jià)是后面的人吃土概率增大。系數(shù) k紅包波動(dòng)典型案例適用場(chǎng)景1.5較小100 元 10 人最高約 30福利發(fā)放、人人差異不大2.0中等100 元 10 人最高約 20通用拼手氣紅包3.0明顯100 元 10 人最高約 30活動(dòng)引流、追求話題性這里還要注意k變大之后必須同步檢查upper_by_reserve。因?yàn)槎侗旧頃?huì)產(chǎn)生更大的單包金額預(yù)留約束會(huì)更容易被觸發(fā)如果不加這個(gè)約束金額不足的情況會(huì)更快暴露。6.3 生產(chǎn)環(huán)境下的并發(fā)與冪等補(bǔ)充算法寫(xiě)完后距離上線還差一層“工程包裝”。我個(gè)人在實(shí)際項(xiàng)目里會(huì)做三件事第一金額存儲(chǔ)全部用整型分?jǐn)?shù)據(jù)庫(kù)字段用int對(duì)外接口再轉(zhuǎn)字符串或元。這樣避免了多種語(yǔ)言環(huán)境下浮點(diǎn)數(shù)不一致的問(wèn)題。第二搶紅包操作必須原子。無(wú)論用 Redis 的 Lua 腳本還是數(shù)據(jù)庫(kù)的UPDATE ... WHERE remain_count 0都要確保兩個(gè)用戶不會(huì)同時(shí)拿到同一個(gè)紅包。二倍平均值法的隨機(jī)分配是冪等的但扣減動(dòng)作必須保護(hù)起來(lái)。第三同一個(gè)用戶重復(fù)點(diǎn)擊要有冪等開(kāi)關(guān)。用戶搶過(guò)一次之后后續(xù)請(qǐng)求直接返回同一個(gè)紅包金額不能再次觸發(fā)隨機(jī)算法分配新金額否則總額會(huì)對(duì)不上。最簡(jiǎn)單的方式是搶之前先查用戶是否已領(lǐng)過(guò)已領(lǐng)過(guò)就直接返回歷史記錄。最后再分享一點(diǎn)個(gè)人心得我在實(shí)際項(xiàng)目里發(fā)現(xiàn)把“紅包金額隨機(jī)算法”和“紅包狀態(tài)流轉(zhuǎn)”拆開(kāi)是最容易維護(hù)的結(jié)構(gòu)。算法層只需要保證一件事——給定剩余金額、剩余人數(shù)和參數(shù)返回一個(gè)合法紅包至于并發(fā)保護(hù)、狀態(tài)持久化、冪等控制全部放到服務(wù)層去處理。二倍平均值法本身并不復(fù)雜真正決定一個(gè)紅包功能能不能上線的往往是你有沒(méi)有把邊界條件想清楚。等你在真實(shí)項(xiàng)目里把這套邏輯跑通一次再回去搶紅包體驗(yàn)會(huì)完全不一樣。