一個漂泊江湖多年的 985 非科班程序員,曾混跡于國企、互聯(lián)網大廠和創(chuàng)業(yè)公司的后臺開發(fā)攻城獅。
1. 引言
當我那天拿著手機,正在和朋友們的微信群里暢聊著八卦新聞和即將到來的周末計劃時,忽然一條帶著喜意的消息撲面而來,消息正中間赫然寫著八個大字:恭喜發(fā)財,大吉大利。
圖片
搶紅包!!相信大部分人對此都不陌生,自 2015 年春節(jié)以來,微信就新增了各類型搶紅包功能,吸引了數(shù)以億萬級的用戶參與體驗,今天,我們就來聊一聊這個奇妙有趣的紅包系統(tǒng)。
2. 概要設計
2.1 系統(tǒng)特點
圖片
搶紅包系統(tǒng)從功能拆分,可以分為包紅包、發(fā)紅包、搶紅包和拆紅包 4 個功能。
對于系統(tǒng)特性來說,搶紅包系統(tǒng)和秒殺系統(tǒng)類似。
圖片
每次發(fā)紅包都是一次商品秒殺流程,包括商品準備,商品上架,查庫存、減庫存,以及秒殺開始,最終的用戶轉賬就是紅包到賬的過程。
2.2 難點
相比秒殺活動,微信發(fā)紅包系統(tǒng)的用戶量更大,設計更加復雜,需要重視的點更多,主要包括以下幾點。
1、高并發(fā)
海量并發(fā)請求,秒殺只有一次活動,但紅包可能同一時刻有幾十萬個秒殺活動。
比如 2017 雞年除夕,微信紅包搶紅包用戶數(shù)高達 3.42 億,收發(fā)峰值 76 萬/秒,發(fā)紅包 37.77 億 個。
2、安全性要求
紅包業(yè)務涉及資金交易,所以一定不能出現(xiàn)超賣、少賣的情況。
- 超賣:發(fā)了 10 塊錢,結果搶到了 11 塊錢,多的錢只能系統(tǒng)補上,如此為愛發(fā)電應用估計早就下架了;
- 少賣:發(fā)了 10 塊錢,只搶了 9 塊,多的錢得原封不動地退還用戶,否則第二天就接到法院傳單了。
3、嚴格事務
參與用戶越多,并發(fā) DB 請求越大,數(shù)據(jù)越容易出現(xiàn)事務問題,所以系統(tǒng)得做好事務一致性。
這也是一般秒殺活動的難點所在,而且搶紅包系統(tǒng)涉及金錢交易,所以事務級別要求更高,不能出現(xiàn)臟數(shù)據(jù)。
3. 概要設計
3.1 功能說明
搶紅包功能允許用戶在群聊中發(fā)送任意個數(shù)和金額的紅包,群成員可以搶到隨機金額的紅包,但要保證每個用戶的紅包金額不小于 0.01 元。
圖片
搶紅包的詳細交互流程如下:
- 用戶接收到搶紅包通知,點擊通知打開群聊頁面;
- 用戶點擊搶紅包,后臺服務驗證用戶資格,確保用戶尚未領取過此紅包;
- 若用戶資格驗證通過,后臺服務分配紅包金額并存儲領取記錄;
- 用戶在微信群中看到領取金額,紅包狀態(tài)更新為“已領取”;
- 異步調用支付接口,將紅包金額更新到錢包里。
3.2 數(shù)據(jù)庫設計
紅包表 redpack 的字段如下:
- id: 主鍵,紅包ID
- userId: 發(fā)紅包的用戶ID
- totalAmount: 總金額
- surplusAmount: 剩余金額
- total: 紅包總數(shù)
- surplusTotal: 剩余紅包總數(shù)
該表用來記錄用戶發(fā)了多少紅包,以及需要維護的剩余金額。
紅包記錄表 redpack_record 如下:
- id: 主鍵,記錄ID
- redpackId: 紅包ID,外鍵
- userId: 用戶ID
- amount: 搶到的金額
記錄表用來存放用戶具體搶到的紅包信息,也是紅包表的副表。
3.3 發(fā)紅包
- 用戶設置紅包的總金額和個數(shù)后,在紅包表中增加一條數(shù)據(jù),開始發(fā)紅包;
- 為了保證實時性和搶紅包的效率,在 redis 中增加一條記錄,存儲紅包 ID 和總人數(shù) n;
- 搶紅包消息推送給所有群成員。
3.4 搶紅包
從 2015 年起,微信紅包的搶紅包和拆紅包就分離了,用戶點擊搶紅包后需要進行兩次操作。
這也是為什么明明有時候搶到了紅包,點開后卻發(fā)現(xiàn)該紅包已經被領取完了。
圖片
搶紅包的交互步驟如下:
- 搶紅包:搶操作在 Redis 緩存層完成,通過原子遞減的操作來更新紅包個數(shù),個數(shù)遞減為 0 后就說明搶光了。
- 拆紅包:拆紅包時,首先會實時計算金額,一般是通過二倍均值法實現(xiàn)(即 0.01 到剩余平均值的 2 倍之間)。
- 紅包記錄:用戶獲取紅包金額后,通過數(shù)據(jù)庫的事務操作累加已經領取的個數(shù)和金額,并更新紅包表和記錄表。
- 轉賬:為了提升效率,最終的轉賬為異步操作,這也是為什么在春節(jié)期間,紅包領取后不能立即在余額中看到的原因。
上述流程,在一般的秒殺活動中隨處可見,但是,紅包系統(tǒng)真的有這么簡單嗎?
當用戶量過大時,高并發(fā)下的事務一致性怎么保證,數(shù)據(jù)分流如何處理,紅包的數(shù)額分配又是怎么做的,接下來我們一一探討。
4. 詳細設計
由于是秒殺類設計,以及 money 分發(fā),所以我們重點關注搶紅包時的高并發(fā)解決方案和紅包分配算法。
4.1 高并發(fā)解決方案
首先,搶紅包系統(tǒng)的用戶量很大,如果幾千萬甚至億萬用戶同時在線發(fā)搶紅包,請求直接打到數(shù)據(jù)庫,必然會導致后端服務過載甚至崩潰。
而在這種業(yè)務量下,簡單地對數(shù)據(jù)庫進行擴容不僅會讓成本消耗劇增,另一方面由于存在磁盤的性能瓶頸,所以大概率解決不了問題。
所以,我們將解決方案集中在 減輕系統(tǒng)壓力、提升響應速度 上,接下來會從緩存、加鎖、異步分治等方案來探討可行性。
1、緩存
和大多數(shù)秒殺系統(tǒng)設計相似,由于搶紅包時并發(fā)很高,如果直接操作 DB 里的數(shù)據(jù)表,可能觸發(fā) DB 鎖的邏輯,導致響應不及時。
圖片
所以,我們可以在 DB 落盤之前加一層緩存,先限制住流量,再處理紅包訂單的數(shù)據(jù)更新。
這樣做的優(yōu)點是用緩存操作替代了磁盤操作,提升了并發(fā)性能,這在一般的小型秒殺活動中非常有效!
但是,隨著微信使用發(fā)&搶紅包的用戶量增多,系統(tǒng)壓力增大,各種連鎖反應產生后,數(shù)據(jù)一致性的問題逐漸暴露出來:
- 假設庫存減少的內存操作成功,但是 DB 持久化失敗了,會出現(xiàn)紅包少發(fā)的問題;
- 如果庫存操作失敗,DB 持久化成功,又可能會出現(xiàn)紅包超發(fā)的問題。
而且在幾十萬的并發(fā)下,直接對業(yè)務加鎖也是不現(xiàn)實的,即便是樂觀鎖。
2、加鎖
在關系型 DB 里,有兩種并發(fā)控制方法:分為樂觀鎖(又叫樂觀并發(fā)控制,Optimistic Concurrency Control,縮寫 “OCC”)和悲觀鎖(又叫悲觀并發(fā),Pessimistic Concurrency Control,縮寫“PCC”)。
圖片
悲觀鎖在操作數(shù)據(jù)時比較悲觀,認為別的事務可能會同時修改數(shù)據(jù),所以每次操作數(shù)據(jù)時會先把數(shù)據(jù)鎖住,直到操作完成。
樂觀鎖正好相反,這種策略主打一個“信任”的思想,認為事務之間的數(shù)據(jù)競爭很小,所以在操作數(shù)據(jù)時不會加鎖,直到所有操作都完成到提交時才去檢查是否有事務更新(通常是通過版本號來判斷),如果沒有則提交,否則進行回滾。
在高并發(fā)場景下,由于數(shù)據(jù)操作的請求很多,所以樂觀鎖的吞吐量更大一些。但是從業(yè)務來看,可能會帶來一些額外的問題:
- 搶紅包時大量用戶涌入,但只有一個可以成功,其它的都會失敗并給用戶報錯,導致用戶體驗極差;
- 搶紅包時,如果第一時間有很多用戶涌入,都失敗回滾了。過一段時間并發(fā)減小后,反而讓手慢的用戶搶到了紅包;
- 大量無效的更新請求和事務回滾,可能給 DB 造成額外的壓力,拖慢處理性能。
總的來說,樂觀鎖適用于數(shù)據(jù)競爭小,沖突較少的業(yè)務場景,而悲觀鎖也不適用于高并發(fā)場景的數(shù)據(jù)更新。
因此對于搶紅包系統(tǒng)來說,加鎖是非常不適合的。
3、異步分治
綜上所述,搶紅包時不僅要解決高并發(fā)問題、還得保障并發(fā)的順序性,所以我們考慮從隊列的角度來設計。
我們知道,每次包紅包、發(fā)紅包、搶紅包時,也有先后依賴關系,因此我們可以將紅包 ID 作為一個唯一 Key,將發(fā)一次紅包看作一個單獨的 set,各個 set 相互獨立處理。
圖片
這樣,我們就把海量的搶紅包系統(tǒng)分成一個個的小型秒殺系統(tǒng),在調度處理中,通過對紅包 ID 哈希取模,將一個個請求打到多臺服務器上解耦處理。
然后,為了保證每個用戶搶紅包的先后順序,我們把一個紅包相關的操作串行起來,放到一個隊列里面,依次消費。
從上述 set 分流我們可以看出,一臺服務器可能會同時處理多個紅包的操作,所以,為了保證消費者處理 DB 不被高并發(fā)打崩,我們還需要在消費隊列時用緩存來限制并發(fā)消費數(shù)量。
搶紅包業(yè)務消費時由于不存儲數(shù)據(jù),只是用緩存來控制并發(fā)。所以我們可以選用大數(shù)據(jù)量下性能更好的 Memcached。
除此之外,在數(shù)據(jù)存儲上,我們可以用紅包 ID 進行哈希分表,用時間維度對 DB 進行冷熱分離,以此來提升單 set 的處理性能。
綜上所述,搶紅包系統(tǒng)在解決高并發(fā)問題上采用了 set 分治、串行化隊列、雙維度分庫分表 等方案,使得單組 DB 的并發(fā)性能得到了有效提升,在應對數(shù)億級用戶請求時取得了良好的效果。
4.2 紅包分配算法
搶紅包后,我們需要進行拆紅包,接下來我們討論一下紅包系統(tǒng)的紅包分配算法。
紅包金額分配時,由于是隨機分配,所以有兩種實現(xiàn)方案:實時拆分和預先生成。
1、實時拆分
實時拆分,指的是在搶紅包時實時計算每個紅包的金額,以實現(xiàn)紅包的拆分過程。
這個對系統(tǒng)性能和拆分算法要求較高,例如拆分過程要一直保證后續(xù)待拆分紅包的金額不能為空,不容易做到拆分的紅包金額服從正態(tài)分布規(guī)律。
2、預先生成
預先生成,指的是在紅包開搶之前已經完成了紅包的金額拆分,搶紅包時只是依次取出拆分好的紅包金額。
這種方式對拆分算法要求較低,可以拆分出隨機性很好的紅包金額,但通常需要結合隊列使用。
3、二倍均值法
綜合上述優(yōu)缺點考慮,以及微信群聊中的人數(shù)不多(目前最高 500 人),所以我們采用實時拆分的方式,用二倍均值法來生成隨機紅包,只滿足隨機即可,不需要正態(tài)分布。
故可能出現(xiàn)很大的紅包差額,但這更刺激不是嗎