一個大型網站應用通常都是從最初小規模網站甚至是單機應用發展而來的,爲了讓系統可以支持足夠大的業務量,從前端到後端也採用了各類各樣技術,前端靜態資源壓縮整合、使用CDN、分佈式SOA架構、緩存、數據庫加索引、讀寫分離等等。 這些技術是高併發系統所必須的,可是今天先不細說,而先談談在這些架構既定的狀況下,一些高併發業務/接口實現時應該注意的原則,以及經過工做中一個6萬QPS的秒殺活動,來介紹一下秒殺業務的特色以及如何優化。前端
高併發的接口/系統有一個共同的特性,那就是」快」。
在系統其它條件既定的狀況下,系統處理請求越快,用戶獲得反饋的時間就越短,單位時間內服務器可以處理請求的數量就會越多。因此」快」幾乎能夠算是高併發系統的要知足的必要條件,要評估一個系統性能如何,某次優化是否提升系統的容量,」快」是一個很直觀的衡量標準。mysql
那麼,如何才能作得快呢?有兩個須要注意的原則
1. 作得少,一方面是指在功能特性上有所爲,有所不爲,另外一方面是指一次處理的信息量要少。
2. 作得巧,根據業務自身的特色,選擇合理的業務實現方式,選擇合理的緩存類型和緩存調用時機。git
世界上最快的程序,是什麼都不作的程序。
一個接口負責的功能越少,讀取信息量越少,速度越快。github
對於一個須要承受高併發的接口,在功能上,儘可能不涉及一些難以緩存和預熱的數據。 一個典型的例子,用戶維度個性化的數據,用戶和用戶的信息不一樣,userId數量又不少,即便加上緩存,緩存命中率依然很低,壓力仍是會打到數據庫,不光接口快不了,高併發的sql也會給數據庫帶來風險。web
舉一個例子,在點評電影早期的秒殺活動頁上,展現了一個用戶當前秒殺資格的信息,因爲不一樣用戶搶到秒殺資格的時間、優惠不一樣,每次都須要讀數據庫的來取,也就是每一個用戶進入主頁都會產生一條sql。
還有一個例子,通常電商搞大促的時候,好比同時有多個優惠活動能夠下降商品的價格,而通常只展現最低價的優惠,同時用戶一個優惠只能參與一次,這樣不一樣用戶參與了不一樣活動以後能夠享受的最低價就會隨之改變,若是要在商品頁面上展現這個動態價格,就免不了取到各個用戶參加這些在線優惠的信息。ajax
若是遇到這樣的數據,要怎麼解決呢?
一個辦法是嘗試轉移數據的維度:剛纔說的秒殺活動資格信息,若是以用戶userId爲key,會出現緩存命中率低,仍要sql讀的狀況,可是可以秒到的用戶數量其實不多,因此若是以此次秒殺活動id爲key,存儲一個成功秒到用戶的userid的list,就可以解決緩存命中率低的問題。redis
還有一個辦法是能夠把這些須要個性化數據的功能在業務流程上後移,流量漏斗,越日後流量越少,建立訂單級的sql查詢是可接受的。 剛纔說的第二個例子,商品最優惠的價格,能夠排除用戶相關信息,只在商品列表/詳情上展現只和優惠相關的最低價,而在提交訂單的時候才真正去取用戶參加活動狀況,若是用戶已經參加過給出提示並選擇次優的優惠。商品的列表/詳情頁都在用戶路徑上相對靠前的位置,排除了用戶個性化信息可讓商品列表/詳情更容易緩存,響應速度更快,系統可承受的高併發量更高。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機器直接返回,而不會給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隊列擋掉了活動進行中,過量的秒殺請求 到最後只留下了秒殺商品數量級的請求到數據庫中。
文章歡迎轉載,轉載時請保留做者與原文連接
做者:趙軒辰
本文原文地址:http://zxcpro.github.io/blog/2015/07/27/gao-bing-fa-miao-sha-xi-tong-de-she-ji/