面試官問我:如何設(shè)計(jì)一個(gè)秒殺場景?
前段時(shí)間在公眾號讀者交流群,有讀者提問到關(guān)于并發(fā)場景相關(guān)的問題:
從讀者的描述,可以看出高并發(fā)處理的經(jīng)驗(yàn),在面試中占據(jù)著舉足輕重的地位,關(guān)于高并發(fā)相關(guān)的面試題,一直都是面試熱題,因?yàn)檫@類面試題能夠更加直觀地體現(xiàn)候選人的技術(shù)水平與深度。如何解決高并發(fā)場景下的問題,永遠(yuǎn)都不會過時(shí)。
在之前的工作經(jīng)歷中,我做過營銷相關(guān)項(xiàng)目,接觸過關(guān)于票券秒殺的高并發(fā)場景,秒殺場景也算是最熱門的高并發(fā)場景之一了。
下面我就把我對秒殺場景的一些理解簡單寫下來,僅供大家參考,歡迎留言糾錯(cuò)或者補(bǔ)充。
核心要素
何為高并發(fā)?
高并發(fā)指的是在同一時(shí)刻,有大量用戶的請求同時(shí)到達(dá)服務(wù)器,而服務(wù)器需要在有限的資源內(nèi)處理這些請求,并盡可能快地響應(yīng)用戶請求。
在秒殺場景中,我們需要從在大量并發(fā)請求過程中提升服務(wù)器的處理性能,在處理過程中數(shù)據(jù)處理不能存錯(cuò),同時(shí)在整個(gè)秒殺鏈路中需要滿足高可用性,即在秒殺過程中,服務(wù)不能突然掉鏈子,需要滿足秒殺場景活動生命周期的完成。
我們可以總結(jié)出秒殺場景中有三個(gè)核心要素:
- 高性能;
- 一致性;
- 高可用性。
如何提高性能?
秒殺場景核心的問題是如何解決海量請求帶來的性能問題,那么我們?nèi)绾卧谟邢薜馁Y源下,盡最大的限度去提高服務(wù)器訪問性能?按照我以往的經(jīng)驗(yàn),我大致總結(jié)有這幾點(diǎn):熱點(diǎn)數(shù)據(jù)處理、流量削峰、資源隔離、服務(wù)器優(yōu)化。
熱點(diǎn)數(shù)據(jù)處理
1、什么是熱點(diǎn)數(shù)據(jù)?
我理解的熱點(diǎn)數(shù)據(jù)指的是用戶請求量非常高的那些數(shù)據(jù),在秒殺場景中,熱點(diǎn)數(shù)據(jù)就是那些要被秒殺的商品數(shù)據(jù)。
這些熱點(diǎn)請求會大量占用服務(wù)器的資源,如果不對這些數(shù)據(jù)進(jìn)行處理,那么會嚴(yán)重占用資源,進(jìn)而影響系統(tǒng)的性能,導(dǎo)致其他業(yè)務(wù)也受影響。
熱點(diǎn)數(shù)據(jù)又可以分為“靜態(tài)熱點(diǎn)數(shù)據(jù)”和“動態(tài)熱點(diǎn)數(shù)據(jù)”。
2、靜態(tài)熱點(diǎn)數(shù)據(jù)
靜態(tài)熱點(diǎn)數(shù)據(jù)指的是可以提前預(yù)知的熱點(diǎn)數(shù)據(jù),比如本文所說的秒殺場景,需要參與本次秒殺的商家提前報(bào)名,并將秒殺的商品錄入熱點(diǎn)分析系統(tǒng)中。業(yè)務(wù)系統(tǒng)通過這次提前錄入的熱點(diǎn)數(shù)據(jù),進(jìn)行預(yù)加載,甚至可以將數(shù)據(jù)放入本地緩存中,這樣做的好處可以有效緩解避緩存集群的壓力,避免流量集中時(shí)壓垮緩存集群。
可能有人會問如何更新本地緩存?
我的做法是將熱點(diǎn)數(shù)據(jù)錄入熱點(diǎn)分析平臺,本地對熱點(diǎn)數(shù)據(jù)進(jìn)行訂閱,并根據(jù)訂閱規(guī)則去更新本地緩存即可。
3、動態(tài)熱點(diǎn)數(shù)據(jù)
動態(tài)指的就是不能提前預(yù)知哪些數(shù)據(jù)是熱點(diǎn)的,需要通過數(shù)據(jù)收集與分析,或者通過大數(shù)據(jù)平臺預(yù)測。
我的做法是通過在網(wǎng)關(guān)平臺中做一個(gè)用于收集日志的異步日志收集系統(tǒng),通過采集商品請求的日志,處理后發(fā)送到熱點(diǎn)分析平臺,熱點(diǎn)分析平臺通過一些列的分析計(jì)算將這些熱點(diǎn)商品進(jìn)行熱點(diǎn)數(shù)據(jù)處理,后端通過訂閱這些熱點(diǎn)數(shù)據(jù)就可以識別哪些商品是熱點(diǎn)數(shù)據(jù)了。
流量削峰
在服務(wù)器資源固定的情況下,說明處理能力是有峰值存在的,如果不對請求處理進(jìn)行處理的話,很可能會在流量峰值的瞬間壓垮服務(wù)器,但流量峰值存在的時(shí)間不長,其實(shí)服務(wù)器的處理能力大部分時(shí)間都是處于閑置狀態(tài),那么我們可不可以將峰值集中的請求分散到其他時(shí)間呢?
1、消息隊(duì)列
消息隊(duì)列除了在解耦、異步場景之外,最大的作用場景是用于流量削峰,面對海量流量請求,可以將這些請求數(shù)據(jù)用異步的方式先存放在消息隊(duì)列中,而消息隊(duì)列一般都能夠存儲大量消息,消息會被消費(fèi)端訂閱消費(fèi),這樣就有效地將峰值均攤到其他時(shí)間進(jìn)行處理了。
如上,消息隊(duì)列就像我們平常見到的水庫一樣,當(dāng)洪水來臨時(shí),攔住并對其進(jìn)行儲蓄,以減少對下游的沖擊,避免了洪水的災(zāi)害。
目前有大量優(yōu)秀的開源消息隊(duì)列框架,如 RocketMQ、Kafka 等,而我之前在中通時(shí)主要負(fù)責(zé)消息平臺的建設(shè)與維護(hù)工作,中通每天面對幾千萬的訂單流量依然那么穩(wěn)固,其中消息隊(duì)列起了很大的“防洪”作用!
2、答題
除了利用消息隊(duì)列對請求進(jìn)行“儲蓄”達(dá)到削峰的目的之外,還可以通過在用戶發(fā)起請求前,對用戶進(jìn)行一些校驗(yàn)操作,比如答題、輸入驗(yàn)證碼等等,這種答題機(jī)制,除了可以防止買家在秒殺過程中使用作弊腳本之外,在秒殺場景中最主要的作還是將請求分散到各個(gè)時(shí)間點(diǎn),秒殺場景一般都是集中在某個(gè)點(diǎn)進(jìn)行,比如 0 點(diǎn)時(shí)刻,如果沒有答題機(jī)制,幾乎所有的流量都在 0 點(diǎn)時(shí)刻涌入服務(wù)器中,如果有答題機(jī)制,就能延緩用戶的請求,從而達(dá)到請求分散到各個(gè)時(shí)間點(diǎn)的目的。
如何保持一致性?
秒殺場景,本質(zhì)上就是在海量買家同時(shí)請求購買時(shí),能夠準(zhǔn)確并將商品賣出去。
在秒殺的高并發(fā)讀寫請求過程中,需要保證商品不會發(fā)生“超賣”現(xiàn)象,因?yàn)槊霘⒌纳唐肥菙?shù)量一定的,但會有成千上萬個(gè)用戶在同一時(shí)間下單購買,在減扣庫存過程中如何保證商品數(shù)量的準(zhǔn)確性至關(guān)重要。
減扣庫存方案分析
我在以前在做秒殺項(xiàng)目的時(shí),分析過幾種減扣庫存的方式,我簡單分析下。
1、下單減扣庫存
買家只要完成下單,立即減扣商品庫存,這種方式實(shí)現(xiàn)是最簡單而且也是最精準(zhǔn)的,通??梢栽谙聠螘r(shí)利用數(shù)據(jù)庫事務(wù)能力即可保證減扣庫存的準(zhǔn)確性,但需要考慮買家下單后不付款的情況。
2、付款減扣庫存
即買家下單后,并不立即減庫存,而是等到有用戶付款后才真正減庫存,否則庫存一直保留給其他買家。但因?yàn)楦犊顣r(shí)才減庫存,如果并發(fā)比較高,有可能出現(xiàn)買家下單后付不了款的情況,因?yàn)榭赡苌唐芬呀?jīng)被其他人買走了。
當(dāng)只有買家下單后,并且已完成付款,才執(zhí)行庫存的減扣,這種方式好處是避免了買家不付款導(dǎo)致實(shí)際沒有賣出這么多商品的情況,但這種方式會造成用戶體驗(yàn)不好,因?yàn)檫@會導(dǎo)致有些用戶付款時(shí)商品有可能被人買走了導(dǎo)致付款失敗的問題。
3、預(yù)扣庫存
這種方式結(jié)合以上兩種方式的優(yōu)點(diǎn),當(dāng)買家下單后,預(yù)扣庫存,只會其保留一定的時(shí)間,比如 10 分鐘,在這段時(shí)間內(nèi)如果買家不付款,則將庫存自動釋放,其它買家可以繼續(xù)搶購。這種做法需要買家付款前,再做一次商品庫是否還有保留,如果沒有保留,則再次嘗試預(yù)扣,預(yù)扣失敗則不允許繼續(xù)付款;如果有保留,付款完成后執(zhí)行真正的減扣庫存動作。
但預(yù)扣庫存依然沒有徹底解決減扣庫存鏈路中存在的問題,比如有些買家可以在釋放的瞬間立馬又重新下單一次,相當(dāng)于將庫存無限地保留下去,因此我們還需要將記錄用戶下單次數(shù),如果連續(xù)下單超過一定次數(shù),或者超過下單并不付款次數(shù),就攔截用戶下單請求。
總結(jié):
一般最簡單的做法就是使用下單減庫存的方式(我之前的項(xiàng)目中就是用的這種),我當(dāng)初的考慮是因?yàn)樵诿霘鼍爸?,商品的性價(jià)比通常很高,秒殺就是創(chuàng)造一種只有少量買家能買到的場景,一般來說買家只要“秒”到商品了,極少情況會出現(xiàn)退款的,即使發(fā)生了少量退款,造成實(shí)際賣出去的商品會比數(shù)據(jù)上少,也是可以通過候補(bǔ)來解決。
如何減扣庫存?
減扣庫存動作應(yīng)該放在哪里執(zhí)行?
下面我具體分析一下減扣庫存的幾種實(shí)現(xiàn)方式:
- 如果鏈路涉及的邏輯比較簡單的,比如下單減庫存這種方式,最簡單的做法就是在下單時(shí),利用數(shù)據(jù)庫的本地事務(wù)機(jī)制進(jìn)行對庫存的減扣,比如使用 where 庫存 >0不滿足就回滾;
- 將庫存數(shù)量值放在緩存中,比如 Redis,并做持久化處理。
需要注意的是,如果遇到減扣庫存的邏輯很復(fù)雜,比如減扣庫存之后需要在同一個(gè)事務(wù)中做一些其他事情,那么就不能使用第二種方式了,只能使用第一種方式在數(shù)據(jù)庫層面上面操作,以保證同在一個(gè)事務(wù)中。面對這種情況,你可以將熱點(diǎn)數(shù)據(jù)進(jìn)行數(shù)據(jù)庫隔離,把這些熱點(diǎn)商品單獨(dú)放在一個(gè)數(shù)據(jù)庫中。
如何實(shí)現(xiàn)高可用性?
最后,為了保證秒殺系統(tǒng)的高可用性,必須要對系統(tǒng)進(jìn)行兜底處理,以便遇到極端的情況系統(tǒng)依然能夠運(yùn)轉(zhuǎn),通常的做法有服務(wù)降級、服務(wù)限流、拒絕請求等方式處理。
服務(wù)降級
當(dāng)請求量達(dá)到系統(tǒng)承受的能力時(shí),需要對系統(tǒng)的一些非核心功能進(jìn)行關(guān)閉操作,盡可能將資源留給秒殺核心鏈路。
比如在秒殺系統(tǒng)中,還存在其他非核心的功能,我們可以在系統(tǒng)中設(shè)計(jì)一些動態(tài)開關(guān),比如在網(wǎng)關(guān)層在路由開關(guān),將這些非核心的請求直接在最外層拒掉。
還有就是對頁面展示的數(shù)據(jù)進(jìn)行精簡化,用降低用戶體驗(yàn)換取核心鏈路的穩(wěn)定運(yùn)行。
服務(wù)限流
限流的目的是通過對并發(fā)訪問/請求進(jìn)行限速或者一個(gè)時(shí)間窗口內(nèi)的的請求進(jìn)行限速來保護(hù)系統(tǒng),常用的有 QPS 限流,用戶請求排隊(duì)限流,需要設(shè)置過期時(shí)間,一旦超過過期時(shí)間則丟棄,這樣做是為了用戶請求可以做到快速失敗的效果,這種機(jī)制在 RocketMQ 中也有相關(guān)的應(yīng)用,RocketMQ broker 會對客戶端請求進(jìn)行排隊(duì)限流處理,當(dāng)請求在隊(duì)列中超過了過期時(shí)間,則丟棄,客戶端快速失敗進(jìn)行第二輪重試。
拒絕請求
如果服務(wù)降級、服務(wù)限流都不能解決問題,最后的兜底,那就是直接拒絕用戶請求,比如直接給用戶返回 “服務(wù)器繁忙,請稍后再試”等提示文案。只會發(fā)生在服務(wù)器負(fù)載過載時(shí)會啟動,因此只會發(fā)生短暫不可用時(shí)刻,由于此時(shí)服務(wù)依然還在穩(wěn)定運(yùn)行中,等負(fù)載下降時(shí),可以快速恢復(fù)正常服務(wù)。
本文轉(zhuǎn)載自微信公眾號「后端進(jìn)階」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系后端進(jìn)階公眾號。