海量請求下的接口併發解決方案

設定一個場景,假如一個商品接口在某段時間突然上升,會怎麼辦?

生活中的例子來說,假設冰墩墩在當天晚上上熱搜之後,迅速有十幾萬人去淘寶下單購買,此時並沒有做好對該商品的緩存預熱以及準備,如何操作?

對於這個問題,在電商高併發系統中,對接口的保護一般採用:緩存、限流、降級 來操作。

假設該接口已經接受過風控的處理,過濾掉一半的機器人腳本請求,剩下都是人爲的下單請求。

服務限流

限流 主要的目的是通過對併發訪問 / 請求進行限速,或者對一個時間窗口內的請求進行限速,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理。

限流算法

  1. 漏斗算法 漏桶算法

    是當請求到達時直接放入漏桶,如果當前容量已達到上限(限流值),則進行丟棄或其他策略(觸發限流策略)。漏桶以固定的速率(根據服務吞吐量)進行釋放訪問請求(即請求通過),直到漏桶爲空。

漏斗算法的思想就是,不管你來多少請求,我的接口消費速度一定是小於等於流出速率的閾值的。

可以基於消息隊列來實現

  1. 令牌桶算法 令牌桶算法

    是程序以 v(v = 時間週期 / 限流值)的速度向令牌桶中增加令牌,直到令牌桶滿,請求到達時向令牌桶請求令牌,如果獲取成功則通過請求,如果獲取失敗觸發限流策略。

令牌桶算法和漏斗算法的思想差別在於,前者可以允許突發請求的發生。

  1. 滑窗算法 滑窗算法

    是將一個時間週期分爲 N 個小週期,分別記錄每個小週期內訪問次數,並且根據時間滑動刪除過期的小週期。

如下圖所示,假設時間週期爲 1 分鐘,將 1 分鐘再分爲 2 個小週期,統計每個小週期的訪問數量,則可以看到,第一個時間週期內,訪問數量爲 75,第二個時間週期內,訪問數量爲 100,如果一個時間週期內所有的小週期總和超過 100 的話,則會觸發限流策略。

Sentinel 的實現 和 TCP 滑窗。

接入層限流

Nginx 限流

Nginx 限流採用的是漏桶算法。

它可以根據客戶端特徵,限制其訪問頻率,客戶端特徵主要指 IP、UserAgent 等。使用 IP 比 UserAgent 更可靠,因爲 IP 無法造假,UserAgent 可隨意僞造。

limit_req 模塊基於 IP:Module ngx_http_limit_req_module (nginx.org)

tgngine:ngx_http_limit_req_module - The Tengine Web Server (taobao.org)

本地接口限流

SemaphoreJava 併發庫 的 Semaphore 可以很輕鬆完成信號量控制,Semaphore 可以控制某個資源可被同時訪問的個數,通過 acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可。

假如我們對外提供一個服務接口,允許最大併發數爲 40,我們可以這樣:

private final Semaphore permit = new Semaphore(40, true);
public void process(){
try{
    permit.acquire();
    //TODO 處理業務邏輯
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    permit.release();
}
}

具體的 Semaphore 實現參考源碼。

分佈式接口限流

使用消息隊列

不管是用 MQ 中間件,或是 Redis 的 List 實現的消息隊列,都可以作爲一個 緩衝隊列 來使用。思想就是基於漏斗算法。

當對於一個接口請求達到一定閾值時,就可以啓用消息隊列來進行接口數據的緩衝,並根據服務的吞吐量來消費數據。

服務降級

在接口做好風控的前提下,發現了接口請求的併發量迅速上升,我們可以啓用兜底方案,進行服務降級。

一般服務降級應該用來對一些 不重要 或 不緊急 的服務或任務進行服務的 延遲使用暫停使用

降級方案

停止邊緣業務

比如淘寶雙 11 前,就不可以查詢三個月前的訂單,對邊緣業務進行降級,保證核心業務的高可用。

拒絕請求

在接口請求併發量大於閾值,或是接口出現大量失敗請求等等突發情況,可以拒絕一些訪問請求。

拒絕策略

隨機拒絕:隨機拒絕超過閾值的請求 。拒絕舊請求:按照請求的時間,優先拒絕更早收到的請求。拒絕非核心請求:根據系統業務設置核心請求清單,將非核心清單內的請求拒絕掉。

恢復方案

在實現服務降級之後,對於突增流量我們可以繼續註冊多個消費者服務來應對併發量,之後我們再對一些服務器進行慢加載。

降級具體實現參考其他文章。

數據緩存

在接口做好風控的前提下,發現了接口請求的併發量迅速上升,我們可以分以下幾個操作執行:

  1. 對訪問請求使用分佈式鎖進行阻塞。

  2. 在這個短時間中,我們可以將對應操作行的熱點數據,緩存在緩存中間件中。

  3. 放行請求後,讓所有請求優先操作緩存數據。

  4. 再將操作的結果通過消息隊列發送給消費接口慢慢消費。

緩存問題

假設我們操作的是一個庫存接口,此時數據庫中只有 100 個庫存。

那假如此時我們將一條數據放入緩存中,如果所有的請求都來訪問這個緩存,那它還是被打掛,我們該怎麼操作?

讀寫分離

第一種想法,讀寫分離。

使用 Redis 的哨兵集羣模式來進行主從複製的讀寫分離操作。讀的操作肯定大於寫操作,等庫存被消費到 0 時,讀操作直接快速失敗。

負載均衡

第二種想法,負載均衡。

在緩存數據後,如果所有請求都來緩存中操作這個庫存,不管是加悲觀鎖還是樂觀鎖,併發率都很低,此時我們可以對這個庫存進行拆分。

我們可以參照 ConcurrentHashMap 中的 counterCells 變量的設計思想,將 100 個庫存拆分到 10 個緩存服務中,每個緩存服務有 10 個緩存,然後我們再對請求進行負載均衡到各個緩存服務上。

但是這種方式會有問題,如果大部分用戶被 hash 到同一個緩存上,導致其他緩存沒有被消費,卻返回沒有庫存,這是不合理的。

page cache

第三種想法,page cache。

大部分軟件架構其實都用到了這種方法,比如 linux 內核的硬盤寫入、mysql 的刷盤等等,即將短時間內的寫操作聚合結果寫入,所有的寫操作在緩存內完成。

作者:舍其小夥伴

來源:blog.csdn.net/weixin_44414492/article/details/123027974

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