[系統設計]秒殺系統

Duancf發表於2024-08-02

如何設計一個秒殺系統?

設計秒殺系統之前,我們首先需要對秒殺系統有一個清晰的認識。
秒殺系統主要為商品(往往是爆款商品)秒殺活動提供支援,這個秒殺活動會限制商品的個數以及秒殺持續時間。
為什麼秒殺系統的設計是一個難點呢? 是因為它的業務複雜麼? 當然不是!
秒殺系統的業務邏輯非常簡單,一般就是下訂單減庫存,難點在於我們如何保障秒殺能夠順利進行。
秒殺開始的時候,會有大量使用者同時參與進來,因此秒殺系統一定要滿足 高併發 和 高效能 。
為了保證秒殺整個流程的順利進行,整個秒殺系統必須要滿足 高可用 。
除此之外,由於商品的庫存有限,在面對大量訂單的情況下,一定不能超賣,我們還需要保證 一致性 。
很多小夥伴可能不太瞭解當代三高網際網路架構:高併發、高效能、高可用。

我這裡簡單解釋一下:高併發簡單來說就是能夠同時處理很多使用者請求。高效能簡單來說就是處理使用者的請求速度要快。高可用簡單來說就是我們的系統要在趨近 100% 的時間內都能正確提供服務。
知道了秒殺系統的特點之後,我們站在技術層面來思考一下:“設計秒殺系統的過程中需要重點關注哪些問題”。
參與秒殺的商品屬於熱點資料,我們該如何處理熱點資料?
商品的庫存有限,在面對大量訂單的情況下,如何解決超賣的問題?
如果系統用了訊息佇列,如何保證訊息佇列不丟失訊息?
如何保證秒殺系統的高可用?
如何對專案進行壓測?有哪些工具?
......
好的,廢話不多說!正式開始!
高效能
熱點資料處理
何為熱點資料? 熱點資料指的就是某一時間段內被大量訪問的資料,比如爆款商品的資料、新聞熱點。
為什麼要關注熱點資料? 熱點資料可能僅僅佔據系統所有資料的 0.1% ,但是其訪問量可能是比其他所有資料之和還要多。不重點處理熱點資料,勢必會給系統資源消耗帶來嚴峻的挑戰。
熱點資料的分類? 根據熱點資料的特點,我們通常將其分為兩類:
靜態熱點資料 :可以提前預測到的熱點資料比如要秒殺的商品。
動態熱點資料 : 不能夠提前預測到的熱點資料,需要透過一些手段動態檢測系統執行情況產生。
另外,處理熱點資料的問題的關鍵就在於 我們如何找到這些熱點資料(或者說熱 key),然後將它們存在 jvm 記憶體裡。 對於併發量非常一般的系統直接將熱點資料存放進快取比如 Redis 中就可以了,不過像淘寶、京東這種級別的併發量,如果把某些熱點資料放在 Redis 中,直接可能就將整個 Redis 叢集給幹掉了。
如何檢測熱點資料?
我瞭解到的是市面上也有一些類似的中介軟體,比如京東零售的 hotkey 就是一款專門用於檢測熱點資料的中介軟體,它可以毫秒級探測熱點資料,毫秒級推送至伺服器叢集記憶體。相關閱讀:京東毫秒級熱 key 探測框架設計與實踐,已完美支撐 618 大促 。
另外,我們平時使用 Redis 做快取比較多,關於如何快速定位 Redis 熱點資料,可以看下如何快速定位 Redis 熱 key這篇文章。
如何處理熱點資料? 熱點資料一定要放在快取中,並且最好可以寫入到 jvm 記憶體一份(多級快取),並設定個過期時間。需要注意寫入到 jvm 的熱點資料不宜過多,避免記憶體佔用過大,一定要設定到淘汰策略。
為什麼還要放在 jvm 記憶體一份? 因為放在 jvm 記憶體中的資料訪問速度是最快的,不存在什麼網路開銷。
靜態資源處理
秒殺頁面可能涉及到很多靜態資源比如商品圖片、CSS、JS。秒殺開始之前以及進行中的時候,會有大量的使用者點開頁面,有的使用者還會不斷的重新整理秒殺介面。如果這些介面中的靜態資源全部透過伺服器獲取,會造成大量的頻寬消耗,甚至造成秒殺還沒開始伺服器就崩了。
對於這些靜態資源,我們可以使用 CDN 進行處理,這是業內目前比較成熟的解決方案。
CDN 全稱是 Content Delivery Network/Content Distribution Network,翻譯過的意思是內容分發網路 。CDN 的作用是將靜態資源分發到多個不同的地方以實現就近訪問,進而加快靜態資源的訪問速度,減輕伺服器以及頻寬的負擔。
你可以將 CDN 看作是服務上一層的特殊快取服務,分佈在全國各地,主要用來處理靜態資源的請求。
基於成本、穩定性和易用性考慮,建議直接選擇專業的雲廠商(比如阿里雲、騰訊雲、華為雲、青雲)或者 CDN 廠商(比如網宿、藍汛)提供的開箱即用的 CDN 服務。
高可用
叢集化
如果我們想要保證系統中某一個元件的高可用,往往需要搭建叢集來避免單點風險,比如說 Nginx 叢集、Kafka 叢集、Redis 叢集。
我們拿 Redis 來舉例說明。如果我們需要保證 Redis 高可用的話,該怎麼做呢?
你直接透過 Redis replication(非同步複製) 搞個一主(master)多從(slave)來提高可用性和讀吞吐量,slave 的多少取決於你的讀吞吐量。
這樣的方式有一個問題:一旦 master 當機,slave 晉升成 master,同時需要修改應用方的主節點地址,還需要命令所有從節點去複製新的主節點,整個過程需要人工干預。
不過,這個問題我們可以透過 Sentinel(哨兵) 來解決。Redis Sentinel 是 Redis 官方推薦的高可用性(HA)解決方案。
Sentinel 是 Redis 的一種執行模式 ,它主要的作用就是對 Redis 執行節點進行監控。當 master 節點出現故障的時候, Sentinel 會幫助我們實現故障轉移,確保整個 Redis 系統的可用性。整個過程完全自動,不需要人工介入!
Sentinel 也是一個 Redis 程序,只是不對外提供讀寫服務,通常哨兵要配置成單數。
限流
限流是秒殺業務最常用的一個手段,所以經常你一參加秒殺就會提示“當前人數過多,請稍後再試”。
介面限流
限流是從使用者訪問壓力的角度來考慮如何應對系統故障。介面限流是為了對服務端的介面接受請求的頻率進行限制,防止服務掛掉。
🌰 舉個例子:我們的秒殺介面一秒只能處理 10w 個請求,結果秒殺活動剛開始一下子來了 15w 個請求。這肯定不行啊!我們只能透過限流把 5w 個請求給攔截住,不然系統直接就給整掛掉了!
介面限流的話可以直接用 Redis 來做(建議基於 Lua 指令碼),也可以使用現成的流量控制元件比如 Sentinel 、Hystrix 、Resilience4J 。
Hystrix 是 Netflix 開源的熔斷降級元件。
Sentinel 是阿里巴巴體提供的面向分散式服務架構的流量控制元件,經歷了淘寶近 10 年雙 11(11.11)購物節的所有核心場景(比如秒殺活動)的考驗。
Sentinel 主要以流量為切入點,提供 流量控制、熔斷降級、系統自適應保護等功能來保護系統的穩定性和可用性。
個人比較建議使用 Sentinel ,更新維護頻率更高,功能更強大,並且生態也更豐富(Sentinel 提供與 Spring Cloud、Dubbo 和 gRPC 等常用框架和庫的開箱即用整合, Sentinel 未來還會對更多常用框架進行適配,並且會為 Service Mesh 提供叢集流量防護的能力)。
除了直接對介面進行限流之外,我們還可以對使用者、IP進行限流,限制同一使用者以及IP單位時間內可以請求介面的次數。
問題/驗證碼
我們可以在使用者發起秒殺請求之前讓其進行答題或者輸入驗證碼。
這種方式一方面可以避免使用者請求過於集中,另一方面可以有效解決使用者使用指令碼作弊。
回答問題/驗證碼這一步建議除了對答案的正確性做校驗,還需要對使用者的提交時間做校驗,比如提交時間過短(<1s)的話,大概就是使用指令碼來處理的。
提前預約
採用提前預約才能參加秒殺活動的方式過濾一批人。並且,我們在秒殺活動開始之前,還可以對預約的這些人進行篩選,透過某些方式找出潛在的黃牛。
流量削峰
對於突發的大流量我們還可以使用訊息佇列進行流量削峰。
秒殺開始之後的流量不是很大,我處理不了嘛!那我就先把這些請求放到訊息佇列中去。然後,咱後端服務再慢慢根據自己的能力去消費這些訊息,這樣就避免直接把後端服務打垮掉。
對於秒殺場景來說,使用者傳送秒殺請求之後先存到訊息佇列中,然後再慢慢去消費。
不過,如果你已經對介面進行的限流處理的話,這裡其實就沒必要再上訊息佇列了。
降級
降級是從系統功能優先順序的角度考慮如何應對系統故障。
服務降級指的是當伺服器壓力劇增的情況下,根據當前業務情況及流量對一些服務和頁面有策略的降級,以此釋放伺服器資源以保證核心任務的正常執行。降級的核心思想就是丟車保帥,優先保證核心業務。x
🌰 舉個例子:當請求量達到一個閾值的時候,我們對系統中一些非核心的功能直接關閉或者讓它們功能降低。這樣的話,系統就有更多的資源留給秒殺功能了!
熔斷
熔斷和降級是兩個比較容易混淆的概念,兩者的含義並不相同。降級的目的在於應對系統自身的故障,而熔斷的目的在於應對當前系統依賴的外部系統或者第三方系統的故障。
熔斷可以防止因為秒殺交易影響到其他正常服務的提供
🌰 舉個例子: 秒殺功能位於服務 A 上,服務 A 上同時還有其他的一些功能比如商品管理。如果服務 A 上的商品管理介面響應非常慢的話,其他服務直接不再請求服務 A 上的商品管理這個介面,從而有效避免其他服務被拖慢甚至拖死。
一致性
減庫存方案
常見的減庫存方案有:
下單即減庫存 :只要使用者下單了,即使不付款,我們就扣庫存。
付款再減庫存 :當使用者付款了之後,我們再減庫存。不過, 這種情況可能會造成使用者下訂單成功,但是付款失敗。
一般情況下都是 下單減扣庫存 ,像現在的購物網站比如京東都是這樣來做的。
不過,我們還會對業務邏輯做進一步最佳化,比如說對超過一定時間不付款的訂單特殊處理,釋放庫存。
對應到程式碼層面,我們應該如何保證不會超賣呢?
我們上面也說,我們一般會提前將秒殺商品的資訊放到快取中去。我們可以透過 Lua 指令碼對庫存進行原子操作。虛擬碼如下:
不過,如果 Lua 指令碼執行時出錯並中途結束,出錯之後的命令是不會被執行的。並且,出錯之前執行的命令是無法被撤銷的,無法實現類似關係型資料庫執行失敗可以回滾的那種原子性效果。因此, 嚴格來說的話,透過 Lua 指令碼來批次執行 Redis 命令實際也是不完全滿足原子性的。如果想要讓 Lua 指令碼中的命令全部執行,必須保證語句語法和命令都是對的
在 Redis 中扣減庫存成功後,需要將庫存同步到 MySQL 。MySQL 的庫存並不需要去實時進行更新,只需要庫存達到最終一致性即可,即先對 Redis 的庫存進行更新,然後再非同步同步到 MySQL 的庫存。這裡的非同步實現方式建議使用 MQ,由 MQ 保證訊息被消費,實現最終一致性,畢竟秒殺場景本身就要引入 MQ 進行流量削峰。
餘額扣減方案
在高併發下,餘額扣減也是關鍵。假設使用者同時下了兩單 a,b,這兩單都同時查到使用者餘額為 10000,然後執行扣款,那有一個訂單就相當於沒付款。
如何解決呢?在併發量高情況下推薦使用悲觀鎖,如果併發量不高可以考慮使用樂觀鎖。
悲觀鎖可以基於Redis 或者 ZooKeeper 實現,但一般不會這麼做,因為還要考慮他們的異常情況。餘額扣減場景,對於正確性要求極高!我們可以直接利用資料庫自帶的排他鎖(X 鎖),這種方案用的最多,大部分銀行都是這樣做的。
在 MySQL 裡使用排他鎖:
樂觀鎖建議使用版本號機制實現,同時要注意 ABA 問題。
介面冪等
透過介面冪等保證使用者不重複下單和支付,介面冪等常見方案可以檢視《Java面試指北》中的這篇文章:
高可用:如何保證介面冪等性?
介面冪等性是面試中常見的問題,也是日常開發過程中經常需要解決的問題。什麼是冪等(idempotency)?冪等(idempotency)本身是一個數學概念,常見於抽象代數中,表示一個函式或者操作的結果不受其輸入或者執行次數的影響。例如, f(n) = 1^n ,無論 n 為多少,f(n)的值永...
《Java面試指北》
對於秒殺這種場景,建議搭配狀態機實現冪等。如果使用分散式鎖的話, key 可以根據請求內容生成。
效能測試
上線之前壓力測試是必不可少的。推薦 4 個比較常用的效能測試工具:
Jmeter :Apache JMeter 是 JAVA 開發的效能測試工具。
LoadRunner:一款商業的效能測試工具。
Galtling :一款基於 Scala 開發的高效能伺服器效能測試工具。
ab :全稱為 Apache Bench 。Apache 旗下的一款測試工具,非常實用。
沒記錯的話,除了 LoadRunner 其他幾款效能測試工具都是開源免費的。
總結
我簡單畫了一張圖來總結一下上面涉及到的一些技術。
另外,上面涉及到知識點還蠻多的,如果面試官單獨挑出一個來深挖還是能夠問出很多問題的。
比如面試官想在訊息隊裡上進行深挖,可能會問:
常見訊息佇列的對比
如何保證訊息的消費順序?
如何保證訊息不丟失?
如何保證訊息不重複消費?
如何設計一個訊息佇列?
......
再比如面試官想在 Redis 上深挖的話,可能會問:
Redis 常用的資料結構瞭解麼?
Redis 如何保證資料不丟失?
Redis 記憶體佔用過大導致響應速度變慢怎麼解決?
快取穿透、快取雪崩瞭解麼?怎麼解決?
......
因此,要想要真正搞懂秒殺系統的設計,你還需要將其涉及到的一些技術給研究透!

相關文章