精品一区二区三区在线成人,欧美精产国品一二三区,Ji大巴进入女人66h,亚洲春色在线视频

我們一起聊聊如何設(shè)計一個秒殺系統(tǒng)?

開發(fā) 前端
針對這類場景問題,最常用的是采用“異構(gòu)索引表”的方式解決,即采用異步機(jī)制將原表的每一次創(chuàng)建或更新,都換另一個維度保存一份完整的數(shù)據(jù)表或索引表,拿空間換時間。

動靜分離

對頁面進(jìn)行徹底的動靜分離,使得用戶秒殺時不需要刷新整個頁面,借此把頁面刷新的數(shù)據(jù)降到最少。

用戶看到的數(shù)據(jù)可以分為:靜態(tài)數(shù)據(jù) 和 動態(tài)數(shù)據(jù)。

簡單來說,"動態(tài)數(shù)據(jù)"和"靜態(tài)數(shù)據(jù)"的主要區(qū)別就是看頁面中輸出的數(shù)據(jù)是否和URL、瀏覽者、時間、地域相關(guān),以及是否含有Cookie等私密數(shù)據(jù)。

比如說:

  • 很多媒體類的網(wǎng)站,某一篇文章的內(nèi)容不管是你訪問還是我訪問,它都是一樣的。所以它就是一個典型的靜態(tài)數(shù)據(jù),但是它是個動態(tài)頁面。
  • 我們?nèi)绻F(xiàn)在訪問淘寶的首頁,每個人看到的頁面可能都是不一樣的,淘寶首頁中包含了很多根據(jù)訪問者特征推薦的信息,而這些個性化的數(shù)據(jù)就可以理解為動態(tài)數(shù)據(jù)了。

這里再強(qiáng)調(diào)一下,我們所說的靜態(tài)數(shù)據(jù),不能僅僅理解為傳統(tǒng)意義上完全存在磁盤上的HTML頁面,它也可能是經(jīng)過Java系統(tǒng)產(chǎn)生的頁面,但是它輸出的頁面本身不包含上面所說的那些因素。

也就是所謂"動態(tài)"還是"靜態(tài)",并不是說數(shù)據(jù)本身是否動靜,而是數(shù)據(jù)中是否含有和訪問者相關(guān)的個性化數(shù)據(jù)。

靜態(tài)化改造

靜態(tài)化改造就是要直接緩存 HTTP 連接。

相較于普通的數(shù)據(jù)緩存而言,你肯定還聽過系統(tǒng)的靜態(tài)化改造。靜態(tài)化改造是直接緩存 HTTP 連接而不是僅僅緩存數(shù)據(jù),如下圖所示,Web 代理服務(wù)器根據(jù)請求 URL,直接取出對應(yīng)的 HTTP 響應(yīng)頭和響應(yīng)體然后直接返回,這個響應(yīng)過程簡單得連 HTTP 協(xié)議都不用重新組裝,甚至連 HTTP 請求頭也不需要解析。

圖片圖片

商詳上靜態(tài)化

高并發(fā)時候,商詳頁面是最先受到?jīng)_擊的,通過商詳靜態(tài)化,可以幫助服務(wù)器擋掉99.9%流量。

分類舉例:商品圖片、商品詳細(xì)描述等,所有用戶看到的內(nèi)容都是一樣的,這一類數(shù)據(jù)就可以上靜態(tài)化。

會員折扣、優(yōu)惠券等信息具備個體差異性,就需要放在動態(tài)接接口中,根據(jù)入?yún)⑿畔崟r查詢。

我們從以下 5 個方面來分離出動態(tài)內(nèi)容:

  • URL 唯一化:商品詳情系統(tǒng)天然地就可以做到 URL 唯一化,比如每個商品都由 ID 來標(biāo)識,那么 http://item.xxx.com/item.htm?id=xxxx 就可以作為唯一的 URL 標(biāo)識。為啥要 URL 唯一呢?前面說了我們是要緩存整個 HTTP 連接,那么以什么作為 Key 呢?就以 URL 作為緩存的 Key,例如以 id=xxx 這個格式進(jìn)行區(qū)分。
  • 分離瀏覽者相關(guān)的因素:瀏覽者相關(guān)的因素包括是否已登錄,以及登錄身份等,這些相關(guān)因素我們可以單獨拆分出來,通過動態(tài)請求來獲取。
  • 分離時間因素:服務(wù)端輸出的時間也通過動態(tài)請求獲取。
  • 異步化地域因素:詳情頁面上與地域相關(guān)的因素做成異步方式獲取,當(dāng)然你也可以通過動態(tài)請求方式獲取,只是這里通過異步獲取更合適。
  • 去掉 Cookie:服務(wù)端輸出的頁面包含的 Cookie 可以通過代碼軟件來刪除,如 Web 服務(wù)器 Varnish 可以通過 unset req.http.cookie 命令去掉 Cookie。注意,這里說的去掉 Cookie 并不是用戶端收到的頁面就不含 Cookie 了,而是說,在緩存的靜態(tài)數(shù)據(jù)中不含有 Cookie。

分離出動態(tài)內(nèi)容之后,如何組織這些內(nèi)容頁就變得非常關(guān)鍵了。

動態(tài)內(nèi)容的處理通常有兩種方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。

  • ESI 方案(或者 SSI):即在 Web 代理服務(wù)器上做動態(tài)內(nèi)容請求,并將請求插入到靜態(tài)頁面中,當(dāng)用戶拿到頁面時已經(jīng)是一個完整的頁面了。這種方式對服務(wù)端性能有些影響,但是用戶體驗較好。
  • CSI 方案。即單獨發(fā)起一個異步 JavaScript 請求,以向服務(wù)端獲取動態(tài)內(nèi)容。這種方式服務(wù)端性能更佳,但是用戶端頁面可能會延時,體驗稍差。

CDN

網(wǎng)站應(yīng)用,靜態(tài)資源占流量的多數(shù)。系統(tǒng)做了動靜分離之后,就可以把靜態(tài)資源通過CDN加速。

這樣,靜態(tài)資源的請求大部分通過就近部署的CDN服務(wù)器提供服務(wù),用戶的延遲也會有明顯的提升。網(wǎng)站服務(wù)器專注于服務(wù)動態(tài)流量,帶寬壓力會小很多。

動靜分離,部署時靜態(tài)資源要給一個單獨域名,這個域名是個CNAME,CNAME映射到CDN服務(wù)廠商提供的DNS服務(wù)器,CDN DNS服務(wù)器會根據(jù)請求的IP地址所在區(qū)域和資源內(nèi)容,返回就近的CDN緩存服務(wù)器ip,后續(xù)用戶對這個DNS的請求都會轉(zhuǎn)到這個IP上來。

Tips:CNAME 簡單來講就是給域名起了個別名。

CDN 工作流程大致如下:

圖片圖片

靜態(tài)資源上 CDN 存在以下幾個問題:

  • 失效問題。前面我們也有提到過緩存時效的問題,不知道你有沒有理解,我再來解釋一下。談到靜態(tài)數(shù)據(jù)時,我說過一個關(guān)鍵詞叫“相對不變”,它的言外之意是“可能會變化”。比如一篇文章,現(xiàn)在不變,但如果你發(fā)現(xiàn)個錯別字,是不是就會變化了?如果你的緩存時效很長,那用戶端在很長一段時間內(nèi)看到的都是錯的。所以,這個方案中也是,我們需要保證 CDN 可以在秒級時間內(nèi),讓分布在全國各地的 Cache 同時失效,這對 CDN 的失效系統(tǒng)要求很高。

失效需要一個失效系統(tǒng)來實現(xiàn),一般有主動失效和被動失效。

主動失效需要監(jiān)控數(shù)據(jù)庫數(shù)據(jù)的變化然后轉(zhuǎn)成消息來發(fā)送失效消息,這個實現(xiàn)比較復(fù)雜,阿里有個系統(tǒng)叫metaq,可以網(wǎng)上參考下。

被動失效就是只緩存固定時間,然后到期后自動失效

  • 命中率問題。Cache 最重要的一個衡量指標(biāo)就是“高命中率”,不然 Cache 的存在就失去了意義。同樣,如果將數(shù)據(jù)全部放到全國的 CDN 上,必然導(dǎo)致 Cache 分散,而 Cache 分散又會導(dǎo)致訪問請求命中同一個 Cache 的可能性降低,那么命中率就成為一個問題。
  • 發(fā)布更新問題。如果一個業(yè)務(wù)系統(tǒng)每周都有日常業(yè)務(wù)需要發(fā)布,那么發(fā)布系統(tǒng)必須足夠簡潔高效,而且你還要考慮有問題時快速回滾和排查問題的簡便性。

部署方式如下圖所示:

圖片圖片

你可能會問,存儲在瀏覽器或 CDN 上,有多大區(qū)別?我的回答是:區(qū)別很大!因為在 CDN 上,我們可以做主動失效,而在用戶的瀏覽器里就更不可控,如果用戶不主動刷新的話,你很難主動地把消息推送給用戶的瀏覽器。

秒殺場景 CDN 應(yīng)用

比如,1 元賣 iPhone,100 臺,于是來了一百萬人搶購。

我們把技術(shù)挑戰(zhàn)放在一邊,先從用戶或是產(chǎn)品的角度來看一下,秒殺的流程是什么樣的。

  • 首先,你需要一個秒殺的 landing page,在這個秒殺頁上有一個倒計時的按鈕。
  • 一旦這個倒計時的時間到了,按鈕就被點亮,讓你可以點擊按鈕下單。
  • 一般來說下單時需要你填寫一個校驗碼,以防止是機(jī)器來搶。

從技術(shù)上來說,這個倒計時按鈕上的時間和按鈕可以被點擊的時間是需要后臺服務(wù)器來校準(zhǔn)的,這意味著:

  • 前端頁面要不斷地向后端來請求,開沒開始,開沒開始……
  • 每次詢問的時候,后端都會給前端一個時間,以校準(zhǔn)前端的時間。
  • 一旦后端服務(wù)器表示 OK 可以開始,后端服務(wù)會返回一個 URL。
  • 這個 URL 會被安置在那個按鈕上,就可以點擊了。
  • 點擊后,如果搶到了庫存,就進(jìn)入支付頁面,如果沒有則返回秒殺已結(jié)束。

很明顯,要讓 100 萬用戶能夠在同一時間打開一個頁面,這個時候,我們就需要用到 CDN 了。數(shù)據(jù)中心肯定是扛不住的,所以,我們要引入 CDN。

在 CDN 上,這 100 萬個用戶就會被幾十個甚至上百個 CDN 的邊緣結(jié)點給分擔(dān)了,于是就能夠扛得住。然后,我們還需要在這些 CDN 結(jié)點上做點小文章。

一方面,我們需要把小服務(wù)部署到 CDN 結(jié)點上去,這樣,當(dāng)前端頁面來問開沒開始時,這個小服務(wù)除了告訴前端開沒開始外,它還可以統(tǒng)計下有多少人在線。每個小服務(wù)會把當(dāng)前在線等待秒殺的人數(shù)每隔一段時間就回傳給我們的數(shù)據(jù)中心,于是我們就知道全網(wǎng)總共在線的人數(shù)有多少。

假設(shè),我們知道有大約 100 萬的人在線等著搶,那么,在我們快要開始的時候,由數(shù)據(jù)中心向各個部署在 CDN 結(jié)點上的小服務(wù)上傳遞一個概率值,比如說是 0.02%。

于是,當(dāng)秒殺開始的時候,這 100 萬用戶都在點下單按鈕,首先他們請求到的是 CDN 上的這些服務(wù),這些小服務(wù)按照 0.02% 的量把用戶放到后面的數(shù)據(jù)中心,也就是 1 萬個人放過去兩個,剩下的 9998 個都直接返回秒殺已結(jié)束。于是,100 萬用戶被放過了 0.02% 的用戶,也就是 200 個左右,而這 200 個人在數(shù)據(jù)中心搶那 100 個 iPhone,也就是 200 TPS,這個并發(fā)量怎么都應(yīng)該能扛住了。

熱點緩存

熱點數(shù)據(jù)亦分 靜態(tài)熱點 和 動態(tài)熱點。

所謂"靜態(tài)熱點數(shù)據(jù)",就是能夠提前預(yù)測的熱點數(shù)據(jù)。

例如,我們可以通過賣家報名的方式提前篩選出來,通過報名系統(tǒng)對這些熱點商品進(jìn)行打標(biāo)。另外,我們還可以通過大數(shù)據(jù)分析來提前發(fā)現(xiàn)熱點商品,比如我們分析歷史成交記錄、用戶的購物車記錄,來發(fā)現(xiàn)哪些商品可能更熱門、更好賣,這些都是可以提前分析出來的熱點。

所謂"動態(tài)熱點數(shù)據(jù)",就是不能被提前預(yù)測到的,系統(tǒng)在運(yùn)行過程中臨時產(chǎn)生的熱點。例如,賣家在抖音上做了廣告,然后商品一下就火了,導(dǎo)致它在短時間內(nèi)被大量購買。

靜態(tài)熱點比較好處理,所以秒級內(nèi)自動發(fā)現(xiàn)熱點商品就成為了熱點緩存的關(guān)鍵。

動態(tài)熱點發(fā)現(xiàn)

這里我給出一個動態(tài)熱點發(fā)現(xiàn)系統(tǒng)的具體實現(xiàn):

  1. 構(gòu)建一個異步的系統(tǒng),它可以收集交易鏈路上各個環(huán)節(jié)中的中間件產(chǎn)品的熱點Key,如Nginx、緩存、RPC服務(wù)框架等這些中間件(一些中間件產(chǎn)品本身已經(jīng)有熱點統(tǒng)計模塊)。
  2. 建立一個熱點上報和可以按照需求訂閱的熱點服務(wù)的下發(fā)規(guī)范,主要目的是通過交易鏈路上各個系統(tǒng)(包括詳情、購物車、交易、優(yōu)惠、庫存、物流等)訪問的時間差把上游已經(jīng)發(fā)現(xiàn)的熱點透傳給下游系統(tǒng),提前做好保護(hù)。比如,,對于大促高峰期,詳情系統(tǒng)是最早知道的,在統(tǒng)一接入層上 Nginx 模塊統(tǒng)計的熱點URL。熱點的統(tǒng)計可以很簡單的對訪問的商品進(jìn)行訪問計數(shù),然后排序。還有就是用通常的隊列的淘汰算法如 LRU 等都可以實現(xiàn)。
  3. 將上游系統(tǒng)收集的熱點數(shù)據(jù)發(fā)送到熱點服務(wù)臺,然后下游系統(tǒng)(如交易系統(tǒng))就會知道哪些商品會被頻繁調(diào)用,然后做熱點保護(hù)。

這里我給出了一個圖,其中用戶訪問商品時經(jīng)過的路徑有很多,我們主要是依賴前面的導(dǎo)購頁面(包括首頁、搜索頁面、商品詳情、購物車等)提前識別哪些商品的訪問量高,通過這些系統(tǒng)中的中間件來收集熱點數(shù)據(jù),并記錄到日志中。

圖片圖片

我們通過部署在每臺機(jī)器上的Agent把日志匯總到聚合和分析集群中,然后把符合一定規(guī)則的熱點數(shù)據(jù),通過訂閱分發(fā)系統(tǒng)再推送到相應(yīng)的系統(tǒng)中。你可以是把熱點數(shù)據(jù)填充到Cache中,或者直接推送到應(yīng)用服務(wù)器的內(nèi)存中,還可以對這些數(shù)據(jù)進(jìn)行攔截,總之下游系統(tǒng)可以訂閱這些數(shù)據(jù),然后根據(jù)自己的需求決定如何處理這些數(shù)據(jù)。

熱點發(fā)現(xiàn)要做到接近實時(3s內(nèi)完成熱點數(shù)據(jù)的發(fā)現(xiàn)),因為只有做到接近實時,動態(tài)發(fā)現(xiàn)才有意義,才能實時地對下游系統(tǒng)提供保護(hù)。

對于緩存系統(tǒng)來講,緩存命中率是最重要的指標(biāo),甚至都沒有之一。時間拉的越長,不確定性越多,緩存命中率必然越低。比如如果10s內(nèi)才發(fā)送熱點就沒意義了,因為10s內(nèi)用戶可以進(jìn)行的操作太多了。時間越長,不可控元素越多,熱點緩存命中率越低。

可以參考,京東開源的熱點探測 Hot Key。

可以考慮建立實時熱點發(fā)現(xiàn)系統(tǒng)。

具體步驟如下:

  1. 接入Nginx將請求轉(zhuǎn)發(fā)給應(yīng)用Nginx。
  2. 應(yīng)用Nginx首先該取本地緩存。如果命中,則直接返回,不命中會讀取分布式緩存、回源到Tomcat進(jìn)行處理。
  3. 應(yīng)用Nginx會將請求上報給實時熱點發(fā)現(xiàn)系統(tǒng),如使用UDP直接上報請求,或者將請求寫到本地 kafka,或者使用 flume 訂閱本地Nginx日志。上報給實時熱點發(fā)現(xiàn)系統(tǒng)后,它將進(jìn)行熱點統(tǒng)計(可以考慮storm實時計算)。
  4. 根據(jù)設(shè)置的閾值將熱點數(shù)據(jù)推送到應(yīng)用Nginx本地緩存。

熱點限制

限制更多的是一種保護(hù)機(jī)制,限制的辦法也有很多,例如對被訪問商品的 ID 做一致性 Hash,然后根據(jù) Hash 做分桶,每個分桶設(shè)置一個處理隊列,這樣可以把熱點商品限制在一個請求隊列里,防止因某些熱點商品占用太多的服務(wù)器資源,而使其他請求始終得不到服務(wù)器的處理資源。

多級緩存

使用Java堆內(nèi)存來存儲緩存對象。使用堆緩存的好處是不需要序列化/反序列化,是最快的緩存。缺點也很明顯,當(dāng)緩存的數(shù)據(jù)量很大時,GC(垃圾回收)暫停時間會變長,存儲容量受限于堆空間大小。

一般通過軟引用/弱引用來存儲緩存對象,即當(dāng)堆內(nèi)存不足時,可以強(qiáng)制回收這部分內(nèi)存釋放堆內(nèi)存空間。一般使用堆緩存存儲較熱的數(shù)據(jù)。可以使用Caffeine Cache實現(xiàn)。

現(xiàn)在應(yīng)用最多的是多級緩存方案,就好比 CPU 也有 L1,L2,L3。

Nginx緩存 → 分布式Redis緩存(可以使用Lua腳本直接在Nginx里讀取Redis)→堆內(nèi)存。

整體流程如下:

  1. 接入Nginx將請求負(fù)載均衡到應(yīng)用Nginx,此處常用的負(fù)載均衡算法是輪詢或者一致性哈希。輪詢可以使服務(wù)器的請求更加均衡,而一致性哈希可以提升應(yīng)用Nginx的緩存命中率。
  2. 應(yīng)用Nginx讀取本地緩存(本地緩存可以使用LuaShared Dict、Nginx Proxy Cache(磁盤/內(nèi)存)、LocalRedis實現(xiàn))。如果本地緩存命中,則直接返回,使用應(yīng)用Nginx本地緩存可以提升整體的吞吐量,降低后端壓力,尤其應(yīng)對熱點問題非常有效。
  3. 如果Nginx本地緩存沒命中,則會讀取相應(yīng)的分布式緩存(如Redis緩存,還可以考慮使用主從架構(gòu)來提升性能和吞吐量),如果分布式緩存命中中,則直接返回相應(yīng)數(shù)據(jù)(并回寫到Nginx本地緩存)。
  4. 如果分布式緩存也沒有命中,則會回源到Tomcat集群,在回源到Tomcat集群時,也可以使用輪詢和一致性哈希作為負(fù)載均衡算法。
  5. 在Tomcat應(yīng)用中,首先讀取本地堆緩存。如果有,則直接返回(并會寫到主Redis集群)。
  6. 作為可選部分,如果步驟4沒有命中,則可以再嘗試一次讀主Redis集群操作,目的是防止當(dāng)從集群有問題時的流量沖擊。
  7. 如果所有緩存都沒有命中,則只能查詢DB或相關(guān)服務(wù)獲取相關(guān)數(shù)據(jù)并返回。
  8. 步驟7返回的數(shù)據(jù)異步寫到主Redis集群,此處可能有多個Tomcat實例同時寫。

流量削峰

秒殺答題

添加秒殺答題。有以下兩個目的:

  • 第一個目的是防止部分買家使用秒殺器在參加秒殺時作弊。
  • 第二個目的其實就是延緩請求,起到對請求流量進(jìn)行削峰的作用,從而讓系統(tǒng)能夠更好地支持瞬時的流量高峰。這個重要的功能就是把峰值的下單請求拉長,從以前的 1s 之內(nèi)延長到 2s~10s。這樣一來,請求峰值基于時間分片了。

限流

請求排隊

  • 應(yīng)用層做排隊。按照商品維度設(shè)置隊列順序執(zhí)行,這樣能減少同一臺機(jī)器對數(shù)據(jù)庫同一行記錄進(jìn)行操作的并發(fā)度,同時也能控制單個商品占用數(shù)據(jù)庫連接的數(shù)量,防止熱點商品占用太多的數(shù)據(jù)庫連接。
  • 數(shù)據(jù)庫層做排隊。應(yīng)用層只能做到單機(jī)的排隊,但是應(yīng)用機(jī)器數(shù)本身很多,這種排隊方式控制并發(fā)的能力仍然有限,所以如果能在數(shù)據(jù)庫層做全局排隊是最理想的。阿里的數(shù)據(jù)庫團(tuán)隊開發(fā)了針對這種 MySQL 的 InnoDB 層上的補(bǔ)丁程序(patch),可以在數(shù)據(jù)庫層上對單行記錄做到并發(fā)排隊。

你可能有疑問了,排隊和鎖競爭不都是要等待嗎,有啥區(qū)別?

如果熟悉 MySQL 的話,你會知道 InnoDB 內(nèi)部的死鎖檢測,以及 MySQL Server 和 InnoDB 的切換會比較消耗性能。

對于分布式限流,目前遇到的場景是業(yè)務(wù)上的限流,而不是流量入口的限流。流量入口限流應(yīng)該在接入層完成,而接入層筆者一般使用 Nginx。業(yè)務(wù)的限流一般用Redis + Lua腳本。

庫存扣減

千萬不要超賣,這是大前提。超賣直接導(dǎo)致的就是資損。

庫存扣減方式

在正常的電商平臺購物場景中,用戶的實際購買過程一般分為兩步:下單和付款。你想買一臺 iPhone 手機(jī),在商品頁面點了“立即購買”按鈕,核對信息之后點擊“提交訂單”,這一步稱為下單操作。下單之后,你只有真正完成付款操作才能算真正購買,也就是俗話說的“落袋為安”。

  • 下單減庫存,即當(dāng)買家下單后,在商品的總庫存中減去買家購買數(shù)量。下單減庫存是最簡單的減庫存方式,也是控制最精確的一種,下單時直接通過數(shù)據(jù)庫的事務(wù)機(jī)制控制商品庫存,這樣一定不會出現(xiàn)超賣的情況。但是你要知道,有些人下完單可能并不會付款。
    下單減庫存有多種方式保證不超賣:一種是在應(yīng)用程序中通過事務(wù)來判斷,即保證減后庫存不能為負(fù)數(shù),否則就回滾;另一種辦法是直接設(shè)置數(shù)據(jù)庫的字段數(shù)據(jù)為無符號整數(shù),這樣減后庫存字段值小于零時會直接執(zhí)行 SQL 語句來報錯;再有一種就是使用 CASE WHEN 判斷語句,例如這樣的 SQL 語句:
UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END
  • 付款減庫存,即買家下單后,并不立即減庫存,而是等到有用戶付款后才真正減庫存,否則庫存一直保留給其他買家。但因為付款時才減庫存,如果并發(fā)比較高,有可能出現(xiàn)買家下單后付不了款的情況,因為可能商品已經(jīng)被其他人買走了。
  • 預(yù)扣庫存,這種方式相對復(fù)雜一些,買家下單后,庫存為其保留一定的時間(如10分鐘),超過這個時間,庫存將會自動釋放,釋放后其他買家就可以繼續(xù)購買。在買家付款前,系統(tǒng)會校驗該訂單的庫存是否還有保留:如果沒有保留,則再次嘗試預(yù)扣;如果庫存不 足(也就是預(yù)扣失敗)則不允許繼續(xù)付款;如果預(yù)扣成功,則完成付款并實際地減去庫存。

先說第一種,"下單減庫存",可能導(dǎo)致惡意下單。

正常情況下,買家下單后付款的概率會很高,所以不會有太大問題。但是有一種場景例外,就是當(dāng)賣家參加某個活動時,此時活動的有效時間是商品的黃金售賣時間,如果有競爭對手通過惡意下單的方式將該賣家的商品全部下單(雇幾個人下單將你的商品全都鎖了),讓這款商品的庫存減為零,那么這款商品就不能正常售賣了。要知道,這些惡意下單的人是不會真正付款的,這正是"下單減庫存"方式的不足之處。

既然,從而影響賣家的商品銷售,那么有沒有辦法解決呢?你可能會想,采用"付款減庫存"的方式是不是就可以了?的確可以。但是,"付款減庫存"又會導(dǎo)致另外一個問題:庫存超賣。

假如有 100 件商品,就可能出現(xiàn) 300 人下單成功的情況,因為下單時不會減庫存,所以也就可能出現(xiàn)下單成功數(shù)遠(yuǎn)遠(yuǎn)超過真正庫存數(shù)的情況,這尤其會發(fā)生在做活動的熱門商品上。這樣一來,就會導(dǎo)致很多買家下單成功但是付不了款,買家的購物體驗自然比較差。

超賣情況可以區(qū)別對待:對普通的商品下單數(shù)量超過庫存數(shù)量的情況,可以通過補(bǔ)貨來解決;但是有些賣家完全不允許庫存為負(fù)數(shù)的情況,那只能在買家付款時提示庫存不足。

預(yù)扣庫存方案確實可以在一定程度上緩解上面的問題。但沒有徹底解決,比如針對惡意下單這種情況,雖然把有效的付款時間設(shè)置為 10 分鐘,但是惡意買家完全可以在 10 分鐘后再次下單,或者采用一次下單很多件的方式把庫存減完。針對這種情況,解決辦法還是要結(jié)合安全和反作弊的措施來制止。

例如,給經(jīng)常下單不付款的買家進(jìn)行識別打標(biāo)(可以在被打標(biāo)的買家下單時不減庫存)、給某些類目設(shè)置最大購買件數(shù)(例如,參加活動的商品一人最多只能買 3 件),以及對重復(fù)下單不付款的操作進(jìn)行次數(shù)限制等。

更新操作轉(zhuǎn)化為插入操作

方案的核心思路:將庫存扣減異步化,庫存扣減流程調(diào)整為下單時只記錄扣減明細(xì)(DB記錄插入),異步進(jìn)行真正庫存扣減(更新)。

大量請求對同一數(shù)據(jù)行的的競爭更新,會導(dǎo)致數(shù)據(jù)庫的性能急劇下降,甚至發(fā)生數(shù)據(jù)庫分片的連接被熱點單商品扣減。

前置校驗庫存,從db更換為redis,庫存扣減操作,從更新操作,直接修改為插入操作(性能角度,插入鎖比更新鎖的性能高)

熱點發(fā)現(xiàn)系統(tǒng)(中間件)會通過消息隊列的方式通知應(yīng)用,應(yīng)用對庫存進(jìn)行熱點打標(biāo)。一但庫存不再是熱點(熱點失效),則會進(jìn)行庫存熱點重置。

庫存分段

將商品庫存分開放,分而治之。例如,原來的秒殺商品的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處理請求的性能和并發(fā)量。

隔離

單個熱點商品會影響整個數(shù)據(jù)庫的性能,導(dǎo)致0.01%的商品影響99.99%的商品的售賣,這是我們不愿意看到的情況。一個解決思路是遵循前面介紹的原則進(jìn)行隔離,把熱點商品放到單獨的熱點庫中。但是這無疑會帶來維護(hù)上的麻煩,比如要做熱點數(shù)據(jù)的動態(tài)遷移以及單獨的數(shù)據(jù)庫等。

線程隔離

線程隔離主要是指線程池隔離,在實際使用時,我們會把請求分類,然后交給不同的線程池處理。當(dāng)一種業(yè)務(wù)的請求處理發(fā)生問題時,不會將故障擴(kuò)散到其他線程池,從而保證其他服務(wù)可用。

圖片圖片

隨著對系統(tǒng)可用性的要求,會進(jìn)行多機(jī)房部署,每個機(jī)房的服務(wù)都有自己的服務(wù)分組,本機(jī)房的服務(wù)應(yīng)該只調(diào)用本機(jī)房服務(wù),不進(jìn)行跨機(jī)房調(diào)用。其中,一個機(jī)房服務(wù)發(fā)生問題時,可以通過DNS/負(fù)載均衡將請求全部切到另一個機(jī)房,或者考慮服務(wù)能自動重試其他機(jī)房的服務(wù),從而提升系統(tǒng)可用性。

圖片圖片

核心業(yè)務(wù)以及非核心業(yè)務(wù)可以放在不同的線程池。

可以使用Hystrix來實現(xiàn)線程池隔離。

降級

所謂“降級”,就是當(dāng)系統(tǒng)的容量達(dá)到一定程度時,是為了保證核心服務(wù)的穩(wěn)定而犧牲非核心服務(wù)的做法。

降級方案可以這樣設(shè)計:當(dāng)秒殺流量達(dá)到 5w/s 時,把成交記錄的獲取從展示 20 條降級到只展示 5 條。“從 20 改到 5”這個操作由一個開關(guān)來實現(xiàn),也就是設(shè)置一個能夠從開關(guān)系統(tǒng)動態(tài)獲取的系統(tǒng)參數(shù)。

降級無疑是在系統(tǒng)性能和用戶體驗之間選擇了前者,降級后肯定會影響一部分用戶的體驗,例如在雙 11 零點時,如果優(yōu)惠券系統(tǒng)扛不住,可能會臨時降級商品詳情的優(yōu)惠信息展示,把有限的系統(tǒng)資源用在保障交易系統(tǒng)正確展示優(yōu)惠信息上,即保障用戶真正下單時的價格是正確的。所以降級的核心目標(biāo)是犧牲次要的功能和用戶體驗來保證核心業(yè)務(wù)流程的穩(wěn)定,是一個不得已而為之的舉措。

拒絕服務(wù)

如果限流還不能解決問題,最后一招就是直接拒絕服務(wù)了。

當(dāng)系統(tǒng)負(fù)載達(dá)到一定閾值時,例如 CPU 使用率達(dá)到 90% 或者系統(tǒng) load 值達(dá)到 2*CPU 核數(shù)時,系統(tǒng)直接拒絕所有請求,這種方式是最暴力但也最有效的系統(tǒng)保護(hù)方式。

在最前端的 Nginx 上設(shè)置過載保護(hù),當(dāng)機(jī)器負(fù)載達(dá)到某個值時直接拒絕 HTTP 請求并返回 503 錯誤碼,在 Java 層同樣也可以設(shè)計過載保護(hù)。

負(fù)載均衡

在項目的架構(gòu)中,我們一般會同時部署 LVS 和 Nginx 來做 HTTP 應(yīng)用服務(wù)的負(fù)載均衡。也就是說,在入口處部署 LVS,將流量分發(fā)到多個 Nginx 服務(wù)器上,再由 Nginx 服務(wù)器分發(fā)到應(yīng)用服務(wù)器上。

為什么這么做呢?

主要和 LVS 和 Nginx 的特點有關(guān),LVS 是在網(wǎng)絡(luò)棧的四層做請求包的轉(zhuǎn)發(fā),請求包轉(zhuǎn)發(fā)之后,由客戶端和后端服務(wù)直接建立連接,后續(xù)的響應(yīng)包不會再經(jīng)過 LVS 服務(wù)器,所以相比 Nginx,性能會更高,也能夠承擔(dān)更高的并發(fā)。

可 LVS 缺陷是工作在四層,而請求的URL是七層的概念,不能針對URL做更細(xì)致地請求分發(fā),而且LVS也沒有提供探測后端服務(wù)是否存活的機(jī)制;而Nginx雖然比LVS的性能差很多,但也可以承擔(dān)每秒幾萬次的請求,并且它在配置上更加靈活,還可以感知后端服務(wù)是否出現(xiàn)問題。

因此,LVS適合在入口處,承擔(dān)大流量的請求分發(fā),而Nginx要部在業(yè)務(wù)服務(wù)器之前做更細(xì)維度的請求分發(fā)。

我給你的建議是,如果你的QPS在十萬以內(nèi),那么可以考慮不引入 LVS 而直接使用 Nginx 作為唯一的負(fù)載均衡服務(wù)器,這樣少維護(hù)一個組件,也會減少系統(tǒng)的維護(hù)成本。

但對于Nginx來說,我們要如何保證配置的服務(wù)節(jié)點是可用的呢?

這就要感謝淘寶開源的 Nginx 模塊 nginx_upstream_check_moduule 了,這個模塊可以讓 Nginx 定期地探測后端服務(wù)的一個指定的接口,然后根據(jù)返回的狀態(tài)碼,來判斷服務(wù)是否還存活。當(dāng)探測不存活的次數(shù)達(dá)到一定閾值時,就自動將這個后端服務(wù)從負(fù)載均衡服務(wù)器中摘除。

它的配置樣例如下:

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; //檢測返回狀態(tài)碼為 200 時認(rèn)為檢測成功
}

不過這兩個負(fù)載均衡服務(wù)適用于普通的Web服務(wù),對于微服務(wù)多架構(gòu)來說,它們是不合適的。因為微服務(wù)架構(gòu)中的服務(wù)節(jié)點存儲在注冊中心里,使用 LVS 就很難和注冊中心交互,獲取全量的服務(wù)節(jié)點列表。

另外,一般微服務(wù)架構(gòu)中,使用的是RPC協(xié)議而不是HTTP協(xié)議,所以Nginx也不能滿足要求。

所以,我們會使用另一類的負(fù)載均衡服務(wù),客戶端負(fù)載均衡服務(wù),也就是把負(fù)載均衡的服務(wù)內(nèi)嵌在RPC客戶端中。

DNS負(fù)載均衡

當(dāng)我們的應(yīng)用單實例不能支撐用戶請求時,此時就需要擴(kuò)容,從一臺服務(wù)器擴(kuò)容到兩臺、幾十臺、幾百臺。

然而,用戶訪問時是通過如 http://www.jd.com 的方式訪問,在請求時,瀏覽器首先會查詢DNS服務(wù)器獲取對應(yīng)的IP,然后通過此 IP 訪問對應(yīng)的服務(wù)。

因此,一種方式是 www.jd.com 域名映射多個IP,但是,存在一個最簡單的問題,假設(shè)某臺服務(wù)器重啟或者出現(xiàn)故障,DNS 會有一定的緩存時間,故障后切換時間長,而且沒有對后端服務(wù)進(jìn)行心跳檢查和失敗重試的機(jī)制。

Nginx負(fù)載均衡

對于一般應(yīng)用來說,有Nginx就可以了。但Nginx一般用于七層負(fù)載均衡,其吞吐量是有一定限制的。為了提升整體吞吐量,會在 DNS 和 Nginx之間引入接入層,如使用LVS(軟件負(fù)載均衡器)、F5(硬負(fù)載均衡器)可以做四層負(fù)載均衡,即首先 DNS解析到LVS/F5,然后LVS/F5轉(zhuǎn)發(fā)給Nginx,再由Nginx轉(zhuǎn)發(fā)給后端Real Server。

圖片圖片

對于一般業(yè)務(wù)開發(fā)人員來說,我們只需要關(guān)心到Nginx層面就夠了,LVS/F5一般由系統(tǒng)/運(yùn)維工程師來維護(hù)。Nginx目前提供了HTTP (ngx_http_upstream_module)七層負(fù)載均衡,而1.9.0版本也開始支持TCP(ngx_stream_upstream_module)四層負(fù)載均衡。

一致性hash算法最好在 lua腳本里指定。

Nginx商業(yè)版還提供了 least_time,即基于最小平均響應(yīng)時間進(jìn)行負(fù)載均衡。

Nginx的服務(wù)檢查是惰性的,Nginx只有當(dāng)有訪問時后,才發(fā)起對后端節(jié)點探測。如果本次請求中,節(jié)點正好出現(xiàn)故障,Nginx依然將請求轉(zhuǎn)交給故障的節(jié)點,然后再轉(zhuǎn)交給健康的節(jié)點處理。所以不會影響到這次請求的正常進(jìn)行。但是會影響效率,因為多了一次轉(zhuǎn)發(fā),而且自帶模塊無法做到預(yù)警。

  • Nginx服務(wù)器是服務(wù)端的負(fù)載均衡,而分布式服務(wù)實現(xiàn)是客戶端的負(fù)載均衡。
  • Nginx是集中式的負(fù)載均衡,分布式服務(wù)是消費者內(nèi)部線程實現(xiàn)的負(fù)載均衡。

數(shù)據(jù)異構(gòu)

比如對于訂單庫,當(dāng)對其分庫分表后,如果想按照商家維度或者按照用戶維度進(jìn)行查詢,那么是非常困難的,因此可以通過異構(gòu)數(shù)據(jù)庫來解決這個問題。可以采用下圖的架構(gòu)。

圖片圖片

異構(gòu)數(shù)據(jù)主要存儲數(shù)據(jù)之間的關(guān)系,然后通過查詢源庫查詢實際數(shù)據(jù)。不過,有時可以通過數(shù)據(jù)冗余存儲來減少源庫查詢量或者提升查詢性能。

針對這類場景問題,最常用的是采用“異構(gòu)索引表”的方式解決,即采用異步機(jī)制將原表的每一次創(chuàng)建或更新,都換另一個維度保存一份完整的數(shù)據(jù)表或索引表,拿空間換時間。

也就是應(yīng)用在插入或更新一條訂單ID為分庫分表鍵的訂單數(shù)據(jù)時,也會再保存一份按照買家ID為分庫分表鍵的訂單索引數(shù)據(jù),其結(jié)果就是同一買家的所有訂單索引表都保存在同一數(shù)據(jù)庫中,這就是給訂單創(chuàng)建了異構(gòu)索引表。

責(zé)任編輯:武曉燕 來源: Java隨想錄
相關(guān)推薦

2024-08-02 09:49:35

Spring流程Tomcat

2022-01-04 12:08:46

設(shè)計接口

2024-10-29 11:19:23

點贊系統(tǒng)同步

2024-09-04 08:55:56

2022-09-22 08:06:29

計算機(jī)平板微信

2024-07-12 08:28:09

聊天系統(tǒng)架構(gòu)

2024-10-15 08:08:13

2024-07-03 08:36:14

序列化算法設(shè)計模式

2021-08-27 07:06:10

IOJava抽象

2024-02-20 21:34:16

循環(huán)GolangGo

2023-06-30 08:18:51

敏捷開發(fā)模式

2023-09-10 21:42:31

2023-08-10 08:28:46

網(wǎng)絡(luò)編程通信

2022-05-24 08:21:16

數(shù)據(jù)安全API

2023-08-04 08:20:56

DockerfileDocker工具

2023-04-03 00:09:13

2024-09-09 00:00:00

編寫技術(shù)文檔

2024-11-27 16:07:45

2024-09-30 09:33:31

2023-11-30 07:40:05

URLCMS
點贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 焉耆| 赤壁市| 宜兴市| 广元市| 辰溪县| 西宁市| 鄂托克前旗| 思南县| 克什克腾旗| 兰考县| 兴安盟| 东乡县| 刚察县| 香港| 中江县| 永康市| 镇平县| 波密县| 安陆市| 隆尧县| 简阳市| 武城县| 清苑县| 鹤峰县| 金昌市| 射阳县| 略阳县| 铁力市| 大兴区| 尚志市| 永平县| 宁南县| 隆林| 北流市| 织金县| 昭觉县| 平度市| 遂川县| 柞水县| 丹东市| 通渭县|