抽獎系統的流量削峰方案
如果觀看抽獎或秒殺系統的請求監控曲線,你就會發現這類系統在活動開放的時間段內會出現一個波峰,而在活動未開放時,系統的請求量、機器負載一般都是比較平穩的。為了節省機器資源,我們不可能時時都提供最大化的資源能力來支援短時間的高峰請求。所以需要使用一些技術手段,來削弱瞬時的請求高峰,讓系統吞吐量在高峰請求下保持可控。
最近在做一個小型的抽獎系統,使用者中獎之後需要呼叫轉賬介面進行虛擬金的轉賬。轉賬介面有頻控的邏輯,因此不能把抽獎瞬間的大量請求都發往轉賬系統,必須對請求進行削峰。削峰的方式有很多種,下面就來簡單地聊一下。
請求排隊
削峰最常用的一種方式是請求排隊。瞬時的請求量太大,那麼就把這些請求先排隊存起來,再依據系統所能提供的消費能力按需消費。在量小的時候,抽獎與發貨這兩個動作可以是同步的(如下左圖),這是一種緊耦合系統,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等訊息佇列,本就是為非同步化訊息消費、應用解耦、流量消費而設計。業務根據需求加以選型即可。