系統設計 - 秒殺系統設計

文 | 蔡佳新 (轉載請註明出處)

公衆號:DDD 和微服務

編輯:少個分號

秒殺活動是大家都熟悉的購物方式。通常的流程是這樣的:商家設定活動開始和結束時間,以及投入的庫存量。在活動即將開始之際,買家會不斷刷新商品詳情頁,一旦看到購買按鈕變爲可點擊狀態,便立即點擊購買、下單並結算,完成商品的購買過程。當商品庫存被搶空或者活動結束時,商品變爲不可售狀態,買家無法繼續購買。

一些耳熟能詳的秒殺活動案例包括小米手機的早期搶購、整點低價蘋果手機、以 1499 元搶購茅臺酒,以及疫情期間的口罩銷售。這些活動各有不同的目的:小米手機搶購是爲了營造熱點,整點低價蘋果手機旨在吸引用戶到店,1499 元搶購茅臺則是爲了加速用戶對茅臺線上銷售的接受程度,而口罩銷售則是平臺的社會責任擔當,希望能夠用有限的庫存幫助更多需要的家庭。然而,它們都面臨着一個共同的挑戰:極度的商品供不應求的情況。這裏面既有真實迫切需求的用戶,也必然引來從中賺差價的黃牛。反應到系統應對的挑戰上,就是相較於以往千倍萬倍的用戶規模,可能是真人可能是機器人,在同一瞬間對系統發起衝擊,需要海量的計算資源才能支撐。

對於各大電商平臺而言,爆款運營和促銷活動的日常化已成爲常態,而支撐這些的秒殺系統自然是不可或缺的一環。同時,秒殺活動的巨大流量就像一頭洪荒之獸,若控制不當,可能會衝擊整個交易體系。因此,秒殺系統在交易體系中便扮演着至關重要的角色。從個人角度來看,秒殺系統的設計套路往往適用於其他高併發場景,具有較高的借鑑價值。同時,其特殊的挑戰和需求,需要架構師在設計中的權衡考量,這也有助於培養個人在權衡取捨方面的能力。

無損的技術方案

應對高併發就好比應對水患。

因爲後續方案會圍繞請求經過的多個層級展開,所以在介紹方案之前,我們需要先了解一個基本情況:一個請求打到服務器的基本鏈路爲:DNS→網關→前端→後端,其中流量峯值也應該逐層減少。如圖:

系統隔離

分佈式系統,系統由一個個發佈單元共同組成。獨立的發佈單元可以按需做伸縮擴容,也可以在發生故障時及時做故障隔離,以保證不會出現個別服務故障導致整個系統不可用的情況。秒殺活動因爲其高峯值的特性,所以一般我們會把它隔離出來,成一個獨立的秒殺系統(常規服務我們都是按領域特性做縱切,但這裏我們按品類做橫切,帶有秒殺活動標的商品將會分流到獨立的秒殺系統集羣)。但交易系統體量是很大的,如果爲秒殺品類把整個交易系統都複製一份,那成本就太大了。所以我們把隔離區分爲物理隔離和邏輯隔離,把需要定製化邏輯的能力和有特殊非功能性要求的能力剝離出來做物理隔離,標準化且沒有特殊非功能要求的能力就採用邏輯隔離。畫成部署架構如下:

首先,我們採用獨立的秒殺域名和 nginx 集羣(物理隔離)。這樣可以隔離流量大頭,防止峯值衝擊交易系統;能夠靈活擴展,針對不同時段的流量預估擴展 nginx 及後邊服務的規模;能夠靈活增減私有的防控邏輯,而不影響原交易系統。

接着,我們將商詳頁和下單頁獨立部署(前端 + BFF)(物理隔離)。因爲秒殺活動的特點,海量用戶在活動快開始時會反覆刷新商詳頁,在活動開始時又會瞬時併發的訪問下單頁,所以這兩個頁面都是承受流量衝擊的大頭,需要隔離開。同時因爲秒殺活動的特性,商品屬於極端供不應求的場景,所以可以做服務降級,以降低計算資源消耗、提高性能。比如:商詳頁可以把履約時效拿掉,不再計算預計多久能到貨。還可以拿掉評價信息,不再展示評價;下單頁可以不再計算優惠金額分攤,秒殺商品不參與任何疊加的優惠活動。僅保留必要的信息,比如商品信息,商品主圖,購買按鈕,金額計算,下單按鈕等等。至於結算頁、收銀臺看情況,如果流量壓力不大是不用做物理隔離的。

最後,商品購買成功還需要依賴,訂單系統創建訂單,庫存系統扣減商品庫存,結算系統確認支付等等步驟。到達這裏流量相對已經比較平穩,並且邏輯上沒有什麼定製性能優化的機會,所以就採用邏輯隔離複用原交易系統的集羣。邏輯隔離有兩種實現思路,第一種是依賴限流框架,比如在訂單系統設置來源是秒殺系統 BFF 的創建訂單請求,TPS 不能超過 100,併發連接數不能超過 20;第二種是依賴 RPC 框架,RPC 框架可以設置分組,只要把訂單系統集羣裏面部分服務節點設置成” 秒殺組”,再把秒殺服務 BFF 的客戶端也設置爲” 秒殺組”,那麼秒殺系統的流量就只會打到訂單系統集羣裏面屬於” 秒殺組” 的節點上。這種隔離方式分割了集羣,集羣節點少了,出現故障發生過載的可能就提高了,可能會導致秒殺系統不可用。

爲什麼集羣節點少了,出現故障發生過載的可能就提高了?就好比公里原本 4 條道能並行 4 輛車,現在給按車輛類型分成了機動車和公交車專用,機動車道 2 條。如果其中 1 條機動車道發生車禍,原本分散在 2 條道上的車流就要匯聚在 1 條道,原本順暢的通行可能立馬就開始堵車了。### 多級緩存

多級緩存,無非就是在系統的多個層級進行數據緩存,以提高響應效率,這是高併發架構最廣泛的方案之一。

DNS 層

一般我們會將靜態資源掛到 CDN 上,藉助 CDN 來分流和提高響應效率。以秒殺系統爲例,就是將秒殺前端系統的商詳頁和下單頁緩存到 CDN 網絡上。一個藉助 CDN 的用戶請求鏈路如下:

如果用戶終端有頁面緩存就走終端本地緩存,沒有就請求遠端 CDN 的域名(靜態資源走 CDN 域名),請求來到 DNS 調度的節點,調度一個最近的 CDN 節點,如果該 CDN 節點有頁面緩存則返回,沒有則向緣站發起溯源,請求就會走普通鏈路過秒殺系統 ng 到秒殺系統前端。

網關層

網關這個有多種組合情況,最簡單的就是一個接入層網關加一個應用層網關,比如:ISV(四層)→ Nginx(七層)。以這個爲例,這裏的緩存優化主要看接入層的負載均衡算法和應用層的本地緩存和集中內存緩存。

之所以說緩存還要提負載均衡算法,是因爲節點的本地緩存的有效性和負載均衡算法是強綁定的。常用的負載均衡算法有輪詢 (也叫取模) 和一致性哈希。輪詢可以讓請求分發更均衡,但同個緩存 key 的請求不一定會路由到同個應用層 Nginx 上,Nginx 的本地緩存命中率低。一致性哈希可以讓同個緩存 key 路由到同個應用層 Nginx 上,Nginx 的本地緩存命中率高,但其請求分發不均衡容易出現單機熱點問題。有一種做法是設置一個閾值,當單節點請求超過閾值時改爲輪詢,可以算是自適應性負載均衡的變種。但這種做法在應對真正的高併發時效果並不理想。

所以想要運用本地緩存強依賴業務運營,需要對每個熱點商品 key 有較爲準確的流量預估,並人爲的組合這些商品 key,控制流量均勻的落到每個應用層 Nginx 上 (其實就是數據分片,然後每片數據流量一致)。這非常困難,所以筆者認爲,大部分時候還是採用輪詢加集中內存緩存比較簡單有效。

一個從接入層開始帶有本地緩存和集中內存緩存的請求鏈路如下:

服務層

應用層 ngnix→秒殺系統 BFF→訂單服務,其實兩兩組合和網關層是一樣的場景。應用層 ngnix 基於 ngnix 的負載均衡轉發請求到秒殺系統 BFF,秒殺系統 BFF 基於 RPC 框架的負載均衡轉發請求到訂單服務。都面臨着負載均衡策略選擇和是否啓用本地緩存的問題。不一樣的點只是緩存的粒度和啓用緩存的技術棧選擇。

多級緩存失效

多級緩存因爲緩存分散到多個層級,所以很難用單一的技術棧來應對緩存失效的問題,但都等到緩存過期,這種更新時延較長又不一定能被業務接受。所以這裏就再展開下這個話題。有一個做法是基於 DB 的 binlog 監聽,各層監聽自己相關的 binlog 信息,在發生緩存被變更的情況時,及時讓集成內存的緩存失效。本地緩存在這裏還有個缺陷,就是緩存失效時需要廣播到所有節點,讓每個節點都失效,對於頻繁變更的熱 key 就可能產生消息風暴。

無損消峯

秒殺活動的特點是瞬時高峯的流量,就像一座高聳的尖塔,短時間內湧入大量請求。爲這個峯值準備對應的服務集羣,首先成本太高,接着單純的水平擴展也不一定能做到(分佈式架構經常會碰到量變引起質變的問題,資源擴展到一定量級,原先的技術方案整個就不適用了。比如,當集羣節點太多,服務註冊發現可能會有消息風暴;出入口的帶寬出現瓶頸,需要在部署上分流)。更別說這個峯值也不受控制,想要高枕無憂就會有很高的冗餘浪費。所以一般我們會採用消峯的方式,一種是直接斷頭,把超出負荷的流量直接都丟棄掉,也就是我們常見的限流,也稱爲有損消峯(如果這是大促的訂單,砍掉的可能都是錢,這個有損是真的資損);另一種就是分流,也叫消峯填谷,通過技術或者業務手段將請求錯開,鋪到更長的時間線上,從而降低峯值,常見的有 MQ 異步消費和驗證碼問答題。這裏我們着重聊下無損消峯,有損放後邊談。

MQ 異步消費

MQ 依賴三個特性可以做到平滑的最終一致,分別是,消息堆積,勻速消費和至少成功一次。有消息堆積才能起到蓄水池的效果,在出水口流速恆定的情況下能接住入水口瞬時的大流量;有勻速消費才能讓下游集羣的流量壓力恆定,不會被衝擊;有至少成功一次,才能保證事物最終一致。以創建訂單爲例。如果沒有消息隊列(MQ),同時有 100W 個創建請求,訂單系統就必須承擔 100W 個並行連接的壓力。但是,如果使用了 MQ,那麼 100W 個創建請求的壓力將全部轉移到 MQ 服務端,訂單系統只需要維持 64 個並行連接,以穩定地消費 MQ 服務端的消息。這樣一來,訂單系統的集羣規模就可以大大減小,而且更重要的是,系統的穩定性得到了保障。由於並行連接數的減少,資源競爭也會降低,整體響應效率也會提高,就像在食堂排隊打飯一樣,有序排隊比亂搶效率更高。但是,用戶體驗可能會受到影響,因爲點擊搶購後可能會收到排隊提示,需要延遲幾十秒甚至幾分鐘才能收到搶購結果。

驗證碼問答題

引入驗證碼問答題其實有兩層好處,一層是消峯,用戶 0.5 秒內併發的下單事件,因爲個人的手速差異,被平滑的分散到幾秒甚至幾十秒中;另外一層是防刷,提高機器作弊的成本。

驗證碼

基本實現步驟如下:

但這樣其實是可以用暴力破解的,比如,用機器仿照一個用戶發起 10W 個請求攜帶不同的 6 位隨機字符。所以校驗驗證碼時可以使用 GETDEL ,讓驗證碼校驗無論對錯都讓驗證碼失效。

問答題

基本實現思路和驗證碼幾乎一樣。差別在於,問答題的題庫要提前生成,請求到來時從題庫中拿到一組問題和答案。然後把答案存 redis,問題塞到圖片裏返回給用戶。驗證碼和問答題具有很好的消峯效果。特別是問答題,想要提高消峯效果只要提高問題難度就行,例如,筆者曾經在 12306 上連續錯了十幾次問答題。但是這也是用戶體驗有損的,例如,雖然筆者當初未能成功搶到票而感到沮喪,但這不當人的題庫依然把我逗笑了。

無損消峯,無損了流量,但損失了用戶體驗。現如今技術水平在不斷進步,解決方法在增多,這些有損用戶體驗的技術方案可能都會慢慢退出歷史舞臺,就像淘寶取消 618 預售。

庫存扣減

我們知道,用戶購買商品需要扣減庫存,扣減庫存需要查詢庫存是否足夠,足夠就佔用庫存,不夠則返回庫存不足。在併發場景,如果查詢庫存和扣減庫存不具備原子性,就有可能出現超賣,而高併發場景超賣的出現概率會增高,超賣的數額也會增高高。處理超賣問題是件麻煩事,一方面,系統全鏈路刷數會很麻煩 (多團隊協作),客服外呼也會有額外成本。另一方面,也是最主要的原因,客戶搶到了訂單又被取消,會嚴重影響客戶體驗,甚至引發客訴產生公關危機。

實現邏輯

業內常用的方案就是使用 redis+lua,藉助 redis 單線程執行 + lua 腳本中的邏輯可以在一次執行中順序完成的特性達到原子性(原子性其實不大準確,叫排它性可能更準確些,因爲這裏不具備回滾動作,異常情況需要自己回滾)。lua 腳本基本實現大致如下:

-- 獲取庫存緩存key KYES[1] = hot_{itemCode-skuCode}_stock
local hot_item_stock = KYES[1]
-- 獲取剩餘庫存數量
local stock = tonumber(redis.call('get', hot_item_stock))
-- 購買數量
local buy_qty = tonumber(ARGV[1])
-- 如果庫存小於購買數量 則返回 1, 表達庫存不足
if stock < buy_qty then return 1 end
-- 庫存足夠
-- 更新庫存數量
stock = stock - buy_qty
redis.call('set', hot_item_stock, tostring(stock))
-- 扣減成功 則返回 2, 表達庫存扣減成功
return 2 end

但這個腳本具備一些問題:

結合以上問題,我們對方案做些增強。增強後的 lua 腳本如下:

-- 獲取庫存扣減記錄緩存key KYES[2] = hot_{itemCode-skuCode}_deduction_history
-- 使用 Redis Cluster hash tag 保證 stock 和 history 在同個槽
local hot_deduction_history = KYES[2]
-- 請求冪等判斷,存在返回0, 表達已扣減過庫存
local exist = redis.call('hexists', hot_deduction_history, ARGV[2])
if exist = 1 then return 0 end

-- 獲取庫存緩存key KYES[1] = hot_{itemCode-skuCode}_stock
local hot_item_stock = KYES[1]
-- 獲取剩餘庫存數量
local stock = tonumber(redis.call('get', hot_item_stock))
-- 購買數量
local buy_qty = tonumber(ARGV[1])
-- 如果庫存小於購買數量 則返回 1, 表達庫存不足
if stock < buy_qty then return 1 end
-- 庫存足夠
-- 1.更新庫存數量
-- 2.插入扣減記錄 ARGV[2] = ${扣減請求唯一key}-${扣減類型} 值爲 buy_qty
stock = stock - buy_qty
redis.call('set', hot_item_stock, tostring(stock))
redis.call('hset', hot_deduction_history, ARGV[2], buy_qty)
-- 如果剩餘庫存等於0 則返回 2, 表達庫存已爲0
if stock = 0 then return 2 end
-- 剩餘庫存不爲0 返回 3 表達還有剩餘庫存
return 3 end

利用 Redis Cluster hash tag 保證 stock 和 history 在同個槽,這樣 lua 腳本才能正常執行。

利用 hot_deduction_history,判斷扣減請求是否執行過,以實現冪等性。

藉助 hot_deduction_history 的 value 值判斷追溯扣減來源,比如:用戶 A 的交易訂單 A 的扣減請求,或者用戶 B 的借出單 B 的扣減請求。

回滾邏輯會先判斷 hot_deduction_history 裏面有沒有 ${扣減請求唯一 key} ,有則執行回補邏輯,沒有則認定回補成功。

但是以上邏輯依舊有漏洞,比如,訂單扣減庫存超時成功觸發了重新扣減庫存,但同時訂單取消觸發了庫存扣減回滾,回滾邏輯先成功,超時成功的重新扣減庫存就會成爲髒數據留在 redis 裏。處理方案有兩種,一種是追加對賬,定期校驗 hot_deduction_history 中數據對應單據的狀態,對於已經取消的單據追加一次回滾請求,存在時延(業務不一定接受)以及額外計算資源開銷。另一種,是使用有序消息,讓扣減庫存和回滾庫存都走同一個 MQ topic 的有序隊列,藉助 MQ 消息的有序性保證回滾動作一定在扣減動作後面執行,但有序串行必然帶來性能下降。

高可用

存在 redis 終究是內存,一旦服務中斷,數據就消失的乾乾淨淨。所以需要追加保護數據不丟失的方案。

運用 redis 部署的高可用方案來實現,方案如下:

定期歸檔冷數據。定期觸發 redis 數據往 DB 同步,流程如下:

CDC 分發數據時,秒殺商品,hot_deduction_history 的數據量不高,可以一次全量同步。但如果是普通大促商品,就需要再追加一個 map 動作分批處理,以保證每次執行 CDC 的數據量恆定,不至於一次性數據量太大出現 OOM。具體代碼如下:

/**
 * 對任務做分發
 * @param stockKey 目標庫存的key值
 */
public void distribute(String stockKey){
    final String historyKey = StrUtil.format("hot_{}_deduction_history", stockKey);
    // 獲取指定庫存key 所有扣減記錄的key(一般會採用分頁獲取,防止數據量太多,內存波動,這裏偷懶下)
    final List<String> keys  = RedisUtil.hkeys(historyKey, stockKey);
    // 以 100 爲大小,分片所有記錄 key
    final List<List<String>> splitKeys = CollUtil.split(keys, 100);
    // 將 集合分發給各個節點執行
    map(historyKey, splitKeys);
}
/**
 * 對單頁任務做執行
 * @param historyKey 目標庫存的key值
 * @param stockKeys  要執行的頁面大小
 */
public void mapExec(String historyKey, List<String> stockKeys){
    // 獲取指定庫存key 指定扣減記錄  的map
    final Map<String, String> keys = RedisUtil.HmgetToMap(historyKey, stockKeys);
    keys.entrySet()
        .stream()
        .map(stockRecordFactory::of)
        .forEach(stockRecord -> {
            // (冪等 + 去重) 扣減 + 保存記錄
            stockConsumer.exec(stockRecord);
            // 刪除redis中的 key 釋放空間
            RedisUtil.hdel(historyKey, stockRecord.getRecordRedisKey());
        });
}

爲什麼不走 DB

商品庫存數據在 DB 最終會落到單庫單表的一行數據上。無法通過分庫分表提高請求的並行度。而在單節點的場景,數據庫的吞吐遠不如 redis。最基礎的原因:IO 效率不是一個量級,DB 是磁盤操作,而且還可能要多次讀盤,redis 是一步到位的內存操作。

同時,一般 DB 都是提交讀隔離級別,爲了保證原子性,執行庫存扣減,得加鎖,無論悲觀還是樂觀。這不僅性能差(搶不到鎖要等待),而且因爲非公平競爭,容易出現線程飢餓的問題。而 redis 是單線程操作,不存在共享變量競爭的問題。

有一些優化思路,比如,合併扣減,走批降低請求的並行連接數。但伴隨而來的是集單的時延,以及按庫分批的訴求;還有拆庫存行,商品 A100 個庫存拆成 2 行商品 A50 庫存,然後扣減時分發請求,以此提高並行連接數(多行可落在不同庫來提高並行連接數)。但伴隨而來的是複雜的庫存行拆分管理(把什麼庫存行在什麼時候拆分到哪些庫),以及部分庫存行超賣的問題(加鎖優化就又串行了,不加總量還有庫存,個別庫存行不足是允許一定係數超賣還是返回庫存不足就是一個要決策的問題)。

當然部分頭部電商還是採用弱緩存抗讀(非庫存不足,不實時更新),DB 抗寫的方案。這個的前提在於,通過一系列技術方案,流量落到庫存已經相對低且平滑了(扛得住,不用再自己實現操作原子性)。

有損的技術方案

秒殺活動有極高的瞬時流量,但僅有極少數流量可以請求成功。這爲我們繞開海量計算資源採用一些特定方案達到同樣的活動效果提供了空間。因爲絕大部分流量都是要請求失敗的,是真實搶購庫存失敗還是被規則過濾掉失敗,都一樣是失敗,對於參與者來說是一樣的活動體驗。所以我們不用耿直的去承接所有流量,變成用一系列過濾手段,公平公正的過濾掉絕大部分流量,僅保留有限的優質流量可以請求到服務羣即可。基本思路就是,通過業務干預過濾無效流量,通過有損消峯丟棄多餘流量,通過防刷風控過濾刷子流量,最終留給下游優質且少量的流量。如圖:

業務干預

提報

藉助提報系統,提早知道商品、價格、活動開始時間、面向什麼地域、預計參與人數、會員要求等等信息。可以預估出大致流量,支撐編排活動調整活動組合分散壓力(也能不斷保持熱點),調整計算機資源應對高併發。設置參與門檻,阻擋非目標人羣參與。

預約

藉助預約系統,對活動做預熱、預估大致參與活動的人數幫助評估計算資源容量。引入風控規則,提早過濾刷子人羣。採用發放參與證書(類似遊戲預約測試資格和發放測試資格),控制參與人數大小。結合提報系統的參數,過濾非目標人羣,並儘量提高參與人員離散度(比如參與證書 1W,華南華北華東華西各 2500)(假設中獎的人影響範圍是一個圓,人羣集中這個圓就有交集,影響範圍就會減少,所以會希望離散些。但也不排除有故意集中發放創造熱點的運營手段)。

會員

藉助會員系統,篩選出優質用戶。願意購買會員的用戶相對粘性就比較高(可以藉助會員體系做一些提高用戶粘性的舉措,比如信用分,積分,會員等級,優惠卷等等)。同時會員用戶的規模也能幫助預估活動參與流量。

限購

藉助限購系統,排除非目標人羣,比如從地區限制,僅華東可以參與購買;從用戶限制,自家員工禁止購買(主要是擔心輿情公關危機,不能既做裁判也下場踢球)。提高離散度,比如從商品限制,一次只能購買一件,一人一個月只能購買一次。

有損消峯

前邊講了分流的無損消峯,這裏我面講直接去頭的有損消峯。常規方案就是採用限流降級手段。這也是應對高併發必用的手段。

限流是系統自我保護的最底層手段。再厲害的系統,總有其流量承載的上限,一旦流量突破這個上限,就會引起實例宕機,進而發生系統雪崩,帶來災難性後果。所以達到這個流量上限後,橫豎都無法再響應請求,於是直接拋棄這部分請求,保正有限的流量能夠正常交互便成了最優解。

分層限流

我們知道一個請求會走過多個層級,最終才能到達響應請求的服務節點。假設一個請求會走過網關→單服務集羣→單服務節點→單接口這幾個層級,每個層級考慮承載上限的維度和容量都不一樣,所以一般都會有獨立的限流規則。

網關一般是以一個路由配置或者一組 api 的吞吐指標進行限流,具體配置大致如下:

單服務集羣一般是以整個集羣所有 API 和所有服務節點爲吞吐指標進行限流(不常用),具體配置大致如下:

單服務節點一般是以服務節點的負載情況來進行限流,比如 Load(綜合計算值)、CPU、內存等等。

單接口一般是以整個集羣的一個 API 的吞吐指標來進行限流。

熱點參數限流

除開分層的限流,還有參數維度的限流。

比如,基於 IP 地址的吞吐量指標做限流。這個維度,對公司用戶很不友好。因爲一般公司就幾個 IP 出口,大家都連着 wify,很容易就觸發限流。所以,一般參與秒殺活動時還是切換回自己的 4G 網,wify 再快也架不住被限流。

比如,基於熱點商品的吞吐量指標做限流。在沒有商品維度限流的情況下,假設秒殺下單接口的集羣併發限流爲 100,同一時間參與秒殺活動的商品有 10 個,商品 A 在一瞬間就搶佔了 80 併發連接數,剩下的 9 商品就只能分攤 20 併發連接數,這會嚴重影響其活動體驗。

限流的口徑有很多,幸運的是它們可以組合使用。這樣就能夠確保服務在各種場景下都有一個可靠的底層防護。

防刷風控

秒殺活動中的供需失衡,也會吸引黑產用戶藉助非常規手段搶購。比如,通過物理或軟件的按鍵精靈,用比正常用戶更快的速度搶購;通過分析接口模仿下單請求,同時發起千萬個請求,用比正常用戶更高的頻次搶購。這些行爲不僅破壞了活動公平性,威脅到普惠和離散訴求,還對系統的高併發峯值帶來了新的量級的挑戰,嚴重影響活動的健康發展。

防刷

從更快的速度搶購的角度很難區分是正常用戶還是黑產用戶,但更高頻次是很好被捕捉的,畢竟正常人總不能 1 秒鐘千萬次的點擊吧。所以我們可以針對高頻次這個場景構建一些防刷手段。

基於 userID 限流

我們可以採用熱點參數限流的方式,基於用戶 ID 的吞吐量指標做限流。例如,規定每個用戶 ID 每秒僅能發起兩次請求。並且,我們應將此限流措施儘可能地置於請求鏈路的上游,如應用網關上,以便在最外層就隔離掉主要流量,從而減少計算資源的浪費。這樣的限流目的與常規的有損消峯略有所不同,它不僅旨在保護服務的穩定性,也在防止黑產用戶的攻擊,以此維護活動的公平性。

基於黑名單限流

依舊是採用熱點參數限流的方式。但不再是看吞吐量指標,而是看是否命中黑名單來實現限流。黑名單裏面的名單,一方面靠一些內部行爲分析,比如發現某個用戶每秒可以請求千萬次來識別(就像遊戲裏面發現外掛封號)。另一方面就是靠外部風控數據的導入了。

風控在系統防護中佔據重要地位,然而其建立卻頗爲艱難。健全的風控體系需要依賴大量數據,並通過實際業務場景的嚴苛考驗,通過不斷的智能修正,以提升風險識別的準確性。簡單來說,風控就像繪製用戶畫像,需要收集用戶的基本信息,如身份證、IP、設備號(如同一設備或同一 IP 的多賬戶並行搶購)、信貸記錄、社保信息、工作信息等多維度信息。同時,還要關注用戶的行爲信息,如是否存在每秒發起千萬次請求的情況,或者用戶是否只在特定活動中才呈現活躍等。

提到用戶畫像,其實好的用戶畫像可能還能做到用戶分級。例如,將用戶劃分爲 1 至 8 級,如果篩選出的流量大部分爲 8 級也符合優質的目的。畢竟,高級用戶在推廣範圍、推廣質量以及自身消費能力上都表現出色。畢竟,這仍時代的舞臺總是 “氪佬 “玩家和 “歐皇 “的。

寫在最後

高併發的主要挑戰在於瞬時激增的大量用戶請求需要同時使用大量的計算資源。爲了解決這一挑戰,互聯網應用選用了水平伸縮的發展路線,即分佈式架構,通過不斷橫向擴展集羣節點來增加計算能力。而我們列舉的方案大部分都直接或間接依賴於分佈式架構設計,所以掌握分佈式架構其實就等同於掌握高併發系統設計的核心。

優秀的架構更注重權衡,而不是追求極端。應該從業務場景和公司實際情況出發,尋找合適且投資回報率高的方案,而非過度設計或追求最極致的解決方案。更不應出於恐懼落後或投機取巧的心態,盲目追求所謂的 “最佳實踐 “。

參考資料

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/u7yp_SpJS18kkzoXdUNXCw