簡介
秒殺能夠以極小的經費撬動巨大的流量,雖然會帶來一定的口碑損失,但因為極具價效比,所以經常被運營同學使用。本文介紹如何設計一款能夠支撐60W QPS的秒殺系統,希望能夠幫助到大家。
這套系統有著漫長的演變歷史,從最初利用Nginx、PHP,到後來使用GO,團隊慢慢的將系統做的更加穩定。唯一不好的地方是,當年我寫的後臺還在使用(寫前端程式碼能力有限),運營配置體驗上有些瑕疵,後期需要優化一版。
目前14臺8+32的機器,可以支撐60W QPS,理論上還能支援的更高,不過單ELB的上限是60W,即使流量再高,在ELB層也會溢位了。
一般大家聽到秒殺系統,最可能想到的是高併發,但高併發只是其中的一部分,需要其他的元件一起配合,才算是一個完整的秒殺系統。
本文從這幾個方面來講述該系統
- 後臺
- 高併發系統設計
- 獲取活動資訊
- 秒殺
- 統計
不過在講述之前,我們先看一下應用場景,讓大家對秒殺有一個直觀的瞭解。
在活動頁面上會有搶購模組,會展示搶購的時間、商品圖片、商品名稱、秒殺價、商品價等資訊。活動開始之前按鈕為Coming soon。
當活動時間到了,按鈕會變為Buy now,瞬間伺服器壓力飆升。點選Buy now時,如果秒殺成功,會跳轉到購物車頁,這時候只需要按照正常流程支付即可。如果不成功,按鈕會變為Out of stock。當然,如果不點選Buy now按鈕,該按鈕文案不會變更,除非重新重新整理頁面。
秒殺活動完成後,會在頁面上展示秒殺成功使用者的id。之所以新增這個功能,是因為很多使用者投訴這是假秒殺,作為商家,做活動也不容易。
後臺
通過對場景的描述,可以分析出後臺需要配置的內容。http://www.processon.com/view/link/5fb0a1e75653bb657c335c60
- 每場活動配置:需要配置每場秒殺活動的開始時間和結束時間,以及參加秒殺的商品資訊。還有一些特殊需求,如只有使用者分享後才能參與秒殺等。
2. 對於活動配置,需要有編輯、推送、校驗、測試功能
-
校驗功能:主要用於檢視推送出去的資料是否和配置的資料一致,主要用來檢查系統正確性、運營操作正確性
-
測試功能:主要用於白名單測試,使測試人員可以在活動頁面真正的演練秒殺過程,同時又不影響正常使用者。因為一次秒殺活動可能有多個場次(如每天一場秒殺,每場秒殺兩個商品,持續7天),為了讓測試同學方便配置,只需要設定好第一場的時間,根據每場活動時間間隔,其他場次的秒殺會自動配置好,商品數量只需設定一次,所有場次商品數量都為該值。
3. 監控後臺,必然需要監控線上情況,但是對於測試情況也需要進行監控,主要為了便利測試人員檢視。監控一般關注於:中獎使用者、秒殺賣出數量是否和配置數量一致、參與使用者數、QPS峰值
最重要的當然是中獎使用者數量、秒殺賣出數量與活動配置數量是否一致,如果不一致,那肯定是出問題了,後面面臨修資料、補資料。
- 黑名單管理:有些地區的使用者,十分喜歡用指令碼刷,對於這些使用者,一部分通過程式自動抓取,一部分分析得出後,使用該管理平臺,手動新增。
秒殺系統的後臺給大家講完了,下面我們進入大家感興趣的高併發處理環節。
高併發系統設計
獲取秒殺活動資訊
獲取秒殺活動資訊,相對比較簡單,核心是通過goroutine,設定計時器,每過一段時間從Redis拉取資料,同步到本地快取,這樣能大大減小Redis的壓力。
目前該介面,在8+32的機器上,qps能支援到3~4W左右,其實仍然有一定的提升空間。
- 可以將部分不變的資料放到CDN,庫存、當前場次等動態變化的資訊提供新介面,這樣可以進一步減少後端冗餘的邏輯和返回資料量,不過對前端要求會提高。
- 即使是在當前的邏輯中,有部分場次的活動,因為並不參與展現,所以可以不參與計算,同時也無需返回,一定程度上也能提高效能。
秒殺介面
秒殺介面為最核心的介面,需要保證指定數量的秒殺商品不超賣,也不少賣。這個介面決定了秒殺系統的最終準確性。本來這個介面也做了流程圖,不過一是裡面有些內容涉及到隱私,另一方面如果給出流程圖,可能大家的設計就都一樣,少了很多其他的可能性。所以這裡只闡述核心點:
- 使用兩級限流措施,第一級為隨機限流,第二級為令牌桶,限流的比例根據預估流量和商品數量來限制,儘量確保1s內所有商品售賣完成。例如,10個商品,60W請求,如果隨機限流設定為千分之二,意味1s只有1200個請求能真正走到邏輯層,邏輯層壓力會小很多。而令牌桶能夠防止邏輯層過載。
- 黑客是需要重點考慮的物件
- 如果提前請求則標記為黑客,進行記錄
- 如果1s內同一個使用者多個請求到達邏輯層,標記為黑客,進行記
- 對於走到邏輯層的請求,需要做眾多判斷,確保系統準確性
- 該使用者或者IP不在黑客列表裡
- 該使用者本次活動中沒有秒殺成功過
- 同一個使用者不能獲得兩次秒殺成功的機會
- 是否仍然有足夠的庫存
- 幫使用者按照秒殺價新增到購物車
本系統使用Redis來管理庫存,雖然使用兩級限流後,Redis負載不大,但是仍然有出錯的可能性。
在庫存管理上,通過一切檢查後,如果符合規定,會先扣減庫存。這樣保證了不會超賣。
有一種情況,如先扣減庫存,新增購物車失敗,但是歸還庫存失敗,這樣會導致少賣。對於這種情況,目前做法為記錄日誌,活動結束後,如果資料不對,根據日誌進行補發。
對於這種情況的優化,我能想到的辦法有錯誤重試、錯誤寫入佇列後非同步處理、分析日誌自動處理錯誤。這幾種方案,在某些極端情況下,仍然會失效,如果大家有更好的方案可以提供一下。
不過因為日誌的存在,讓我們有了保底的方案,而且如果在如此小流量下,Redis都無法穩定的話,可能問題就不僅僅是這一個服務了。
統計
對於秒殺成功使用者的統計,比較容易完成,秒殺成功後寫入Redis即可。
但是對於秒殺流量的統計,就無法使用這種方案了,畢竟60W的流量,Redis可能也撐不住。
這裡介紹一個比較巧的方案。
-
每次請求秒殺介面時,使用golang的原子操作,將統計變數statNow的值加1
-
起goroutine,設定定時任務,計算當前統計總數與上次統計總數的差值,寫到Redis中
ticker := time.NewTicker(time.Millisecond * 100)
go func() {
for range ticker.C {
orig := atomic.LoadUint64(&statOrig)
now := atomic.LoadUint64(&statNow)
num := int64(now - orig)
if num > 0 {
//將增加的數值incr到Redis中
}
atomic.SwapUint64(&statOrig, statNow)
}
}()
複製程式碼
總結
Golang是門好語言,幫我們解決了眾多問題。單機使用Nginx,併發2W左右,不使用Nginx,直接用go,併發4W,在語言層面上直接解決了高併發問題。
使用兩級限流策略,保證伺服器壓力可控。
靈活運用Go提供的功能,Goroutine、定時器、本地儲存、原子操作、讀寫鎖等。
合理使用Redis,保證服務的準確性與穩定性。
雖然還有少許的待完善點,但並不影響使用。
如果後續壓力繼續增加,一個可行方案是CDN邊緣計算。當然,如果有錢,不必這麼扣扣索索的,堆機器也是可以的。
最後
大家如果喜歡我的文章,可以關注我的公眾號(程式設計師麻辣燙)
我的個人部落格
往期文章回顧:
技術
- 秒殺系統
- 分散式系統與一致性協議
- 微服務之服務框架和註冊中心
- Beego框架使用
- 淺談微服務
- TCP效能優化
- 限流實現1
- Redis實現分散式鎖
- Golang原始碼BUG追查
- 事務原子性、一致性、永續性的實現原理
- CDN請求過程詳解
- 常用快取技巧
- 如何高效對接第三方支付
- Gin框架簡潔版
- InnoDB鎖與事務簡析
- 演算法總結
讀書筆記
思考