對頁面進行徹底的動靜分離,使得用戶秒殺時不需要刷新整個頁面,借此把頁面刷新的數據降到最少。
用戶看到的數據可以分為:靜態數據 和 動態數據。
簡單來說,"動態數據"和"靜態數據"的主要區別就是看頁面中輸出的數據是否和URL、瀏覽者、時間、地域相關,以及是否含有Cookie等私密數據。
比如說:
這里再強調一下,我們所說的靜態數據,不能僅僅理解為傳統意義上完全存在磁盤上的HTML頁面,它也可能是經過Java系統產生的頁面,但是它輸出的頁面本身不包含上面所說的那些因素。
也就是所謂"動態"還是"靜態",并不是說數據本身是否動靜,而是數據中是否含有和訪問者相關的個性化數據。
靜態化改造就是要直接緩存 HTTP 連接。
相較于普通的數據緩存而言,你肯定還聽過系統的靜態化改造。靜態化改造是直接緩存 HTTP 連接而不是僅僅緩存數據,如下圖所示,Web 代理服務器根據請求 URL,直接取出對應的 HTTP 響應頭和響應體然后直接返回,這個響應過程簡單得連 HTTP 協議都不用重新組裝,甚至連 HTTP 請求頭也不需要解析。
圖片
高并發時候,商詳頁面是最先受到沖擊的,通過商詳靜態化,可以幫助服務器擋掉99.9%流量。
分類舉例:商品圖片、商品詳細描述等,所有用戶看到的內容都是一樣的,這一類數據就可以上靜態化。
會員折扣、優惠券等信息具備個體差異性,就需要放在動態接接口中,根據入參信息實時查詢。
我們從以下 5 個方面來分離出動態內容:
分離出動態內容之后,如何組織這些內容頁就變得非常關鍵了。
動態內容的處理通常有兩種方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。
網站應用,靜態資源占流量的多數。系統做了動靜分離之后,就可以把靜態資源通過CDN加速。
這樣,靜態資源的請求大部分通過就近部署的CDN服務器提供服務,用戶的延遲也會有明顯的提升。網站服務器專注于服務動態流量,帶寬壓力會小很多。
動靜分離,部署時靜態資源要給一個單獨域名,這個域名是個CNAME,CNAME映射到CDN服務廠商提供的DNS服務器,CDN DNS服務器會根據請求的IP地址所在區域和資源內容,返回就近的CDN緩存服務器ip,后續用戶對這個DNS的請求都會轉到這個IP上來。
Tips:CNAME 簡單來講就是給域名起了個別名。
CDN 工作流程大致如下:
圖片
靜態資源上 CDN 存在以下幾個問題:
失效需要一個失效系統來實現,一般有主動失效和被動失效。
主動失效需要監控數據庫數據的變化然后轉成消息來發送失效消息,這個實現比較復雜,阿里有個系統叫metaq,可以網上參考下。
被動失效就是只緩存固定時間,然后到期后自動失效
部署方式如下圖所示:
圖片
你可能會問,存儲在瀏覽器或 CDN 上,有多大區別?我的回答是:區別很大!因為在 CDN 上,我們可以做主動失效,而在用戶的瀏覽器里就更不可控,如果用戶不主動刷新的話,你很難主動地把消息推送給用戶的瀏覽器。
比如,1 元賣 iPhone,100 臺,于是來了一百萬人搶購。
我們把技術挑戰放在一邊,先從用戶或是產品的角度來看一下,秒殺的流程是什么樣的。
從技術上來說,這個倒計時按鈕上的時間和按鈕可以被點擊的時間是需要后臺服務器來校準的,這意味著:
很明顯,要讓 100 萬用戶能夠在同一時間打開一個頁面,這個時候,我們就需要用到 CDN 了。數據中心肯定是扛不住的,所以,我們要引入 CDN。
在 CDN 上,這 100 萬個用戶就會被幾十個甚至上百個 CDN 的邊緣結點給分擔了,于是就能夠扛得住。然后,我們還需要在這些 CDN 結點上做點小文章。
一方面,我們需要把小服務部署到 CDN 結點上去,這樣,當前端頁面來問開沒開始時,這個小服務除了告訴前端開沒開始外,它還可以統計下有多少人在線。每個小服務會把當前在線等待秒殺的人數每隔一段時間就回傳給我們的數據中心,于是我們就知道全網總共在線的人數有多少。
假設,我們知道有大約 100 萬的人在線等著搶,那么,在我們快要開始的時候,由數據中心向各個部署在 CDN 結點上的小服務上傳遞一個概率值,比如說是 0.02%。
于是,當秒殺開始的時候,這 100 萬用戶都在點下單按鈕,首先他們請求到的是 CDN 上的這些服務,這些小服務按照 0.02% 的量把用戶放到后面的數據中心,也就是 1 萬個人放過去兩個,剩下的 9998 個都直接返回秒殺已結束。于是,100 萬用戶被放過了 0.02% 的用戶,也就是 200 個左右,而這 200 個人在數據中心搶那 100 個 iPhone,也就是 200 TPS,這個并發量怎么都應該能扛住了。
熱點數據亦分 靜態熱點 和 動態熱點。
所謂"靜態熱點數據",就是能夠提前預測的熱點數據。
例如,我們可以通過賣家報名的方式提前篩選出來,通過報名系統對這些熱點商品進行打標。另外,我們還可以通過大數據分析來提前發現熱點商品,比如我們分析歷史成交記錄、用戶的購物車記錄,來發現哪些商品可能更熱門、更好賣,這些都是可以提前分析出來的熱點。
所謂"動態熱點數據",就是不能被提前預測到的,系統在運行過程中臨時產生的熱點。例如,賣家在抖音上做了廣告,然后商品一下就火了,導致它在短時間內被大量購買。
靜態熱點比較好處理,所以秒級內自動發現熱點商品就成為了熱點緩存的關鍵。
這里我給出一個動態熱點發現系統的具體實現:
這里我給出了一個圖,其中用戶訪問商品時經過的路徑有很多,我們主要是依賴前面的導購頁面(包括首頁、搜索頁面、商品詳情、購物車等)提前識別哪些商品的訪問量高,通過這些系統中的中間件來收集熱點數據,并記錄到日志中。
圖片
我們通過部署在每臺機器上的Agent把日志匯總到聚合和分析集群中,然后把符合一定規則的熱點數據,通過訂閱分發系統再推送到相應的系統中。你可以是把熱點數據填充到Cache中,或者直接推送到應用服務器的內存中,還可以對這些數據進行攔截,總之下游系統可以訂閱這些數據,然后根據自己的需求決定如何處理這些數據。
熱點發現要做到接近實時(3s內完成熱點數據的發現),因為只有做到接近實時,動態發現才有意義,才能實時地對下游系統提供保護。
對于緩存系統來講,緩存命中率是最重要的指標,甚至都沒有之一。時間拉的越長,不確定性越多,緩存命中率必然越低。比如如果10s內才發送熱點就沒意義了,因為10s內用戶可以進行的操作太多了。時間越長,不可控元素越多,熱點緩存命中率越低。
可以參考,京東開源的熱點探測 Hot Key。
可以考慮建立實時熱點發現系統。
具體步驟如下:
限制更多的是一種保護機制,限制的辦法也有很多,例如對被訪問商品的 ID 做一致性 Hash,然后根據 Hash 做分桶,每個分桶設置一個處理隊列,這樣可以把熱點商品限制在一個請求隊列里,防止因某些熱點商品占用太多的服務器資源,而使其他請求始終得不到服務器的處理資源。
使用Java堆內存來存儲緩存對象。使用堆緩存的好處是不需要序列化/反序列化,是最快的緩存。缺點也很明顯,當緩存的數據量很大時,GC(垃圾回收)暫停時間會變長,存儲容量受限于堆空間大小。
一般通過軟引用/弱引用來存儲緩存對象,即當堆內存不足時,可以強制回收這部分內存釋放堆內存空間。一般使用堆緩存存儲較熱的數據。可以使用Caffeine Cache實現。
現在應用最多的是多級緩存方案,就好比 CPU 也有 L1,L2,L3。
Nginx緩存 → 分布式Redis緩存(可以使用Lua腳本直接在Nginx里讀取Redis)→堆內存。
整體流程如下:
添加秒殺答題。有以下兩個目的:
你可能有疑問了,排隊和鎖競爭不都是要等待嗎,有啥區別?
如果熟悉 MySQL 的話,你會知道 InnoDB 內部的死鎖檢測,以及 MySQL Server 和 InnoDB 的切換會比較消耗性能。
對于分布式限流,目前遇到的場景是業務上的限流,而不是流量入口的限流。流量入口限流應該在接入層完成,而接入層筆者一般使用 Nginx。業務的限流一般用Redis + Lua腳本。
千萬不要超賣,這是大前提。超賣直接導致的就是資損。
在正常的電商平臺購物場景中,用戶的實際購買過程一般分為兩步:下單和付款。你想買一臺 iPhone 手機,在商品頁面點了“立即購買”按鈕,核對信息之后點擊“提交訂單”,這一步稱為下單操作。下單之后,你只有真正完成付款操作才能算真正購買,也就是俗話說的“落袋為安”。
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
先說第一種,"下單減庫存",可能導致惡意下單。
正常情況下,買家下單后付款的概率會很高,所以不會有太大問題。但是有一種場景例外,就是當賣家參加某個活動時,此時活動的有效時間是商品的黃金售賣時間,如果有競爭對手通過惡意下單的方式將該賣家的商品全部下單(雇幾個人下單將你的商品全都鎖了),讓這款商品的庫存減為零,那么這款商品就不能正常售賣了。要知道,這些惡意下單的人是不會真正付款的,這正是"下單減庫存"方式的不足之處。
既然,從而影響賣家的商品銷售,那么有沒有辦法解決呢?你可能會想,采用"付款減庫存"的方式是不是就可以了?的確可以。但是,"付款減庫存"又會導致另外一個問題:庫存超賣。
假如有 100 件商品,就可能出現 300 人下單成功的情況,因為下單時不會減庫存,所以也就可能出現下單成功數遠遠超過真正庫存數的情況,這尤其會發生在做活動的熱門商品上。這樣一來,就會導致很多買家下單成功但是付不了款,買家的購物體驗自然比較差。
超賣情況可以區別對待:對普通的商品下單數量超過庫存數量的情況,可以通過補貨來解決;但是有些賣家完全不允許庫存為負數的情況,那只能在買家付款時提示庫存不足。
預扣庫存方案確實可以在一定程度上緩解上面的問題。但沒有徹底解決,比如針對惡意下單這種情況,雖然把有效的付款時間設置為 10 分鐘,但是惡意買家完全可以在 10 分鐘后再次下單,或者采用一次下單很多件的方式把庫存減完。針對這種情況,解決辦法還是要結合安全和反作弊的措施來制止。
例如,給經常下單不付款的買家進行識別打標(可以在被打標的買家下單時不減庫存)、給某些類目設置最大購買件數(例如,參加活動的商品一人最多只能買 3 件),以及對重復下單不付款的操作進行次數限制等。
方案的核心思路:將庫存扣減異步化,庫存扣減流程調整為下單時只記錄扣減明細(DB記錄插入),異步進行真正庫存扣減(更新)。
大量請求對同一數據行的的競爭更新,會導致數據庫的性能急劇下降,甚至發生數據庫分片的連接被熱點單商品扣減。
前置校驗庫存,從db更換為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)。將key分散到redis的不同槽位中,這就能夠提升Redis處理請求的性能和并發量。
單個熱點商品會影響整個數據庫的性能,導致0.01%的商品影響99.99%的商品的售賣,這是我們不愿意看到的情況。一個解決思路是遵循前面介紹的原則進行隔離,把熱點商品放到單獨的熱點庫中。但是這無疑會帶來維護上的麻煩,比如要做熱點數據的動態遷移以及單獨的數據庫等。
線程隔離主要是指線程池隔離,在實際使用時,我們會把請求分類,然后交給不同的線程池處理。當一種業務的請求處理發生問題時,不會將故障擴散到其他線程池,從而保證其他服務可用。
圖片
隨著對系統可用性的要求,會進行多機房部署,每個機房的服務都有自己的服務分組,本機房的服務應該只調用本機房服務,不進行跨機房調用。其中,一個機房服務發生問題時,可以通過DNS/負載均衡將請求全部切到另一個機房,或者考慮服務能自動重試其他機房的服務,從而提升系統可用性。
圖片
核心業務以及非核心業務可以放在不同的線程池。
可以使用Hystrix來實現線程池隔離。
所謂“降級”,就是當系統的容量達到一定程度時,是為了保證核心服務的穩定而犧牲非核心服務的做法。
降級方案可以這樣設計:當秒殺流量達到 5w/s 時,把成交記錄的獲取從展示 20 條降級到只展示 5 條。“從 20 改到 5”這個操作由一個開關來實現,也就是設置一個能夠從開關系統動態獲取的系統參數。
降級無疑是在系統性能和用戶體驗之間選擇了前者,降級后肯定會影響一部分用戶的體驗,例如在雙 11 零點時,如果優惠券系統扛不住,可能會臨時降級商品詳情的優惠信息展示,把有限的系統資源用在保障交易系統正確展示優惠信息上,即保障用戶真正下單時的價格是正確的。所以降級的核心目標是犧牲次要的功能和用戶體驗來保證核心業務流程的穩定,是一個不得已而為之的舉措。
如果限流還不能解決問題,最后一招就是直接拒絕服務了。
當系統負載達到一定閾值時,例如 CPU 使用率達到 90% 或者系統 load 值達到 2*CPU 核數時,系統直接拒絕所有請求,這種方式是最暴力但也最有效的系統保護方式。
在最前端的 Nginx 上設置過載保護,當機器負載達到某個值時直接拒絕 HTTP 請求并返回 503 錯誤碼,在 Java 層同樣也可以設計過載保護。
在項目的架構中,我們一般會同時部署 LVS 和 Nginx 來做 HTTP 應用服務的負載均衡。也就是說,在入口處部署 LVS,將流量分發到多個 Nginx 服務器上,再由 Nginx 服務器分發到應用服務器上。
為什么這么做呢?
主要和 LVS 和 Nginx 的特點有關,LVS 是在網絡棧的四層做請求包的轉發,請求包轉發之后,由客戶端和后端服務直接建立連接,后續的響應包不會再經過 LVS 服務器,所以相比 Nginx,性能會更高,也能夠承擔更高的并發。
可 LVS 缺陷是工作在四層,而請求的URL是七層的概念,不能針對URL做更細致地請求分發,而且LVS也沒有提供探測后端服務是否存活的機制;而Nginx雖然比LVS的性能差很多,但也可以承擔每秒幾萬次的請求,并且它在配置上更加靈活,還可以感知后端服務是否出現問題。
因此,LVS適合在入口處,承擔大流量的請求分發,而Nginx要部在業務服務器之前做更細維度的請求分發。
我給你的建議是,如果你的QPS在十萬以內,那么可以考慮不引入 LVS 而直接使用 Nginx 作為唯一的負載均衡服務器,這樣少維護一個組件,也會減少系統的維護成本。
但對于Nginx來說,我們要如何保證配置的服務節點是可用的呢?
這就要感謝淘寶開源的 Nginx 模塊 nginx_upstream_check_moduule 了,這個模塊可以讓 Nginx 定期地探測后端服務的一個指定的接口,然后根據返回的狀態碼,來判斷服務是否還存活。當探測不存活的次數達到一定閾值時,就自動將這個后端服務從負載均衡服務器中摘除。
它的配置樣例如下:
upstream server { server 192.168.1.1:8080; server 192.168.1.2:8080; check interval=3000 rise=2 fall=5 timeout=1000 type=http default_down=true check_http_send "GET /health_check HTTP/1.0/r/n/n/n/n"; //檢測URL check_http_expect_alivehttp_2xx; //檢測返回狀態碼為 200 時認為檢測成功}
不過這兩個負載均衡服務適用于普通的Web服務,對于微服務多架構來說,它們是不合適的。因為微服務架構中的服務節點存儲在注冊中心里,使用 LVS 就很難和注冊中心交互,獲取全量的服務節點列表。
另外,一般微服務架構中,使用的是RPC協議而不是HTTP協議,所以Nginx也不能滿足要求。
所以,我們會使用另一類的負載均衡服務,客戶端負載均衡服務,也就是把負載均衡的服務內嵌在RPC客戶端中。
當我們的應用單實例不能支撐用戶請求時,此時就需要擴容,從一臺服務器擴容到兩臺、幾十臺、幾百臺。
然而,用戶訪問時是通過如 http://www.jd.com 的方式訪問,在請求時,瀏覽器首先會查詢DNS服務器獲取對應的IP,然后通過此 IP 訪問對應的服務。
因此,一種方式是 www.jd.com 域名映射多個IP,但是,存在一個最簡單的問題,假設某臺服務器重啟或者出現故障,DNS 會有一定的緩存時間,故障后切換時間長,而且沒有對后端服務進行心跳檢查和失敗重試的機制。
對于一般應用來說,有Nginx就可以了。但Nginx一般用于七層負載均衡,其吞吐量是有一定限制的。為了提升整體吞吐量,會在 DNS 和 Nginx之間引入接入層,如使用LVS(軟件負載均衡器)、F5(硬負載均衡器)可以做四層負載均衡,即首先 DNS解析到LVS/F5,然后LVS/F5轉發給Nginx,再由Nginx轉發給后端Real Server。
圖片
對于一般業務開發人員來說,我們只需要關心到Nginx層面就夠了,LVS/F5一般由系統/運維工程師來維護。Nginx目前提供了HTTP (ngx_http_upstream_module)七層負載均衡,而1.9.0版本也開始支持TCP(ngx_stream_upstream_module)四層負載均衡。
一致性hash算法最好在 lua腳本里指定。
Nginx商業版還提供了 least_time,即基于最小平均響應時間進行負載均衡。
Nginx的服務檢查是惰性的,Nginx只有當有訪問時后,才發起對后端節點探測。如果本次請求中,節點正好出現故障,Nginx依然將請求轉交給故障的節點,然后再轉交給健康的節點處理。所以不會影響到這次請求的正常進行。但是會影響效率,因為多了一次轉發,而且自帶模塊無法做到預警。
比如對于訂單庫,當對其分庫分表后,如果想按照商家維度或者按照用戶維度進行查詢,那么是非常困難的,因此可以通過異構數據庫來解決這個問題。可以采用下圖的架構。
圖片
異構數據主要存儲數據之間的關系,然后通過查詢源庫查詢實際數據。不過,有時可以通過數據冗余存儲來減少源庫查詢量或者提升查詢性能。
針對這類場景問題,最常用的是采用“異構索引表”的方式解決,即采用異步機制將原表的每一次創建或更新,都換另一個維度保存一份完整的數據表或索引表,拿空間換時間。
也就是應用在插入或更新一條訂單ID為分庫分表鍵的訂單數據時,也會再保存一份按照買家ID為分庫分表鍵的訂單索引數據,其結果就是同一買家的所有訂單索引表都保存在同一數據庫中,這就是給訂單創建了異構索引表。
本文鏈接:http://www.tebozhan.com/showinfo-26-94282-0.html我們一起聊聊如何設計一個秒殺系統?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 探索Word文檔導入導出的前端實現方案