高併發系統的設計及秒殺實踐

前言:文末有許令波老師的秒殺專欄,只需要9.9,有這方面需要的童鞋們不妨關注一下

一個大型網站應用一般都是從最初小規模網站甚至是單機應用發展而來的,爲了讓系統能夠支持足夠大的業務量,從前端到後端也採用了各種各樣技術,前端靜態資源壓縮整合、使用CDN、分佈式SOA架構、緩存、數據庫加索引、讀寫分離等等。 這些技術是高併發系統所必須的,但是今天先不細說,而先談談在這些架構既定的情況下,一些高併發業務/接口實現時應該注意的原則,以及通過工作中一個6萬QPS的秒殺活動,來介紹一下秒殺業務的特點以及如何優化。

高併發系統設計原則

高併發的接口/系統有一個共同的特性,那就是”快”。 在系統其它條件既定的情況下,系統處理請求越快,用戶得到反饋的時間就越短,單位時間內服務器能夠處理請求的數量就會越多。所以”快”幾乎可以算是高併發系統的要滿足的必要條件,要評估一個系統性能如何,某次優化是否提高系統的容量,”快”是一個很直觀的衡量標準。

那麼,如何才能做得快呢?有兩個需要注意的原則 1. 做得少,一方面是指在功能特性上有所爲,有所不爲,另一方面是指一次處理的信息量要少。 2. 做得巧,根據業務自身的特點,選擇合理的業務實現方式,選擇合理的緩存類型和緩存調用時機。

做得少

世界上最快的程序,是什麼都不做的程序。 一個接口負責的功能越少,讀取信息量越少,速度越快。

功能特性有選擇

對於一個需要承受高併發的接口,在功能上,儘量不涉及一些難以緩存和預熱的數據。 一個典型的例子,用戶維度個性化的數據,用戶和用戶的信息不同,userId數量又很多,即使加上緩存,緩存命中率依然很低,壓力還是會打到數據庫,不光接口快不了,高併發的sql也會給數據庫帶來風險。

舉一個例子,在點評電影早期的秒殺活動頁上,展示了一個用戶當前秒殺資格的信息,由於不同用戶搶到秒殺資格的時間、優惠不同,每次都需要讀數據庫的來取,也就是每個用戶進入主頁都會產生一條sql。 還有一個例子,一般電商搞大促的時候,比如同時有多個優惠活動可以降低商品的價格,而一般只展示最低價的優惠,同時用戶一個優惠只能參與一次,這樣不同用戶參與了不同活動之後可以享受的最低價就會隨之改變,如果要在商品頁面上展示這個動態價格,就免不了取到各個用戶參加這些在線優惠的信息。

如果遇到這樣的數據,要怎麼解決呢? 一個辦法是嘗試轉移數據的維度:剛纔說的秒殺活動資格信息,如果以用戶userId爲key,會出現緩存命中率低,仍要sql讀的情況,但是能夠秒到的用戶數量其實很少,所以如果以這次秒殺活動id爲key,存儲一個成功秒到用戶的userid的list,就能夠解決緩存命中率低的問題。

還有一個辦法是可以把這些需要個性化數據的功能在業務流程上後移,流量漏斗,越往後流量越少,創建訂單級的sql查詢是可接受的。 剛纔說的第二個例子,商品最優惠的價格,可以排除用戶相關信息,只在商品列表/詳情上展示只和優惠相關的最低價,而在提交訂單的時候才真正去取用戶參加活動情況,如果用戶已經參加過給出提示並選擇次優的優惠。商品的列表/詳情頁都在用戶路徑上相對靠前的位置,排除了用戶個性化信息可以讓商品列表/詳情更容易緩存,響應速度更快,系統可承受的高併發量更高。

處理信息量要少

我們寫業務代碼的時候都有對應的業務對象,它們都存在一定的業務範圍之內,比如類目、地區、日期等自身相關的維度。 一個系統中的業務對象,在多個維度的細分下,對應的量並不多,但如果一次全部都展示在一個頁面/接口下,即使覆蓋上了緩存,也會由於緩存佔用空間過大或者緩存key數目過多、網絡傳輸耗時、對象序列化反序列耗時等拖慢接口/頁面響應速度。一般只要看一下這個頁面/接口給出的業務對象的數量級,就能大致知道這個接口的性能了。

大家在做設計的時候,一般會估算一個接口的量級,如果一看就有幾千幾萬個業務對象,就不會這樣設計了,但是需要警惕的是業務對象數量級可變的情況,比如隨着業務發展數量會快速增長,或者某些特殊維度下業務對象特別多。設計的時候要按照預估的最大量級來,並且對接口/頁面做出數量的限制,如果發現當前返回的業務對象過多,可以繼續根據業務維度來拆分,分次分批來處理。

舉一個例子,比如一個影院下所有的活動場次,開始的時候一家影院下的場次有限,幾十一百場,很好展示,後來隨着業務發展,一個影院下各個影院下場次數到了幾百一千,一次全部拿完,在高併發時,memcached緩存的multi get會出現很多超時,請求會打到mysql數據庫,給系統很大壓力。之後我們做了改造項目,每次根據用戶的交互按照影片、日期、影院的維度來分批取,一次只有十幾個場次,接口響應變快了,服務的壓力也小的多。

做得巧

根據業務特性選擇實現方式

平時涉及到的業務,總有屬於它的特性,比如實時性要求多高,數據一致性要求多高,涉及什麼維度的數據,量有多大等等,我們要根據這些特性來選擇實現的方案,比如一些統計數據,如某類目下所有商品的最低價,按照邏輯需要遍歷商品來獲取,但這樣每次實時讀取所有的對象,涉及讀取緩存數據庫操作,接口會很耗時,但如果選擇作業離線計算,把計算結果寫表,加上緩存,搜索直接讀取,顯然會快很多了。

涉及到業務各階段特性的例子就是秒殺系統,在第二部分秒殺實踐中我會詳細介紹。

合適選擇和調用緩存

除了業務特性方面,緩存是業務對抗高併發非常重要的一個環節,合理選擇緩存的類型和調用緩存的時機非常重要。

我們知道內存運算速度快於遠程連接,所以存儲上來說效率如下 內存 <= ehcache < redis <= memcached < mysql 可以看出,儘量少的遠程連接,常規覆蓋數據庫訪問的緩存,都能提高程序的性能。

要根據不同緩存的特性和原理,才能根據業務選出最合適的,來看看幾種常用的緩存 1. varnish,可以作爲反向代理,緩存一些資源,例如可以把struts,freemarker動態生成的頁面存儲起來,達到直接擋掉到達web服務器的請求。 2. ehcache,主要存儲在當前機器內存中,存取非常快,缺點是內存有限,各臺機器內存中各存一份,失效時間不一致,數據就會出現不一致,一般用來緩存不常變化,且緩存個數較少的數據。 3. memcached緩存,kv分佈式緩存集羣,可擴展性好,可以存儲個數較多的緩存對象,也可以承接高流量的訪問,讀取緩存時遠程連接,一般耗時也在零點幾到幾ms不等。 4. redis,nosql,是內存的kv存儲,可以做爲緩存使用,也可以持久化,它的性能和memcached相近。而redis最大的特點是一個data-structure store,這時redis官網首頁介紹redis的第一句話,它可以保存list,hash,set,sorted set等數據結構,使用時和memcached區別是,它不用將數據取到客戶端再做邏輯判斷,而是可以直接在redis服務器上完成操作,比如查看某個元素是不是一個範圍內,隊列的長度有多長等。redis可以用來做分佈式服務器的進程間的通信,比如我們經常有需要分佈式鎖的場景,控制同一個用戶發券的併發等。

根據業務需要選擇了合適類型的緩存後,還要合理去使用。 雖然說緩存是爲了抵擋數據庫的流量而生,本身性能非常強大,但仍然是受到緩存服務器性能甚至服務器網卡流量的限制的,不合理的使用比如單個key對應的緩存對象過大、一次讀取中緩存key數量過多、短時間內頻繁更新緩存等都是系統的隱患、併發越高時就越能體現。

秒殺實踐

秒殺業務分析

秒殺業務的典型特點有: 1. 瞬時流量大 2. 參與用戶多,可秒殺商品數量少 3. 請求讀多寫少 4. 秒殺狀態轉換實時性要求高

一次秒殺的流程可以分爲三個階段: 1. 活動未開始 活動開始前,用戶進入活動頁,這個階段有兩種請求,一種是加載活動頁信息,一個是查詢活動狀態得到未開始的結果, 一個用戶進入頁面兩個請求各發起一次,這兩種請求佔比各半。 2. 活動進行中 這個階段持續時間非常短,看到搶購按鈕的用戶大量發起秒殺請求,瞬時秒殺請求佔比增高,能不能抗住秒殺請求就是秒殺系統是否能抗住高併發的關鍵。 3. 活動結束 當商品被搶購完,進入結束狀態,請求情況同活動開始前

各階段流量圖 其實貫穿整個活動的只有三種請求,加載活動頁請求,讀取活動狀態請求,秒殺請求

加載活動頁請求

主要是展示活動相關配置信息,活動背景圖片,優惠力度,活動規則等相對靜態的內容,通過web項目渲染成頁面。

對於這樣的請求,我們可以使用varnish反向代理,以頁面相關的參數比如本次秒殺的活動ID和城市ID的hash爲key把整個頁面緩存在varnish機器上,而秒殺活動的狀態等動態信息通過ajax來刷新。

varnish作用機制

達到的效果是活動期間,加載頁面請求都會打到varnish機器直接返回,而不會給web和service帶來任何壓力。

查詢活動狀態

秒殺狀態就三種,未開始,可搶,已搶完,由兩個因素共同決定 1. 活動開始時間 2. 剩餘庫存

讀取秒殺狀態的請求數併發也是非常高的,對於這個接口也要加上合適的緩存來處理。 對於活動開始時間,是一個較固定且不會發生變化的屬性,並且,同時在線的秒殺活動數目並不多,所以把它也作爲discount相關的信息,選擇用響應快的ehcache來緩存。

對於庫存,剩餘庫存個數,一般來說是全局需要一致的,可以用memcached來緩存,在秒殺的過程中,庫存變化的非常快,如果直接對庫存個數進行緩存,那麼秒殺期間就需要頻繁的更新緩存,像之前說的,雖然緩存是用來扛併發的,但要調用緩存的時機也要合理,memcached處理的併發請求越少,相對成功率就會越高。 其實對於秒殺活動來說,當時的剩餘庫存數在秒殺期間變化非常快,某個時間點上的庫存個數並沒有太大的意義,而用戶更關心的是 能不能搶,true or false。如果緩存true or false的話,這個值在秒殺期間是相對穩定的,只需要在庫存耗盡的時候更新一次,而且爲了防止這一次的更新失敗,可以重複更新,利用memcached的cas操作,最後memcached也只會真正執行一次set寫操作。 因爲秒殺期間查詢活動狀態的請求都打在memcached上,減少寫的頻率可以明顯減輕memcached的負擔。

其實活動狀態除了活動時間和庫存之外,還有第三個因素來決定,下面說到秒殺請求的優化時會詳細來說

秒殺請求

秒殺請求分析

秒殺請求是一個秒殺系統能不能抗住高併發的關鍵 因爲秒殺請求和之前兩個請求不同,它是寫請求,不能緩存,而且是活動峯值的主力。

一個用戶從發出秒殺請求到成功秒殺簡單地說需要兩個步驟: 1. 扣庫存 2. 發送秒殺商品 這是至少兩條數據庫操作,而且扣庫存的這一步,在mysql的innodb引擎行鎖機制下,update的sql到了數據庫就開始排隊,期間數據庫連接是被佔用的,當請求足夠多時就會造成數據庫的擁堵。 可以看出,秒殺請求接口是一個耗時相對長的接口,而且併發越高耗時越長,所以首先,一定要限制能夠真正進行秒殺的人數。

秒殺流程圖

上面說了,秒殺業務的一個特點是參與人數多,但是可供秒殺的商品少,也就是說只有極少部分的用戶最終能夠秒殺成功 比如有2500個名額,理論上來說先發送請求的2500個用戶能夠秒殺成功,這2500個用戶扣庫存的sql在數據庫排隊的時候,庫存還沒有消耗完,比如2500個請求,全部排隊更新完是需要時間的,就比如說0.5s 在這個時間內,用戶會看到當前仍然是可搶狀態,所以這段時間內持續會有秒殺請求進入,秒殺的高峯期,0.5秒也有幾萬的請求,讓幾萬條sql來競爭是沒有意義的,所以要限制這些參與到扣庫存這一步的人數。

秒殺隊列校驗

可搶狀態需要第三個因素來決定,那就是當前秒殺的排隊人數。 加在判斷庫存剩餘之前,擋上一層排隊人數的校驗, 即有庫存 並且 排隊人數 < 限制請求數 = 可搶,有庫存 並且 排隊人數 >= 限制請求數 = 搶完

比如2500個名額秒殺名額,目標放過去3000個秒殺請求

那麼排隊人數記在哪裏? 這個可以有所選擇,如果只記請求個數,可以用memcached的計數,一個用戶進入秒殺流程increase一次,判斷庫存之前先判斷隊列長度,這樣就限制了可參與秒殺的用戶數量。

排隊秒殺流程圖

發起秒殺先去問排隊隊列是不是已滿,滿了直接秒殺失敗,同時可以去更新之前緩存了是否可搶 true or false的緩存,直接把前臺可搶的狀態變爲不可搶。沒滿繼續查詢庫存等後續流程,開始扣庫存的時候,把當前用戶id入隊。 這樣,就限制了真正進入秒殺的人數。

這種方法,可能會有一個問題,既然限制了請求數,那就必須要保證放過去的用戶能夠秒完商品,假設有重複提交的用戶,如果重複提交的量大,比如放過去的請求中有一半都是重複提交,就會造成最後沒秒完的情況,怎麼屏蔽重複用戶呢? 就要有個地方來記參與的用戶id,可以使用redis的set結構來保存,這個時候set的size代表當前排隊的用戶數,扣庫存之前add當前用戶id到set,根據add是否成功的結果,來判斷是否繼續處理請求。

最終,把實際上幾萬個參與數據庫操作的用戶從減少到秒殺商品的級別,這是一個數據庫可控制的範圍,即使參與的用戶再多,實際上也只處理了秒殺商品數量級的請求。

更多的優化

1.分庫存 一般這樣做就已經能夠滿足常規秒殺的需求了,但有一個問題依然沒有解決,那就是加鎖釦庫存依然很慢 假設的活動秒殺的商品量能夠再上一個量級,像小米賣個手機,一次有幾W到幾十萬的時候,數據庫也是扛不住這個量的,可以先把庫存數放在redis上,然而單一庫存加鎖排隊依然存在,庫存這個熱點數據會成爲扣庫存的瓶頸。

一個解決的辦法是 分庫存,比如總共有50000個秒殺名額,可以分50份,放在redis上的50個不同的key,那麼每份上1000個庫存,用戶進入秒殺流程後隨機到其中一個庫存來修改,這樣有50個庫存數來競爭,縮短請求的排隊時間。

這樣專門爲高併發設計的系統最大的敵人 是低流量,在大部分庫存都好近,而有幾個剩餘庫存時, 用戶會看到明明還能搶卻總是搶不到,而在高併發下,用戶根本就覺察不到。

2.異步消息 如果有必要繼續優化,就是扣庫存和發貨這兩個費時的流程,可以改爲異步,得到秒殺結果後通過短信/push異步通知用戶。 主要是利用消息系統削峯填谷的特性 來增加系統的容量。

秒殺總結

流量圖

先用varnish擋掉了所有的讀取狀態請求 然後用ehcache緩存活動時間,擋掉活動未開始時查詢活動狀態的請求 memcached緩存是否可搶的狀態,擋掉活動開始後到結束狀態的活動查詢請求 redis隊列擋掉了活動進行中,過量的秒殺請求 到最後只留下了秒殺商品數量級的請求到數據庫中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章