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