外貿領航
首頁外貿學堂 > 如何設計一個秒殺系統「秒殺流程設計」

如何設計一個秒殺系統「秒殺流程設計」

來源:互聯網 2024-07-17 14:04:03
什么是秒殺

百度百科對秒殺這個詞的解釋有多個,第一種是:

在某些領域以壓倒性的優勢超越其他人,或者是在極短時間(比如一秒鐘)內解決對手,該種語言通常使用在網絡游戲中。

還有一種解釋語義用在網購場景中,通常是指:

網絡商家一個非常優惠,極具吸引力的價格發布一款商品,并限定在一段非常短的時間內開放給消費者購買。由于價格十分實惠,往往會吸引很多消費者爭相購買,商品會在很短時間內被一搶而空,有時甚至在一秒鐘之內商品就被搶完了。因此將這種電商的限時低價搶購活動形象地稱為秒殺。

當然,我們今天的主題肯定是第二個了。下面就先來看看網購秒殺的場景有哪些特點。

秒殺場景的特點

生活中最常見的秒殺場景有雙十一的電商促銷活動,節假日12306的搶票場景等。這些場景的特點就是:

瞬間系統的并發請求特別高;商品數量有限,往往供不應求。

因此我們在針對秒殺場景設計系統時就要充分考慮以下問題:

系統怎么扛住高并發的請求;怎么防止商品超賣; ---不考慮秒殺的話,就是一個普通的下單流程,樂觀鎖扣減庫存惡意軟件刷單秒殺; ---針對IP過濾限流?秒殺的接口需要到指定時間才放開,到指定時間失效; --- 后臺判定訂單長時間沒有支付,應該及時釋放該商品,補充到庫存中。 ---死信隊列秒殺業務并發量大,是否會對其業務造成影響; ---單獨部署秒殺系統?惡意DDos攻擊; ---高防IP怎么防止用戶不同重復點擊秒殺按鈕;怎么保證先來的流量先搶到商品;從系統架構的角度優化

在秒殺進行的瞬間,會有大量的請求涌進系統。如果系統不具備高并發能力的話,那么系統會立馬進入癱瘓狀態。這對于大公司是不能接受的,因為不僅會影響業務的正常開展,還會給用戶留下這個公司技術能力差的映像。所以讓秒殺系統具備高并發能力是我們在設計系統時首先需要考慮的問題。

讓系統具備高并發的能力是一個很大的話題,這邊限于篇幅,不會深入展開每個細節點。

負載均衡提高系統水平擴展能力

高并發的系統架構都會采用分布式集群部署,服務上層有著層層負載均衡,并提供各種容災手段 (雙活機房、節點容錯、服務器災備等)來保證系統的高可用,流量也會根據不同的負載能力和配置策略均衡到不同的服務器上。下邊是一個簡單的負載均衡示意圖:

上面的三層負載均衡的架構,能大大提升了系統的水平擴展能力。可以根據秒殺的并發量來靈活地調整機器的數量。如果預估請求量比較大的話,可以同步往上加機器。

LVS和Nginx都是和負載均衡相關的技術,一個是四層負載,一個是七層負載。LVS的吞吐量比Nginx要高很多,可以達到幾十萬,Nginx的吞吐量也相對較高,可以達到幾萬的量級。關于兩者更深入的知識,大家可以自己學習。

接口限流減少不必要的流量

秒殺的商品庫存只有100件,但是一秒鐘內可能會有10000個,甚至更多的用戶來搶購這100件商品。很顯然其中的大多數是搶不到的,那么就很有必要做一下限制——一定時間內不要讓這么多用戶進來搶,比如說一秒內我只放行5000個用戶進來秒殺。這就是限流措施,在秒殺系統中引進限流措施可以大大較少系統資源的浪費,減少無意義的爭搶。

限流可以分為前端限流后端限流。

前端限流的措施有:

首先第一步就是通過前端限流,用戶在秒殺按鈕點擊以后發起請求,那么在接下來的5秒是無法點擊(通過設置按鈕為disable)。這一小舉措開發起來成本很小,但是很有效。

后端限流:

每個用戶一定時間內只能秒殺一次:具體多少秒需要根據實際業務和秒殺的人數而定,一般限定為10秒。具體的做法就是通過redis的鍵過期策略,首先對每個請求都從String value = Redis.get(userId);如果獲取到這個value為空或者為null,表示它是有效的請求,然后放行這個請求。如果不為空表示它是重復性請求,直接丟掉這個請求。如果有效,采用redis.setexpire(userId,value,10).value可以是任意值,一般放業務屬性比較好,這個是設置以userId為key,10秒的過期時間(10秒后,key對應的值自動為null)。限流算法限流:到這步已經將很多無效的爭搶流量限制住了,但是極限并發下還是會有很多請求進來。這時我們就可以使用限流算法來進一步限制流量,比如1秒內最多放行1000個請求。

比較成熟的限流算法有:

令牌桶算法漏桶算法

當然,已經有很多的限流算法實現了。比如Guava、Nignx和Spring等,都可以讓我們實現限流措施。大家可以自己去查詢使用。(后面會寫專門的文章來介紹限流的實現)

從秒殺流程的角度優化

從上面的介紹我們知道用戶秒殺流量通過層層的負載均衡,均勻到了不同的服務器上,但即使如此,集群中的單機所承受的 QPS 也是非常高的,因此我們還需要盡可能地優化單機的性能。

其實如果我們拋開秒殺場景的大并發特點的話,秒殺就是一個普通的電商下單流程。一個普通的電商下單流程包括以下幾步:

查詢庫存,庫存不足的話就不能購買;生成訂單并扣減庫存(生成訂單和庫存扣減的順序有講究,下面會討論);用戶支付;長時間沒支付的訂單處理成失效,并釋放庫存。

為了將整個流程說清楚,這邊簡單建兩個表:商品表和訂單表,用簡單的代碼描述下大體的過程。

-- 商品表CREATE TABLE `stock` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`name` varchar(50) NOT NULL DEFAULT '' COMMENT '商品名稱',`count` int(11) NOT NULL COMMENT '庫存',`sale` int(11) NOT NULL COMMENT '已售',`version` int(11) NOT NULL COMMENT '樂觀鎖,版本號',PRIMARY KEY(`id`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;-- 訂單表CREATE TABLE `stock_order`(`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`sid` int(11) NOT NULL COMMENT '庫存ID',`name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名稱',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創建時間',PRIMARY KEY(`id`)) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8;庫存扣減的優化

1. 樂觀鎖更新庫存

在高并發系統中存在一個普遍的問題就是多個請求并發修改一條記錄。在秒殺系統中也存在這種情況,就是在扣減庫存的時候。一個請求首先去查商品現在的庫存,發現庫存還充足,準備去扣減相應的庫存并生成訂單。但是這個時候其他請求可能已經在這個請求之前扣減了庫存,導致庫存已經不足了。這個時候如果再繼續去扣減的話就會導致“超賣”現象。

下面是有問題的代碼邏輯

上面出問題的地方就是更新扣減庫存時沒有檢查當前的庫存是否被人更新過了。解決的辦法也非常簡單,就是在扣減更新庫存時加一個樂觀鎖更新。樂觀鎖我們通常一個時間戳版本號實現,所以將更新庫存的sql改成如下,就可以解決超賣的問題。

update stock set sales = sales 1, version = version 1 where id = # and version = version

其實這步還有優化的空間:將查詢庫存和扣減庫存合成一步,可以用這樣的做法:

update stock set sales = sales 1, version = version 1 where id ={id} and version = #{version} and count - sale > 0;

這樣的話,就可以保證庫存不會超賣并且一次更新庫存。當update語句返回更新的數據條數是1的時候就認為還有庫存。

2. 將庫存信息放入Redis緩存

在上面的下單流程中,我們可以發現每次都要去數據庫去查下庫存信息。這在大并發情況下對數據庫的壓力是相當大的。所以我們可以將庫存信息放入Redis分布式緩存,減少數據庫的壓力。更新庫存時同時將緩存中的庫存數據也更新。(這邊有個問題,當并發級別真的非常高時,Redis會不會成為查詢的瓶頸?)

3. 釋放長時間沒有支付的訂單

上面的流程中,一旦訂單創建成功就會占據一個庫存。如果這個訂單一直沒支付的話就會導致其他想買的用戶不能買到商品。所以我們要想辦法釋放這種長期沒有支付的訂單,將庫存釋放到總庫存中去。

方案一:寫一個定時job, 每分鐘掃描一下數據庫的訂單表,如果訂單超過了15分鐘,那么訂單狀態改為失效,并且商品表數量要加1,因為剛剛刪除的訂單釋放了一個商品。但是這樣會給數據庫造成很大的壓力,而且如果長時間都沒有過期的訂單,而job依然會每分鐘跑一次,浪費資源。(問題:刪除訂單時如果用戶同時支付了怎么辦?加樂觀鎖?還有什么需要注意的?)

方案二:使用延遲隊列處理,創建訂單的時候同步向延遲隊列中發送相關的訂單信息。然后消費者在指定的延遲時間后取出訂單ID,去查詢訂單是否已經支付,如果沒有支付則設置成失效。

這邊只是簡單介紹下方案,后面會寫詳細的文章進行分析。

4. 為什么要先扣庫存再創建訂單

我們上面設計的流程是:扣減庫存 --> 創建訂單 --> 支付。有沒小伙伴想過為什么這個流程會比較好。能不能是創建訂單-->扣減庫存-->支付;或者是創建訂單-->支付-->扣減庫存呢?

先說創建訂單-->扣減庫存-->支付這個順序,這種流程存在的一個比較大的問題就是:一個用戶會創建很多訂單,但是他只需要買一個,所以會占據其他用戶的購買名額。

再說創建訂單-->支付-->扣減庫存這種順序,這個流程存在的問題就是:用戶創建了訂單并支付成功了,但是因為存在高并發的情況,其他用戶可能在這個用戶支付的過程中已經提前支付買走了最后的商品,這就會導致“超賣”的現象——錢付了,貨沒了,尷尬。

異步創建訂單的優化

如果還要繼續提升系統性能的話,可以考慮將最后一步的創建訂單從同步轉為異步。通過引入消息隊列,將訂單的信息發到消息隊列,消費者負責消費信息并創建訂單。因為異步了,所以最終需要采取回調或者是其他提醒的方式提醒用戶購買完成。當然也可以輪詢訂單的創建情況,主動完成支付。

秒殺頁面靜態化

用戶在秒殺開始前,一般會通過不停刷新瀏覽器頁面以保證不會錯過秒殺,這些請求如果按照一般的網站應用架構,訪問應用服務器、連接數據庫,會對應用服務器和數據庫服務器造成負載壓力。

重新設計秒殺商品頁面,不使用網站原來的商品詳細頁面,將商品的描述、參數、成交記錄、圖像、評價等全部寫入到一個靜態頁面,用戶請求不需要通過訪問后端服務器。

具體的方法可以使用freemarker模板技術,建立網頁模板,填充數據,然后渲染網頁。

秒殺靜態頁面CDN部署

秒殺的瞬間,傳遞商品靜態頁面需要的貸款可能會超過平時服務器的貸款,所以可以考慮將 商品靜態頁面部署到CDN來節省貸款。

秒殺系統優化思路總結盡量將請求攔截在上游。還可以根據 UID 進行限流。最大程度地減少請求落到 DB。多利用緩存。同步操作異步化。fail fast,盡早失敗,保護應用。

其實不止秒殺系統,個人覺得系統優化都可以參考這幾個維度。這些方面都進行優化過了,可以再考慮其他方面的優化。

秒殺系統的開源代碼

Spring-Boot相關的開源秒殺框架,Github 上自己搜索。

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如有侵權行為,請第一時間聯系我們修改或刪除,多謝。

CopyRight ? 外貿領航 2023 All Rights Reserved.