51CTO.com原創稿件】說到“秒殺”,恐怕大多數人想到的就是“雙 11”,“促銷”,“買買買”等火爆的場面吧。
圖片來自 Pexels
大家為了打折商品蜂擁而至,造成電商網站一片繁華的景象。但作為程序員的我們,看到的卻是背后的高并發和可靠性。無論你處在軟件開發的哪個階段,都希望能夠設計一套屬于自己的秒殺系統。
今天我們一起來看看,一套秒殺系統在架構設計上需要有哪些考量:
秒殺場景的特點
秒殺場景是電商網站定期舉辦的活動,這個活動有明確的開始和結束時間,而且參與互動的商品是事先定義好了,參與秒殺商品的個數也是有限制的。同時會提供一個秒殺的入口,讓用戶通過這個入口進行搶購。
總結一下秒殺場景的特點:
系統隔離的設計思路
在分析秒殺的特點后,我們發現秒殺活動是有計劃的,并且在短時間內會爆發大量的請求。為了不影響現有的業務系統的正常運行,我們需要把它和現有的系統做隔離。
即使秒殺活動出現問題也不會影響現有的系統。隔離的設計思路可以從三個維度來思考。
業務隔離
既然秒殺是一場活動,那它一定和常規的業務不同,我們可以把它當成一個單獨的項目來看。在活動開始之前,最好設計一個“熱場”。
“熱場”的形式多種多樣,例如:分享活動領優惠券,領秒殺名額等等。“熱場”的形式不重要,重要的是通過它獲取一些準備信息。
例如:有可能參與的用戶數,他們的地域分布,他們感興趣的商品。為后面的技術架構提供數據支持。
技術隔離
技術隔離架構圖
前面有了準備工作,那么從技術上需要有以下幾個方面的考慮:
所以,在這一層我們要考慮的是如何做好限流,當超過系統承受范圍的時候,需要果斷阻止請求的涌入。
需要使用服務集群和水平擴展,讓“高峰”請求分流到不同的服務器進行處理。同時,還會利用緩存和隊列技術減輕應用處理的壓力,通過異步請求的方式做到最終一致性。
由于是多線程操作,而且商品的額度有限,為了解決超賣的問題,需要考慮進程鎖的問題。
數據庫隔離
秒殺活動持續時間短,瞬時數據量大。為了不影響現有數據庫的正常業務,可以建立新的庫或者表來處理。
在秒殺結束以后,需要把這部分數據同步到主業務系統中,或者查詢表中。如果數據量特別巨大,到千萬級別甚至上億,建議使用分表或者分庫。
客戶端設計
上面提到的三個隔離維度中,我們對技術維度是最為關心的。如果說瀏覽器/客戶端是用戶接觸“秒殺系統”的入口,那么在這一層提供緩存數據就是非常必要的。
在設計之初,我們會為秒殺的商品生成專門的商品頁面和訂單頁面。這些頁面以靜態的 HTML 為主,包括的動態信息盡量少。
從業務的角度來說,這些商品的信息早就被用戶熟識了,在秒殺的時候,他們關心的是如何快速下單。
既然商品的詳情頁面和訂單頁面都是靜態生成的,那么就需要定義一個 URL,當要開始秒殺之前,開放這個 URL 給用戶訪問。
為了防止“程序員或者內部人員”作弊,這里的地址可以通過時間戳和 Hash 算法來生成,也就是說這個地址只有系統知道,到了快秒殺之前才由系統發放出去。
有人說瀏覽器/客戶端如果存放的都是靜態頁面,那么“控制開始下單”的按鈕,以及發送“下單請求”的按鈕,也是靜態的嗎?
答案是否定的,其實靜態頁面是方便客戶端好緩存,下單的動作以及下單時間的控制還是在服務器端。
只不過是通過 JS 文件的方式發送給客戶端,在快要秒殺之前,會把這部分 JS 下載到客戶端。
因為,其業務邏輯很少,基本只包括時間,用戶信息,商品信息等等。所以,其對網絡的要求不高。
同時,在網絡設計上,我們也會將 JS 和 HTML 同時緩存在 CDN 上面,讓用戶從離自己最近的 CDN 服務器上獲取這些信息。
為了避免秒殺程序參與秒殺,在客戶端也會設計一些問答或者滑塊的功能,減少此類機器人對服務器的壓力。
秒殺系統前端設計簡圖
代理層設計
說完了秒殺系統的前端設計,請求自然地來到了代理層。由于用戶的請求量大,我們需要用負載均衡加上服務器集群,來面對如此空前的壓力。
代理層三大功能簡圖
在這一層是可以做緩存,過濾和限流的:
主要想表達的意思是,可以將一些變化不頻繁的數據,提到代理層來緩存,提高響應的效率。
同時,還可以根據風控系統返回的信息,過濾一些疑似機器人或者惡意請求。例如:從固定 IP 過來的,頻率過高的請求。最重要的就是在這一層,可以識別來自秒殺系統的請求。
如果是帶有秒殺系統的參數,就要把請求路由到秒殺系統的服務器集群。這樣才能和正常的業務系統分割開來。
閥值的設置可以是動態調整的。例如:集群服務器中有 10 個服務器,其中一臺由于壓力過大掛掉了。
此時就需要調整代理層的流量閥值,將能夠處理的請求流量減少,保護后端的應用服務器。
當服務器恢復以后,又可以將閥值調回原位。可以通過 Nginx+Lua 合作完成,Lua 從服務注冊中心讀取服務健康狀態,動態調整流量。
應用層設計
“秒殺系統”秒殺的是什么?無非是商品。對于系統來說就是商品的庫存,購買的商品一旦超過了庫存就不能再賣了。
防止超賣
超過了庫存還可以賣給用戶,這就是“超賣”,也是系統設計需要避免的。為了承受大流量的訪問,我們用了水平擴展的服務,但是對于他們消費的資源“庫存”來說,卻只有一個。
為了提高效率,會將這個庫存信息放到緩存中。以流行的 Redis 為例,用它存放庫存信息,由多個線程來訪問就會出現資源爭奪的情況。也就是分布式程序爭奪唯一資源,為了解決這個問題我們需要實現分布式鎖。
假設這里有多個應用響應用戶的訂單請求,他們同時會去訪問 Redis 中存放的庫存信息,每接受用戶一次請求,都會從 Redis 的庫存中減去 1 個商品庫存量。
當任何一個進程訪問 Redis 中的庫存資源時,其他進程是不能訪問的,所以這里需要考慮鎖的情況(樂觀,悲觀)。
Redis 緩存承載庫存變量
如果鎖長期沒有釋放,需要考慮鎖的過期時間,需要設置兩個超時時間:
訂單處理流程
這里的“扣減服務”完成了最簡單的扣減庫存工作,并沒有和其他項目服務打交道,更沒有訪問數據庫。
訂單流程示意圖
后面的流程相對比較復雜,我們先看圖,根據圖示來講解:
注意,這里可以加入類似 ZooKeeper 這樣的服務調度來幫助,協調服務調度和任務分配。
一旦發現不一致,會去做重試操作。如果重試依舊不成功,會重寫信息到緩存,讓用戶知道失敗原因。
雖然,這個信息和最終結果有偏差,但是在秒殺的場景,要求高性能是前提,結果的一致性,可以后期補償。
數據庫設計
講完了秒殺的處理流程,來談談數據庫設計要注意的點。
數據估算
前面說了秒殺場景需要注意隔離,這里的隔離包括“業務隔離”。就是說我們在秒殺之前,需要通過業務的手段,例如:熱場活動,問卷調查,歷史數據分析。通過他們去估算這次秒殺可能需要存儲的數據量。
這里有兩部分的數據需要考慮:
前者不言而喻是給業務系統用的。后者,是用來分析和后續處理問題訂單用的,秒殺完畢以后還可以用來復盤。
分表分庫
對于這些數據的存放,需要分情況討論,例如,MySQL 單表推薦的存儲量是 500W 條記錄(經驗數字)。
如果估算的時候超過了這個數據,建議做分表。如果服務的連接數較多,建議進行分庫的操作。
數據隔離
由于大量的數據操作是插入,有少部分的修改操作。如果使用關系型數據來存儲,建議用專門的表來存放,不建議使用業務系統正在使用的表。
這個開頭提到了,數據隔離是必須的,一旦秒殺系統掛了,不會影響到正常業務系統,這個風險意識要有。表的設計除了 ID 以外,最好不要設置其他主鍵,保證能夠快速地插入。
數據合并
由于是用的專用表存儲,在秒殺活動完畢以后,需要將其和現有的數據做合并。其實,交易已經完成,合并的目的也就是查詢。
這個合并需要根據具體情況來分析,如果對于那些“只讀”的數據,對于做了讀寫分離的公司,可以導入到專門負責讀的數據庫或者 NoSQL 數據庫中。
壓力測試
構建了秒殺系統,一定會面臨上線,那么在上線之前壓力測試是必不可少的。
我們做壓力測試的目的是檢驗系統崩潰的邊緣在哪里?系統的極限在哪里?
這樣才能合理地設置流量的上限,為了保證系統的穩定性,多余的流量需要被拋棄。
壓力測試的方法
合理的測試方法可以幫助我們對系統有深入的了解,這里介紹兩種壓力測試的方法:
正壓力測試。每次秒殺活動都會計劃,使用多少服務器資源,承受多少的請求量。
可以在這個請求量上面不斷加壓,直到系統接近崩潰或者真正崩潰。簡單的說就是做加法。
正壓力測試示意圖
負壓力測試。在系統正常運行的情況下,逐步減少支撐系統的資源(服務器),看什么時候系統無法支撐正常的業務請求。
例如:在系統正常運行的情況下,逐步減少服務器或者微服務的數量,觀察業務請求的情況。說白了就是做減法。
負壓力測試示意圖
壓力測試的步驟
測試步驟
有了測試方法的加持,我們來看看需要遵循哪些測試步驟。下面的操作偏套路化,大家在其他系統的壓力測試也可以這么做,給大家做個參考。
第一,確定測試目標。與性能測試不同的是,壓力測試的目標是,什么時候系統會接近崩潰。比如:需要支撐 500W 訪問量。
第二,確定關鍵功能。壓力測試其實是有重點的,根據 2/8 原則,系統中 20% 的功能被使用的是最多的,我們可以針對這些核心功能進行壓力測試。例如:下單,庫存扣減。
關注核心服務
第三,確定負載。這個和關鍵服務的思路一致,不是每個服務都有高負載的,我們的測試其實是要關注那些負載量大的服務,或者是一段時間內系統中某些服務的負載有波動。這些都是測試目標。
第四,選擇環境,建議搭建和生產環境一模一樣的環境進行測試。
第五,確定監視點,實際上就是對關注的參數進行監視,例如 CPU 負載,內存使用率,系統吞吐量等等。
第六,產生負載,這里需要從生產環境去獲取一些真實的數據作為負載數據源,這部分數據源根據目標系統的承受要求由腳本驅動,對系統進行沖擊。
建議使用往期秒殺系統的數據,或者實際生產系統的數據進行測試。
第七,執行測試,這里主要是根據目標系統,關鍵組件,用負載進行測試,返回監視點的數據。
建議團隊可以對測試定一個計劃,模擬不同的網絡環境,硬件條件進行有規律的測試。
第八,分析數據,針對測試的目的,對關鍵服務的壓力測試數據進行分析得知該服務的承受上限在哪里。
對一段時間內有負載波動或者大負載的服務進行數據分析,得出服務改造的方向。
總結
秒殺系統的特點,并發量大,資源有限,操作相對簡單,訪問的都是熱點數據。因此,我們需要把它從業務,技術,數據上做隔離,保證不影響到現有的系統。
因此,架構設計需要分幾層來考慮,從客戶請求到數據庫存儲,到最后上線前的壓力測試。
簡易的思維導圖送給大家
思考順序如下,客戶端→代理層→應用層→數據庫→壓力測試:
客戶端 90% 靜態 HTML+10% 動態 JS;配合 CDN 做好緩存工作。
接入層專注于過濾和限流。
應用層利用緩存+隊列+分布式處理好訂單。
做好數據的預估,隔離,合并。
上線之前記得進行壓力測試。
作者:崔皓
簡介:十六年開發和架構經驗,曾擔任過惠普武漢交付中心技術專家,需求分析師,項目經理,后在創業公司擔任技術/產品經理。善于學習,樂于分享。目前專注于技術架構與研發管理。
【51CTO原創稿件,合作站點轉載請注明原文作者和出處為51CTO.com】
很多小伙伴反饋說,高并發專題學了那么久,但是,在真正做項目時,仍然不知道如何下手處理高并發業務場景!甚至很多小伙伴仍然停留在只是簡單的提供接口(CRUD)階段,不知道學習的并發知識如何運用到實際項目中,就更別提如何構建高并發系統了!
究竟什么樣的系統算是高并發系統?今天,我們就一起解密高并發業務場景下典型的秒殺系統的架構,結合高并發專題下的其他文章,學以致用。
在電商領域,存在著典型的秒殺業務場景,那何謂秒殺場景呢。簡單的來說就是一件商品的購買人數遠遠大于這件商品的庫存,而且這件商品在很短的時間內就會被搶購一空。 比如每年的618、雙11大促,小米新品促銷等業務場景,就是典型的秒殺業務場景。
我們可以將電商系統的架構簡化成下圖所示。
由圖所示,我們可以簡單的將電商系統的核心層分為:負載均衡層、應用層和持久層。接下來,我們就預估下每一層的并發量。
所以,負載均衡層、應用層和持久層各自的并發度是不同的,那么,為了提升系統的總體并發度和緩存,我們通??梢圆扇∧男┓桨改??
(1)系統擴容
系統擴容包括垂直擴容和水平擴容,增加設備和機器配置,絕大多數的場景有效。
(2)緩存
本地緩存或者集中式緩存,減少網絡IO,基于內存讀取數據。大部分場景有效。
(3)讀寫分離
采用讀寫分離,分而治之,增加機器的并行處理能力。
對于秒殺系統來說,我們可以從業務和技術兩個角度來闡述其自身存在的一些特點。
這里,我們可以使用12306網站來舉例,每年春運時,12306網站的訪問量是非常大的,但是網站平時的訪問量卻是比較平緩的,也就是說,每年春運時節,12306網站的訪問量會出現瞬時突增的現象。
再比如,小米秒殺系統,在上午10點開售商品,10點前的訪問量比較平緩,10點時同樣會出現并發量瞬時突增的現象。
所以,秒殺系統的流量和并發量我們可以使用下圖來表示。
由圖可以看出,秒殺系統的并發量存在瞬時凸峰的特點,也叫做流量突刺現象。
我們可以將秒殺系統的特點總結如下。
(1)限時、限量、限價
在規定的時間內進行;秒殺活動中商品的數量有限;商品的價格會遠遠低于原來的價格,也就是說,在秒殺活動中,商品會以遠遠低于原來的價格出售。
例如,秒殺活動的時間僅限于某天上午10點到10點半,商品數量只有10萬件,售完為止,而且商品的價格非常低,例如:1元購等業務場景。
限時、限量和限價可以單獨存在,也可以組合存在。
(2)活動預熱
需要提前配置活動;活動還未開始時,用戶可以查看活動的相關信息;秒殺活動開始前,對活動進行大力宣傳。
(3)持續時間短
購買的人數數量龐大;商品會迅速售完。
在系統流量呈現上,就會出現一個突刺現象,此時的并發訪問量是非常高的,大部分秒殺場景下,商品會在極短的時間內售完。
我們可以將秒殺系統的技術特點總結如下。
(1)瞬時并發量非常高
大量用戶會在同一時間搶購商品;瞬間并發峰值非常高。
(2)讀多寫少
系統中商品頁的訪問量巨大;商品的可購買數量非常少;庫存的查詢訪問數量遠遠大于商品的購買數量。
在商品頁中往往會加入一些限流措施,例如早期的秒殺系統商品頁會加入驗證碼來平滑前端對系統的訪問流量,近期的秒殺系統商品詳情頁會在用戶打開頁面時,提示用戶登錄系統。這都是對系統的訪問進行限流的一些措施。
(3)流程簡單
秒殺系統的業務流程一般比較簡單;總體上來說,秒殺系統的業務流程可以概括為:下單減庫存。
針對這種短時間內大流量的系統來說,就不太適合使用系統擴容了,因為即使系統擴容了,也就是在很短的時間內會使用到擴容后的系統,大部分時間內,系統無需擴容即可正常訪問。 那么,我們可以采取哪些方案來提升系統的秒殺性能呢?
針對秒殺系統的特點,我們可以采取如下的措施來提升系統的性能。
(1)異步解耦
將整體流程進行拆解,核心流程通過隊列方式進行控制。
(2)限流防刷
控制網站整體流量,提高請求的門檻,避免系統資源耗盡。
(3)資源控制
將整體流程中的資源調度進行控制,揚長避短。
由于應用層能夠承載的并發量比緩存的并發量少很多。所以,在高并發系統中,我們可以直接使用OpenResty由負載均衡層訪問緩存,避免了調用應用層的性能損耗。大家可以到https://openresty.org/cn/來了解有關OpenResty更多的知識。同時,由于秒殺系統中,商品數量比較少,我們也可以使用動態渲染技術,CDN技術來加速網站的訪問性能。
如果在秒殺活動開始時,并發量太高時,我們可以將用戶的請求放入隊列中進行處理,并為用戶彈出排隊頁面。
網上很多的秒殺系統和對秒殺系統的解決方案,并不是真正的秒殺系統,他們采用的只是同步處理請求的方案,一旦并發量真的上來了,他們所謂的秒殺系統的性能會急劇下降。我們先來看一下秒殺系統在同步下單時的時序圖。
在同步下單流程中,首先,用戶發起秒殺請求。商城服務需要依次執行如下流程來處理秒殺請求的業務。
(1)識別驗證碼是否正確
商城服務判斷用戶發起秒殺請求時提交的驗證碼是否正確。
(2)判斷活動是否已經結束
驗證當前秒殺活動是否已經結束。
(3)驗證訪問請求是否處于黑名單
在電商領域中,存在著很多的惡意競爭,也就是說,其他商家可能會通過不正當手段來惡意請求秒殺系統,占用系統大量的帶寬和其他系統資源。此時,就需要使用風控系統等實現黑名單機制。為了簡單,也可以使用攔截器統計訪問頻次實現黑名單機制。
(4)驗證真實庫存是否足夠
系統需要驗證商品的真實庫存是否足夠,是否能夠支持本次秒殺活動的商品庫存量。
(5)扣減緩存中的庫存
在秒殺業務中,往往會將商品庫存等信息存放在緩存中,此時,還需要驗證秒殺活動使用的商品庫存是否足夠,并且需要扣減秒殺活動的商品庫存數量。
(6)計算秒殺的價格
由于在秒殺活動中,商品的秒殺價格和商品的真實價格存在差異,所以,需要計算商品的秒殺價格。
注意:如果在秒殺場景中,系統涉及的業務更加復雜的話,會涉及更多的業務操作,這里,我只是列舉出一些常見的業務操作。
(1)訂單入口
將用戶提交的訂單信息保存到數據庫中。
(2)扣減真實庫存
訂單入庫后,需要在商品的真實庫存中將本次成功下單的商品數量扣除。
如果我們使用上述流程開發了一個秒殺系統,當用戶發起秒殺請求時,由于系統每個業務流程都是串行執行的,整體上系統的性能不會太高,當并發量太高時,我們會為用戶彈出下面的排隊頁面,來提示用戶進行等待。
此時的排隊時間可能是15秒,也可能是30秒,甚至是更長時間。這就存在一個問題:在用戶發起秒殺請求到服務器返回結果的這段時間內,客戶端和服務器之間的連接不會被釋放,這就會占大量占用服務器的資源。
網上很多介紹如何實現秒殺系統的文章都是采用的這種方式,那么,這種方式能做秒殺系統嗎?答案是可以做,但是這種方式支撐的并發量并不是太高。此時,有些網友可能會問:我們公司就是這樣做的秒殺系統??!上線后一直在用,沒啥問題啊!我想說的是:使用同步下單方式確實可以做秒殺系統,但是同步下單的性能不會太高。之所以你們公司采用同步下單的方式做秒殺系統沒出現大的問題,那是因為你們的秒殺系統的并發量沒達到一定的量級,也就是說,你們的秒殺系統的并發量其實并不高。
所以,很多所謂的秒殺系統,存在著秒殺的業務,但是稱不上真正的秒殺系統,原因就在于他們使用的是同步的下單流程,限制了系統的并發流量。之所以上線后沒出現太大的問題,是因為系統的并發量不高,不足以壓死整個系統。
如果12306、淘寶、天貓、京東、小米等大型商城的秒殺系統是這么玩的話,那么,他們的系統遲早會被玩死,他們的系統工程師不被開除才怪!所以,在秒殺系統中,這種同步處理下單的業務流程的方案是不可取的。
以上就是同步下單的整個流程操作,如果下單流程更加復雜的話,就會涉及到更多的業務操作。
既然同步下單流程的秒殺系統稱不上真正的秒殺系統,那我們就需要采用異步的下單流程了。異步的下單流程不會限制系統的高并發流量。
用戶發起秒殺請求后,商城服務會經過如下業務流程。
(1)檢測驗證碼是否正確
用戶發起秒殺請求時,會將驗證碼一同發送過來,系統會檢驗驗證碼是否有效,并且是否正確。
(2)是否限流
系統會對用戶的請求進行是否限流的判斷,這里,我們可以通過判斷消息隊列的長度來進行判斷。因為我們將用戶的請求放在了消息隊列中,消息隊列中堆積的是用戶的請求,我們可以根據當前消息隊列中存在的待處理的請求數量來判斷是否需要對用戶的請求進行限流處理。
例如,在秒殺活動中,我們出售1000件商品,此時在消息隊列中存在1000個請求,如果后續仍然有用戶發起秒殺請求,則后續的請求我們可以不再處理,直接向用戶返回商品已售完的提示。
所以,使用限流后,我們可以更快的處理用戶的請求和釋放連接的資源。
(3)發送MQ
用戶的秒殺請求通過前面的驗證后,我們就可以將用戶的請求參數等信息發送到MQ中進行異步處理,同時,向用戶響應結果信息。在商城服務中,會有專門的異步任務處理模塊來消費消息隊列中的請求,并處理后續的異步流程。
在用戶發起秒殺請求時,異步下單流程比同步下單流程處理的業務操作更少,它將后續的操作通過MQ發送給異步處理模塊進行處理,并迅速向用戶返回響應結果,釋放請求連接。
我們可以將下單流程的如下操作進行異步處理。
(1)判斷活動是否已經結束
(2)判斷本次請求是否處于系統黑名單,為了防止電商領域同行的惡意競爭可以為系統增加黑名單機制,將惡意的請求放入系統的黑名單中??梢允褂脭r截器統計訪問頻次來實現。
(3)扣減緩存中的秒殺商品的庫存數量。
(4)生成秒殺Token,這個Token是綁定當前用戶和當前秒殺活動的,只有生成了秒殺Token的請求才有資格進行秒殺活動。
這里我們引入了異步處理機制,在異步處理中,系統使用多少資源,分配多少線程來處理相應的任務,是可以進行控制的。
這里,可以采取客戶端短輪詢查詢是否獲得秒殺資格的方案。例如,客戶端可以每隔3秒鐘輪詢請求服務器,查詢是否獲得秒殺資格,這里,我們在服務器的處理就是判斷當前用戶是否存在秒殺Token,如果服務器為當前用戶生成了秒殺Token,則當前用戶存在秒殺資格。否則繼續輪詢查詢,直到超時或者服務器返回商品已售完或者無秒殺資格等信息為止。
采用短輪詢查詢秒殺結果時,在頁面上我們同樣可以提示用戶排隊處理中,但是此時客戶端會每隔幾秒輪詢服務器查詢秒殺資格的狀態,相比于同步下單流程來說,無需長時間占用請求連接。
此時,可能會有網友會問:采用短輪詢查詢的方式,會不會存在直到超時也查詢不到是否具有秒殺資格的狀態呢?答案是:有可能! 這里我們試想一下秒殺的真實場景,商家參加秒殺活動本質上不是為了賺錢,而是提升商品的銷量和商家的知名度,吸引更多的用戶來買自己的商品。所以,我們不必保證用戶能夠100%的查詢到是否具有秒殺資格的狀態。
(1)驗證下單Token
客戶端提交秒殺結算時,會將秒殺Token一同提交到服務器,商城服務會驗證當前的秒殺Token是否有效。
(2)加入秒殺購物車
商城服務在驗證秒殺Token合法并有效后,會將用戶秒殺的商品添加到秒殺購物車。
(1)訂單入庫
將用戶提交的訂單信息保存到數據庫中。
(2)刪除Token
秒殺商品訂單入庫成功后,刪除秒殺Token。
這里大家可以思考一個問題:我們為什么只在異步下單流程的粉色部分采用異步處理,而沒有在其他部分采取異步削峰和填谷的措施呢?
這是因為在異步下單流程的設計中,無論是在產品設計上還是在接口設計上,我們在用戶發起秒殺請求階段對用戶的請求進行了限流操作,可以說,系統的限流操作是非常前置的。在用戶發起秒殺請求時進行了限流,系統的高峰流量已經被平滑解決了,再往后走,其實系統的并發量和系統流量并不是非常高了。
所以,網上很多的文章和帖子中在介紹秒殺系統時,說是在下單時使用異步削峰來進行一些限流操作,那都是在扯淡! 因為下單操作在整個秒殺系統的流程中屬于比較靠后的操作了,限流操作一定要前置處理,在秒殺業務后面的流程中做限流操作是沒啥卵用的。
假設,在秒殺系統中我們使用Redis實現緩存,假設Redis的讀寫并發量在5萬左右。我們的商城秒殺業務需要支持的并發量在100萬左右。如果這100萬的并發全部打入Redis中,Redis很可能就會掛掉,那么,我們如何解決這個問題呢?接下來,我們就一起來探討這個問題。
在高并發的秒殺系統中,如果采用Redis緩存數據,則Redis緩存的并發處理能力是關鍵,因為很多的前綴操作都需要訪問Redis。而異步削峰只是基本的操作,關鍵還是要保證Redis的并發處理能力。
解決這個問題的關鍵思想就是:分而治之,將商品庫存分開放。
我們在Redis中存儲秒殺商品的庫存數量時,可以將秒殺商品的庫存進行“分割”存儲來提升Redis的讀寫并發量。
例如,原來的秒殺商品的id為10001,庫存為1000件,在Redis中的存儲為(10001, 1000),我們將原有的庫存分割為5份,則每份的庫存為200件,此時,我們在Redia中存儲的信息為(10001_0, 200),(10001_1, 200),(10001_2, 200),(10001_3, 200),(10001_4, 200)。
此時,我們將庫存進行分割后,每個分割后的庫存使用商品id加上一個數字標識來存儲,這樣,在對存儲商品庫存的每個Key進行Hash運算時,得出的Hash結果是不同的,這就說明,存儲商品庫存的Key有很大概率不在Redis的同一個槽位中,這就能夠提升Redis處理請求的性能和并發量。
分割庫存后,我們還需要在Redis中存儲一份商品id和分割庫存后的Key的映射關系,此時映射關系的Key為商品的id,也就是10001,Value為分割庫存后存儲庫存信息的Key,也就是10001_0,10001_1,10001_2,10001_3,10001_4。在Redis中我們可以使用List來存儲這些值。
在真正處理庫存信息時,我們可以先從Redis中查詢出秒殺商品對應的分割庫存后的所有Key,同時使用AtomicLong來記錄當前的請求數量,使用請求數量對從Redia中查詢出的秒殺商品對應的分割庫存后的所有Key的長度進行求模運算,得出的結果為0,1,2,3,4。再在前面拼接上商品id就可以得出真正的庫存緩存的Key。此時,就可以根據這個Key直接到Redis中獲取相應的庫存信息。
在高并發業務場景中,我們可以直接使用Lua腳本庫(OpenResty)從負載均衡層直接訪問緩存。
這里,我們思考一個場景:如果在秒殺業務場景中,秒殺的商品被瞬間搶購一空。此時,用戶再發起秒殺請求時,如果系統由負載均衡層請求應用層的各個服務,再由應用層的各個服務訪問緩存和數據庫,其實,本質上已經沒有任何意義了,因為商品已經賣完了,再通過系統的應用層進行層層校驗已經沒有太多意義了?。《鴳脤拥牟l訪問量是以百為單位的,這又在一定程度上會降低系統的并發度。
為了解決這個問題,此時,我們可以在系統的負載均衡層取出用戶發送請求時攜帶的用戶id,商品id和秒殺活動id等信息,直接通過Lua腳本等技術來訪問緩存中的庫存信息。如果秒殺商品的庫存小于或者等于0,則直接返回用戶商品已售完的提示信息,而不用再經過應用層的層層校驗了。 針對這個架構,我們可以參見本文中的電商系統的架構圖(正文開始的第一張圖)。
原文鏈接:https://www.cnblogs.com/binghe001/p/12663557.html
到“秒殺”,恐怕大多數人想到的就是“雙 11”,“促銷”,“買買買”等火爆的場面吧。
大家為了打折商品蜂擁而至,造成電商網站一片繁華的景象。但作為程序員的我們,看到的卻是背后的高并發和可靠性。無論你處在軟件開發的哪個階段,都希望能夠設計一套屬于自己的秒殺系統。
今天我們一起來看看,一套秒殺系統在架構設計上需要有哪些考量?
秒殺場景是電商網站定期舉辦的活動,這個活動有明確的開始和結束時間,而且參與互動的商品是事先定義好了,參與秒殺商品的個數也是有限制的。同時會提供一個秒殺的入口,讓用戶通過這個入口進行搶購。
總結一下秒殺場景的特點:
在分析秒殺的特點后,我們發現秒殺活動是有計劃的,并且在短時間內會爆發大量的請求。為了不影響現有的業務系統的正常運行,我們需要把它和現有的系統做隔離。
即使秒殺活動出現問題也不會影響現有的系統。隔離的設計思路可以從三個維度來思考。
業務隔離
既然秒殺是一場活動,那它一定和常規的業務不同,我們可以把它當成一個單獨的項目來看。在活動開始之前,最好設計一個“熱場”。
“熱場”的形式多種多樣,例如:分享活動領優惠券,領秒殺名額等等?!盁釄觥钡男问讲恢匾?,重要的是通過它獲取一些準備信息。
例如:有可能參與的用戶數,他們的地域分布,他們感興趣的商品。為后面的技術架構提供數據支持。
技術隔離
技術隔離架構圖
前面有了準備工作,那么從技術上需要有以下幾個方面的考慮:
所以,在這一層我們要考慮的是如何做好限流,當超過系統承受范圍的時候,需要果斷阻止請求的涌入。
需要使用服務集群和水平擴展,讓“高峰”請求分流到不同的服務器進行處理。同時,還會利用緩存和隊列技術減輕應用處理的壓力,通過異步請求的方式做到最終一致性。
由于是多線程操作,而且商品的額度有限,為了解決超賣的問題,需要考慮進程鎖的問題。
數據庫隔離
秒殺活動持續時間短,瞬時數據量大。為了不影響現有數據庫的正常業務,可以建立新的庫或者表來處理。
在秒殺結束以后,需要把這部分數據同步到主業務系統中,或者查詢表中。如果數據量特別巨大,到千萬級別甚至上億,建議使用分表或者分庫。
上面提到的三個隔離維度中,我們對技術維度是最為關心的。如果說瀏覽器/客戶端是用戶接觸“秒殺系統”的入口,那么在這一層提供緩存數據就是非常必要的。
在設計之初,我們會為秒殺的商品生成專門的商品頁面和訂單頁面。這些頁面以靜態的 HTML 為主,包括的動態信息盡量少。
從業務的角度來說,這些商品的信息早就被用戶熟識了,在秒殺的時候,他們關心的是如何快速下單。
既然商品的詳情頁面和訂單頁面都是靜態生成的,那么就需要定義一個 URL,當要開始秒殺之前,開放這個 URL 給用戶訪問。
為了防止“程序員或者內部人員”作弊,這里的地址可以通過時間戳和 Hash 算法來生成,也就是說這個地址只有系統知道,到了快秒殺之前才由系統發放出去。
有人說瀏覽器/客戶端如果存放的都是靜態頁面,那么“控制開始下單”的按鈕,以及發送“下單請求”的按鈕,也是靜態的嗎?
答案是否定的,其實靜態頁面是方便客戶端好緩存,下單的動作以及下單時間的控制還是在服務器端。
只不過是通過 JS 文件的方式發送給客戶端,在快要秒殺之前,會把這部分 JS 下載到客戶端。
因為,其業務邏輯很少,基本只包括時間,用戶信息,商品信息等等。所以,其對網絡的要求不高。
同時,在網絡設計上,我們也會將 JS 和 HTML 同時緩存在 CDN 上面,讓用戶從離自己最近的 CDN 服務器上獲取這些信息。
為了避免秒殺程序參與秒殺,在客戶端也會設計一些問答或者滑塊的功能,減少此類機器人對服務器的壓力。
秒殺系統前端設計簡圖
說完了秒殺系統的前端設計,請求自然地來到了代理層。由于用戶的請求量大,我們需要用負載均衡加上服務器集群,來面對如此空前的壓力。
代理層三大功能簡圖
在這一層是可以做緩存,過濾和限流的:
“秒殺系統”秒殺的是什么?無非是商品。對于系統來說就是商品的庫存,購買的商品一旦超過了庫存就不能再賣了。
超過了庫存還可以賣給用戶,這就是“超賣”,也是系統設計需要避免的。為了承受大流量的訪問,我們用了水平擴展的服務,但是對于他們消費的資源“庫存”來說,卻只有一個。
為了提高效率,會將這個庫存信息放到緩存中。以流行的 Redis 為例,用它存放庫存信息,由多個線程來訪問就會出現資源爭奪的情況。也就是分布式程序爭奪唯一資源,為了解決這個問題我們需要實現分布式鎖。
假設這里有多個應用響應用戶的訂單請求,他們同時會去訪問 Redis 中存放的庫存信息,每接受用戶一次請求,都會從 Redis 的庫存中減去 1 個商品庫存量。
當任何一個進程訪問 Redis 中的庫存資源時,其他進程是不能訪問的,所以這里需要考慮鎖的情況(樂觀,悲觀)。
Redis 緩存承載庫存變量
如果鎖長期沒有釋放,需要考慮鎖的過期時間,需要設置兩個超時時間:
這里的“扣減服務”完成了最簡單的扣減庫存工作,并沒有和其他項目服務打交道,更沒有訪問數據庫。
訂單流程示意圖
后面的流程相對比較復雜,我們先看圖,根據圖示來講解:
雖然,這個信息和最終結果有偏差,但是在秒殺的場景,要求高性能是前提,結果的一致性,可以后期補償。
講完了秒殺的處理流程,來談談數據庫設計要注意的點。
數據估算
前面說了秒殺場景需要注意隔離,這里的隔離包括“業務隔離”。就是說我們在秒殺之前,需要通過業務的手段,例如:熱場活動,問卷調查,歷史數據分析。通過他們去估算這次秒殺可能需要存儲的數據量。
這里有兩部分的數據需要考慮:
前者不言而喻是給業務系統用的。后者,是用來分析和后續處理問題訂單用的,秒殺完畢以后還可以用來復盤。
分表分庫
對于這些數據的存放,需要分情況討論,例如,MySQL 單表推薦的存儲量是 500W 條記錄(經驗數字)。
如果估算的時候超過了這個數據,建議做分表。如果服務的連接數較多,建議進行分庫的操作。
數據隔離
由于大量的數據操作是插入,有少部分的修改操作。如果使用關系型數據來存儲,建議用專門的表來存放,不建議使用業務系統正在使用的表。
這個開頭提到了,數據隔離是必須的,一旦秒殺系統掛了,不會影響到正常業務系統,這個風險意識要有。表的設計除了 ID 以外,最好不要設置其他主鍵,保證能夠快速地插入。
數據合并
由于是用的專用表存儲,在秒殺活動完畢以后,需要將其和現有的數據做合并。其實,交易已經完成,合并的目的也就是查詢。
這個合并需要根據具體情況來分析,如果對于那些“只讀”的數據,對于做了讀寫分離的公司,可以導入到專門負責讀的數據庫或者 NoSQL 數據庫中。
構建了秒殺系統,一定會面臨上線,那么在上線之前壓力測試是必不可少的。
我們做壓力測試的目的是檢驗系統崩潰的邊緣在哪里?系統的極限在哪里?
這樣才能合理地設置流量的上限,為了保證系統的穩定性,多余的流量需要被拋棄。
壓力測試的方法
合理的測試方法可以幫助我們對系統有深入的了解,這里介紹兩種壓力測試的方法:
正壓力測試。每次秒殺活動都會計劃,使用多少服務器資源,承受多少的請求量。
可以在這個請求量上面不斷加壓,直到系統接近崩潰或者真正崩潰。簡單的說就是做加法。
負壓力測試。在系統正常運行的情況下,逐步減少支撐系統的資源(服務器),看什么時候系統無法支撐正常的業務請求。
例如:在系統正常運行的情況下,逐步減少服務器或者微服務的數量,觀察業務請求的情況。說白了就是做減法。
壓力測試的步驟
測試步驟
有了測試方法的加持,我們來看看需要遵循哪些測試步驟。下面的操作偏套路化,大家在其他系統的壓力測試也可以這么做,給大家做個參考。
第一,確定測試目標。與性能測試不同的是,壓力測試的目標是,什么時候系統會接近崩潰。比如:需要支撐 500W 訪問量。
第二,確定關鍵功能。壓力測試其實是有重點的,根據 2/8 原則,系統中 20% 的功能被使用的是最多的,我們可以針對這些核心功能進行壓力測試。例如:下單,庫存扣減。
關注核心服務
第三,確定負載。這個和關鍵服務的思路一致,不是每個服務都有高負載的,我們的測試其實是要關注那些負載量大的服務,或者是一段時間內系統中某些服務的負載有波動。這些都是測試目標。
第四,選擇環境,建議搭建和生產環境一模一樣的環境進行測試。
第五,確定監視點,實際上就是對關注的參數進行監視,例如 CPU 負載,內存使用率,系統吞吐量等等。
第六,產生負載,這里需要從生產環境去獲取一些真實的數據作為負載數據源,這部分數據源根據目標系統的承受要求由腳本驅動,對系統進行沖擊。
建議使用往期秒殺系統的數據,或者實際生產系統的數據進行測試。
第七,執行測試,這里主要是根據目標系統,關鍵組件,用負載進行測試,返回監視點的數據。
建議團隊可以對測試定一個計劃,模擬不同的網絡環境,硬件條件進行有規律的測試。
第八,分析數據,針對測試的目的,對關鍵服務的壓力測試數據進行分析得知該服務的承受上限在哪里。
對一段時間內有負載波動或者大負載的服務進行數據分析,得出服務改造的方向。
秒殺系統的特點,并發量大,資源有限,操作相對簡單,訪問的都是熱點數據。因此,我們需要把它從業務,技術,數據上做隔離,保證不影響到現有的系統。
因此,架構設計需要分幾層來考慮,從客戶請求到數據庫存儲,到最后上線前的壓力測試。
簡易的思維導圖送給大家
思考順序如下,客戶端→代理層→應用層→數據庫→壓力測試:
客戶端 90% 靜態 HTML+10% 動態 JS;配合 CDN 做好緩存工作。
接入層專注于過濾和限流。
應用層利用緩存+隊列+分布式處理好訂單。
做好數據的預估,隔離,合并。
上線之前記得進行壓力測試。
作者簡介:
崔皓,十六年開發和架構經驗,曾擔任過惠普武漢交付中心技術專家,需求分析師,項目經理,后在創業公司擔任技術/產品經理。善于學習,樂于分享。目前專注于技術架構與研發管理。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。