秒殺系統

語言: CN / TW / HK

簡介

秒殺能夠以極小的經費撬動巨大的流量,雖然會帶來一定的口碑損失,但因為極具價效比,所以經常被運營同學使用。本文介紹如何設計一款能夠支撐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

  1. 每場活動配置:需要配置每場秒殺活動的開始時間和結束時間,以及參加秒殺的商品資訊。還有一些特殊需求,如只有使用者分享後才能參與秒殺等。

2. 對於活動配置,需要有編輯、推送、校驗、測試功能

  • 校驗功能:主要用於檢視推送出去的資料是否和配置的資料一致,主要用來檢查系統正確性、運營操作正確性

  • 測試功能:主要用於白名單測試,使測試人員可以在活動頁面真正的演練秒殺過程,同時又不影響正常使用者。因為一次秒殺活動可能有多個場次(如每天一場秒殺,每場秒殺兩個商品,持續7天),為了讓測試同學方便配置,只需要設定好第一場的時間,根據每場活動時間間隔,其他場次的秒殺會自動配置好,商品數量只需設定一次,所有場次商品數量都為該值。

3. 監控後臺,必然需要監控線上情況,但是對於測試情況也需要進行監控,主要為了便利測試人員檢視。監控一般關注於:中獎使用者、秒殺賣出數量是否和配置數量一致、參與使用者數、QPS峰值

最重要的當然是中獎使用者數量、秒殺賣出數量與活動配置數量是否一致,如果不一致,那肯定是出問題了,後面面臨修資料、補資料。

  1. 黑名單管理:有些地區的使用者,十分喜歡用指令碼刷,對於這些使用者,一部分通過程式自動抓取,一部分分析得出後,使用該管理平臺,手動新增。

秒殺系統的後臺給大家講完了,下面我們進入大家感興趣的高併發處理環節。

高併發系統設計

獲取秒殺活動資訊

獲取秒殺活動資訊,相對比較簡單,核心是通過goroutine,設定計時器,每過一段時間從Redis拉取資料,同步到本地快取,這樣能大大減小Redis的壓力。

目前該介面,在8+32的機器上,qps能支援到3~4W左右,其實仍然有一定的提升空間。

  1. 可以將部分不變的資料放到CDN,庫存、當前場次等動態變化的資訊提供新介面,這樣可以進一步減少後端冗餘的邏輯和返回資料量,不過對前端要求會提高。
  2. 即使是在當前的邏輯中,有部分場次的活動,因為並不參與展現,所以可以不參與計算,同時也無需返回,一定程度上也能提高效能。

秒殺介面

秒殺介面為最核心的介面,需要保證指定數量的秒殺商品不超賣,也不少賣。這個介面決定了秒殺系統的最終準確性。本來這個介面也做了流程圖,不過一是裡面有些內容涉及到隱私,另一方面如果給出流程圖,可能大家的設計就都一樣,少了很多其他的可能性。所以這裡只闡述核心點:

  1. 使用兩級限流措施,第一級為隨機限流,第二級為令牌桶,限流的比例根據預估流量和商品數量來限制,儘量確保1s內所有商品售賣完成。例如,10個商品,60W請求,如果隨機限流設定為千分之二,意味1s只有1200個請求能真正走到邏輯層,邏輯層壓力會小很多。而令牌桶能夠防止邏輯層過載。
  2. 黑客是需要重點考慮的物件
    • 如果提前請求則標記為黑客,進行記錄
    • 如果1s內同一個使用者多個請求到達邏輯層,標記為黑客,進行記
  3. 對於走到邏輯層的請求,需要做眾多判斷,確保系統準確性
    • 該使用者或者IP不在黑客列表裡
    • 該使用者本次活動中沒有秒殺成功過
    • 同一個使用者不能獲得兩次秒殺成功的機會
    • 是否仍然有足夠的庫存
    • 幫使用者按照秒殺價新增到購物車

本系統使用Redis來管理庫存,雖然使用兩級限流後,Redis負載不大,但是仍然有出錯的可能性。

在庫存管理上,通過一切檢查後,如果符合規定,會先扣減庫存。這樣保證了不會超賣。

有一種情況,如先扣減庫存,新增購物車失敗,但是歸還庫存失敗,這樣會導致少賣。對於這種情況,目前做法為記錄日誌,活動結束後,如果資料不對,根據日誌進行補發。

對於這種情況的優化,我能想到的辦法有錯誤重試、錯誤寫入佇列後非同步處理、分析日誌自動處理錯誤。這幾種方案,在某些極端情況下,仍然會失效,如果大家有更好的方案可以提供一下。

不過因為日誌的存在,讓我們有了保底的方案,而且如果在如此小流量下,Redis都無法穩定的話,可能問題就不僅僅是這一個服務了。

統計

對於秒殺成功使用者的統計,比較容易完成,秒殺成功後寫入Redis即可。

但是對於秒殺流量的統計,就無法使用這種方案了,畢竟60W的流量,Redis可能也撐不住。

這裡介紹一個比較巧的方案。

  1. 每次請求秒殺介面時,使用golang的原子操作,將統計變數statNow的值加1

  2. 起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邊緣計算。當然,如果有錢,不必這麼扣扣索索的,堆機器也是可以的。

最後

大家如果喜歡我的文章,可以關注我的公眾號(程式設計師麻辣燙)

我的個人部落格

往期文章回顧:

技術

  1. 秒殺系統
  2. 分散式系統與一致性協議
  3. 微服務之服務框架和註冊中心
  4. Beego框架使用
  5. 淺談微服務
  6. TCP效能優化
  7. 限流實現1
  8. Redis實現分散式鎖
  9. Golang原始碼BUG追查
  10. 事務原子性、一致性、永續性的實現原理
  11. CDN請求過程詳解
  12. 常用快取技巧
  13. 如何高效對接第三方支付
  14. Gin框架簡潔版
  15. InnoDB鎖與事務簡析
  16. 演算法總結

讀書筆記

  1. 敏捷革命
  2. 如何鍛鍊自己的記憶力
  3. 簡單的邏輯學-讀後感
  4. 熱風-讀後感
  5. 論語-讀後感
  6. 孫子兵法-讀後感

思考

  1. 對專案管理的一些看法
  2. 對產品經理的一些思考
  3. 關於程式設計師職業發展的思考
  4. 關於程式碼review的思考
  5. Markdown編輯器推薦-typora