秒殺你們都不陌生。自2011年首次出現以來,不管是雙十一購物仍是 12306 搶票,秒殺場景已隨處可見。簡單來講,秒殺就是在同一時刻大量請求爭搶購買同一商品並完成交易的過程。從架構視角來看,秒殺系統本質是一個高性能、高一致、高可用的三高系統。而打造並維護一個超大流量的秒殺系統須要進行哪些關注,就是本文討論的話題。前端
首先從高維度出發,總體思考問題。秒殺無外乎解決兩個核心問題,一是併發讀,一是併發寫,對應到架構設計,就是高可用、一致性和高性能的要求。關於秒殺系統的設計思考,本文即基於此 3 層依次推動,簡述以下——sql
你們可能會注意到,秒殺過程當中你是不須要刷新整個頁面的,只有時間在不停跳動。這是由於通常都會對大流量的秒殺系統作系統的靜態化改造,即數據意義上的動靜分離。動靜分離三步走:一、數據拆分;二、靜態緩存;三、數據整合。數據庫
動靜分離的首要目的是將動態頁面改形成適合緩存的靜態頁面。所以第一步就是分離出動態數據,主要從如下 2 個方面進行:segmentfault
分離出動靜態數據以後,第二步就是將靜態數據進行合理的緩存,由此衍生出兩個問題:一、怎麼緩存;二、哪裏緩存後端
靜態化改造的一個特色是直接緩存整個 HTTP 鏈接而不是僅僅緩存靜態數據,如此一來,Web 代理服務器根據請求 URL,能夠直接取出對應的響應體而後直接返回,響應過程無需重組 HTTP 協議,也無需解析 HTTP 請求頭。而做爲緩存鍵,URL惟一化是必不可少的,只是對於商品系統,URL 自然是能夠基於商品 ID 來進行惟一標識的,好比淘寶的 https://item.taobao.com/item....。瀏覽器
靜態數據緩存到哪裏呢?能夠有三種方式:一、瀏覽器;二、CDN ;三、服務端。緩存
瀏覽器固然是第一選擇,但用戶的瀏覽器是不可控的,主要體如今若是用戶不主動刷新,系統很難主動地把消息推送給用戶(注意,當討論靜態數據時,潛臺詞是 「相對不變」,言外之意是 「可能會變」),如此可能會致使用戶端在很長一段時間內看到的信息都是錯誤的。對於秒殺系統,保證緩存能夠在秒級時間內失效是不可或缺的。安全
服務端主要進行動態邏輯計算及加載,自己並不擅長處理大量鏈接,每一個鏈接消耗內存較多,同時 Servlet 容器解析 HTTP 較慢,容易侵佔邏輯計算資源;另外,靜態數據下沉至此也會拉長請求路徑。性能優化
所以一般將靜態數據緩存在 CDN,其自己更擅長處理大併發的靜態文件請求,既能夠作到主動失效,又離用戶儘量近,同時規避 Java 語言層面的弱點。須要注意的是,上 CDN 有如下幾個問題須要解決:服務器
所以,將數據放到全國全部的 CDN 節點是不太現實的,失效問題、命中率問題都會面臨比較大的挑戰。更爲可行的作法是選擇若干 CDN 節點進行靜態化改造,節點的選取一般須要知足如下幾個條件:
基於以上因素,選擇 CDN 的二級緩存比較合適,由於二級緩存數量偏少,容量也更大,訪問量相對集中,這樣就能夠較好解決緩存的失效問題以及命中率問題,是當前比較理想的一種 CDN 化方案。部署方式以下圖所示:
分離出動靜態數據以後,前端如何組織數據頁就是一個新的問題,主要在於動態數據的加載處理,一般有兩種方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。
動靜分離對於性能的提高,抽象起來只有兩點,一是數據要儘可能少,以便減小不必的請求,二是路徑要儘可能短,以便提升單次請求的效率。具體方法其實就是基於這個大方向進行的。
熱點分爲熱點操做和熱點數據,如下分開進行討論。
零點刷新、零點下單、零點添加購物車等都屬於熱點操做。熱點操做是用戶的行爲,很差改變,但能夠作一些限制保護,好比用戶頻繁刷新頁面時進行提示阻斷。
熱點數據的處理三步走,一是熱點識別,二是熱點隔離,三是熱點優化。
熱點數據分爲靜態熱點和動態熱點,具體以下:
所以秒殺系統須要實現熱點數據的動態發現能力,一個常見的實現思路是:
須要注意的是:
熱點數據識別出來以後,第一原則就是將熱點數據隔離出來,不要讓 1% 影響到另外的 99%,能夠基於如下幾個層次實現熱點隔離:
固然,實現隔離還有不少種辦法。好比,能夠按照用戶來區分,爲不一樣的用戶分配不一樣的 Cookie,入口層路由到不一樣的服務接口中;再好比,域名保持一致,但後端調用不一樣的服務接口;又或者在數據層給數據打標進行區分等等,這些措施的目的都是把已經識別的熱點請求和普通請求區分開來。
熱點數據隔離以後,也就方便對這 1% 的請求作針對性的優化,方式無外乎兩種:
數據的熱點優化與動靜分離是不同的,熱點優化是基於二八原則對數據進行了縱向拆分,以便進行鍼對性地處理。熱點識別和隔離不只對「秒殺」這個場景有意義,對其餘的高性能分佈式系統也很是有參考價值。
對於一個軟件系統,提升性能能夠有不少種手段,如提高硬件水平、調優JVM 性能,這裏主要關注代碼層面的性能優化——
性能優化須要一個基準值,因此係統還須要作好應用基線,好比性能基線(什麼時候性能忽然降低)、成本基線(去年大促用了多少機器)、鏈路基線(核心流程發生了哪些變化),經過基線持續關注系統性能,促使系統在代碼層面持續提高編碼質量、業務層面及時下掉不合理調用、架構層面不斷優化改進。
秒殺系統中,庫存是個關鍵數據,賣不出去是個問題,超賣更是個問題。秒殺場景下的一致性問題,主要就是庫存扣減的準確性問題。
電商場景下的購買過程通常分爲兩步:下單和付款。「提交訂單」即爲下單,「支付訂單」即爲付款。基於此設定,減庫存通常有如下幾個方式:
可以看到,減庫存方式是基於購物過程的多階段進行劃分的,但不管是在下單階段仍是付款階段,都會存在一些問題,下面進行具體分析。
優點:用戶體驗最好。下單減庫存是最簡單的減庫存方式,也是控制最精確的一種。下單時能夠直接經過數據庫事務機制控制商品庫存,因此必定不會出現已下單卻付不了款的狀況。
劣勢:可能賣不出去。正常狀況下,買家下單後付款機率很高,因此不會有太大問題。但有一種場景例外,就是當賣家參加某個促銷活動時,競爭對手經過惡意下單的方式將該商品所有下單,致使庫存清零,那麼這就不能正常售賣了——要知道,惡意下單的人是不會真正付款的,這正是 「下單減庫存」 的不足之處。
優點:必定實際售賣。「下單減庫存」 可能致使惡意下單,從而影響賣家的商品銷售, 「付款減庫存」 因爲須要付出真金白銀,能夠有效避免。
劣勢:用戶體驗較差。用戶下單後,不必定會實際付款,假設有 100 件商品,就可能出現 200 人下單成功的狀況,由於下單時不會減庫存,因此也就可能出現下單成功數遠遠超過真正庫存數的狀況,這尤爲會發生在大促的熱門商品上。如此一來就會致使不少買家下單成功後卻付不了款,購物體驗天然是比較差的。
優點:緩解了以上兩種方式的問題。預扣庫存實際就是「下單減庫存」和 「付款減庫存」兩種方式的結合,將兩次操做進行了先後關聯,下單時預扣庫存,付款時釋放庫存。
劣勢:並無完全解決以上問題。好比針對惡意下單的場景,雖然能夠把有效付款時間設置爲 10 分鐘,但惡意買家徹底能夠在 10 分鐘以後再次下單。
減庫存的問題主要體如今用戶體驗和商業訴求兩方面,其本質緣由在於購物過程存在兩步甚至多步操做,在不一樣階段減庫存,容易存在被惡意利用的漏洞。
業界最爲常見的是預扣庫存。不管是外賣點餐仍是電商購物,下單後通常都有個 「有效付款時間」,超過該時間訂單自動釋放,這就是典型的預扣庫存方案。但如上所述,預扣庫存還須要解決惡意下單的問題,保證商品賣的出去;另外一方面,如何避免超賣,也是一個痛點。
sql UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
業務手段保證商品賣的出去,技術手段保證商品不會超賣,庫存問題歷來就不是簡單的技術難題,解決問題的視角是多種多樣的。
庫存是個關鍵數據,更是個熱點數據。對系統來講,熱點的實際影響就是 「高讀」 和 「高寫」,也是秒殺場景下最爲核心的一個技術難題。
秒殺場景解決高併發讀問題,關鍵詞是「分層校驗」。即在讀鏈路時,只進行不影響性能的檢查操做,如用戶是否具備秒殺資格、商品狀態是否正常、用戶答題是否正確、秒殺是否已經結束、是否非法請求等,而不作一致性校驗等容易引起瓶頸的檢查操做;直到寫鏈路時,纔對庫存作一致性檢查,在數據層保證最終準確性。
所以,在分層校驗設定下,系統能夠採用分佈式緩存甚至LocalCache來抵抗高併發讀。即容許讀場景下必定的髒數據,這樣只會致使少許本來無庫存的下單請求被誤認爲是有庫存的,等到真正寫數據時再保證最終一致性,由此作到高可用和一致性之間的平衡。
實際上,分層校驗的核心思想是:不一樣層次儘量過濾掉無效請求,只在「漏斗」 最末端進行有效處理,從而縮短系統瓶頸的影響路徑。
高併發寫的優化方式,一種是更換DB選型,一種是優化DB性能,如下分別進行討論。
秒殺商品和普通商品的減庫存是有差別的,核心區別在數據量級小、交易時間短,所以可否把秒殺減庫存直接放到緩存系統中實現呢,也就是直接在一個帶有持久化功能的緩存中進行減庫存操做,好比 Redis?
若是減庫存邏輯很是單一的話,好比沒有複雜的 SKU 庫存和總庫存這種聯動關係的話,我的認爲是徹底能夠的。但若是有比較複雜的減庫存邏輯,或者須要使用到事務,那就必須在數據庫中完成減庫存操做。
庫存數據落地到數據庫實現實際上是一行存儲(MySQL),所以會有大量線程來競爭 InnoDB 行鎖。但併發越高,等待線程就會越多,TPS 降低,RT 上升,吞吐量會受到嚴重影響——注意,這裏假設數據庫已基於上文【性能優化】完成數據隔離,以便於討論聚焦 。
解決併發鎖的問題,有兩種辦法:
高讀和高寫的兩種處理方式截然不同。讀請求的優化空間要大一些,而寫請求的瓶頸通常都在存儲層,優化思路的本質仍是基於 CAP 理論作平衡。
固然,減庫存還有不少細節問題,例如預扣的庫存超時後如何進行回補,再好比第三方支付如何保證減庫存和付款時的狀態一致性,這些也是很大的挑戰。
盯過秒殺流量監控的話,會發現它不是一條蜿蜒而起的曲線,而是一條挺拔的直線,這是由於秒殺請求高度集中於某一特定的時間點。這樣一來就會形成一個特別高的零點峯值,而對資源的消耗也幾乎是瞬時的。因此秒殺系統的可用性保護是不可或缺的。
對於秒殺的目標場景,最終可以搶到商品的人數是固定的,不管 100 人和 10000 人蔘加結果都是同樣的,即有效請求額度是有限的。併發度越高,無效請求也就越多。但秒殺做爲一種商業營銷手段,活動開始以前是但願有更多的人來刷頁面,只是真正開始後,秒殺請求不是越多越好。所以系統能夠設計一些規則,人爲的延緩秒殺請求,甚至能夠過濾掉一些無效請求。
早期秒殺只是簡單的點擊秒殺按鈕,後來才增長了答題。爲何要增長答題呢?主要是經過提高購買的複雜度,達到兩個目的:
須要注意的是,答題除了作正確性驗證,還須要對提交時間作驗證,好比<1s 人爲操做的可能性就很小,能夠進一步防止機器答題的狀況。
答題目前已經使用的很是廣泛了,本質是經過在入口層削減流量,從而讓系統更好地支撐瞬時峯值。
最爲常見的削峯方案是使用消息隊列,經過把同步的直接調用轉換成異步的間接推送緩衝瞬時流量。除了消息隊列,相似的排隊方案還有不少,例如:
排隊方式的弊端也是顯而易見的,主要有兩點:
排隊本質是在業務層將一步操做轉變成兩步操做,從而起到緩衝的做用,但鑑於此種方式的弊端,最終仍是要基於業務量級和秒殺場景作出妥協和平衡。
過濾的核心結構在於分層,經過在不一樣層次過濾掉無效請求,達到數據讀寫的精準觸發。常見的過濾主要有如下幾層:
過濾的核心目的是經過減小無效請求的數據IO保障有效請求的IO性能。
系統能夠經過入口層的答題、業務層的排隊、數據層的過濾達到流量削峯的目的,本質是在尋求商業訴求與架構性能之間的平衡。另外,新的削峯手段也層出不窮,以業務切入居多,好比零點大促時同步發放優惠券或發起抽獎活動,將一部分流量分散到其餘系統,這樣也能起到削峯的做用。
當一個系統面臨持續的高峯流量時,實際上是很難單靠自身調整來恢復狀態的,平常運維沒有人可以預估全部狀況,意外老是沒法避免。尤爲在秒殺這一場景下,爲了保證系統的高可用,必須設計一個 Plan B 方案來進行兜底。
高可用建設,實際上是一個系統工程,貫穿在系統建設的整個生命週期。
具體來講,系統的高可用建設涉及架構階段、編碼階段、測試階段、發佈階段、運行階段,以及故障發生時,逐一進行分析:
對於平常運維而言,高可用更可能是針對運行階段而言的,此階段須要額外進行增強建設,主要有如下幾種手段:
高可用實際上是在說 「穩定性」,穩定性是一個平時不重要,但出了問題就要命的事情,然而它的落地又是一個問題——平時業務發展良好,穩定性建設就會降級給業務讓路。解決這個問題必須在組織上有所保障,好比讓業務負責人背上穩定性績效指標,同時在部門中創建穩定性建設小組,小組成員由每條線的核心力量兼任,績效由穩定性負責人來打分,這樣就能夠把體系化的建設任務落實到具體的業務系統中了。
一個秒殺系統的設計,能夠根據不一樣級別的流量,由簡單到複雜打造出不一樣的架構,本質是各方面的取捨和權衡。固然,你可能注意到,本文並無涉及具體的選型方案,由於這些對於架構來講並不重要,做爲架構師,應該時刻提醒本身主線是什麼。
同時也在這裏抽象、提煉一下,主要是我的對於秒殺設計的提綱式整理,方便各位同窗進行參考—!