抽獎系統的流量削峯方案

如果觀看抽獎或秒殺系統的請求監控曲線,你就會發現這類系統在活動開放的時間段內會出現一個波峯,而在活動未開放時,系統的請求量、機器負載一般都是比較平穩的。爲了節省機器資源,我們不可能時時都提供最大化的資源能力來支持短時間的高峯請求。所以需要使用一些技術手段,來削弱瞬時的請求高峯,讓系統吞吐量在高峯請求下保持可控。

​​

最近在做一個小型的抽獎系統,用戶中獎之後需要調用轉賬接口進行虛擬金的轉賬。轉賬接口有頻控的邏輯,因此不能把抽獎瞬間的大量請求都發往轉賬系統,必須對請求進行削峯。削峯的方式有很多種,下面就來簡單地聊一下。

請求排隊

削峯最常用的一種方式是請求排隊。瞬時的請求量太大,那麼就把這些請求先排隊存起來,再依據系統所能提供的消費能力按需消費。在量小的時候,抽獎與發貨這兩個動作可以是同步的(如下左圖),這是一種緊耦合系統,SVR B的處理能力必須跟得上SVR A的處理能力。當SVR A 與SVR B 存在處理能力差異時,可以引入消息隊列,把對服務的同步調用轉化成對隊列的異步消費。

​​

可以用來作爲隊列的工具有很多,典型的如Message Queue消息隊列,也可以利用數據庫Mysql或是Redis來實現分佈式隊列,跟進業務場景來自行進行選擇。例如,我在實現抽獎系統的時候,使用的是Mysql,原因是SVR A已經把用戶的抽獎信息落地到的數據庫,那麼SVR B就可以利用Mysql作爲一個隊列,來達到按能力消費的需求。

Mysql

用戶中獎的時候,SVR A 會將用戶中獎信息寫到數據庫中。SVR B按照自己的消費能力,從數據庫中把數據select出來執行轉賬的邏輯。數據庫表中的每一行記錄,都可以看作是一個等待被消費的消息。如何保證消息按序(正序或倒序)消費?可以利用update_time 來標記消息入隊時間,設定update_time字段:

 update_time timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP  DEFAULT CURRENT_TIMESTAMP COMMENT '更新時間'

必須使用一個字段來標記某行記錄的消費狀態。消費過的消息不必再select出來處理。另外,在有多個消息消費者的時候(比如有多個線程來消費數據庫中的這些中獎信息時),需要保證消息不會重複被消費。可以使用二段式提交的方式來保證。以字段present_flag來表示消費狀態,present_flag有三個取值: 0:中獎,未轉賬 1:一階段提交(即準備轉賬) 2:二階段提交(轉賬完成)

對於SVR B ,需要進行如下的操作: 步驟一:將數據庫中present_flag 爲0 的記錄按序撈取出來,這裏可以批量拉取,比如一次拉取100條記錄 步驟二:按序處理每筆中獎記錄的轉賬邏輯,調用轉賬接口之前,將present_flag設置爲1,sql中的條件是present_flag爲0; 步驟三:執行轉賬邏輯 步驟四:轉賬成功,將present_flag設置爲2,sql中條件是present_flag爲1。

這樣即使同一行記錄被多個消費者拉取出來,也能保證只有一個能夠成功執行步驟三。轉賬失敗(消費失敗) 的記錄如何處理?可以使用一個定時腳本將present_flag爲1的update成present_flag爲0,再次進行消費。

通過這種異步消費的方式,來保證中獎記錄慢慢被消費完。這種方式在極端的情況下,比如剛剛執行完步驟三 機器就掛掉了,那麼可能會出現重複消費的情況。根據業務對重複消費的容忍度來進行選擇。

Redis

Redis的list數據結構提供了BLPOP和BRPOP,表示列表的阻塞式彈出。BLPOP的BRPOP的區別僅僅在取元素的位置不同。使用方式爲:

BRPOP key timeout

當給定的列表內沒有任何元素可供彈出的時候,連接將被阻塞,直到等待超時或發現可彈出的元素爲止,超時參數 timeout 接受一個以秒爲單位的數字作爲值。超時參數設爲 0 表示阻塞時間可以無限期延長。相同的key可以被多個客戶端同時阻塞,不同的客戶端會被放進一個隊列中,按照【先阻塞先服務】的順序爲key執行BRPOP 命令。利用這個特點,可以來實現一個輕量級的消息隊列服務。

消息隊列組件

例如kafka、ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ等消息隊列,本就是爲異步化消息消費、應用解耦、流量消費而設計。業務根據需求加以選型即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章