若是觀看抽獎或秒殺系統的請求監控曲線,你就會發現這類系統在活動開放的時間段內會出現一個波峯,而在活動未開放時,系統的請求量、機器負載通常都是比較平穩的。爲了節省機器資源,咱們不可能時時都提供最大化的資源能力來支持短期的高峯請求。因此須要使用一些技術手段,來削弱瞬時的請求高峯,讓系統吞吐量在高峯請求下保持可控。mysql
最近在作一個小型的抽獎系統,用戶中獎以後須要調用轉帳接口進行虛擬金的轉帳。轉帳接口有頻控的邏輯,所以不能把抽獎瞬間的大量請求都發往轉帳系統,必須對請求進行削峯。削峯的方式有不少種,下面就來簡單地聊一下。redis
削峯最經常使用的一種方式是請求排隊。瞬時的請求量太大,那麼就把這些請求先排隊存起來,再依據系統所能提供的消費能力按需消費。在量小的時候,抽獎與發貨這兩個動做能夠是同步的(以下左圖),這是一種緊耦合系統,SVR B的處理能力必須跟得上SVR A的處理能力。當SVR A 與SVR B 存在處理能力差別時,能夠引入消息隊列,把對服務的同步調用轉化成對隊列的異步消費。sql
能夠用來做爲隊列的工具備不少,典型的如Message Queue消息隊列,也能夠利用數據庫Mysql或是Redis來實現分佈式隊列,跟進業務場景來自行進行選擇。例如,我在實現抽獎系統的時候,使用的是Mysql,緣由是SVR A已經把用戶的抽獎信息落地到的數據庫,那麼SVR B就能夠利用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的list數據結構提供了BLPOP和BRPOP,表示列表的阻塞式彈出。BLPOP的BRPOP的區別僅僅在取元素的位置不一樣。使用方式爲:設計
BRPOP key timeout
當給定的列表內沒有任何元素可供彈出的時候,鏈接將被阻塞,直到等待超時或發現可彈出的元素爲止,超時參數 timeout 接受一個以秒爲單位的數字做爲值。超時參數設爲 0 表示阻塞時間能夠無限期延長。相同的key能夠被多個客戶端同時阻塞,不一樣的客戶端會被放進一個隊列中,按照【先阻塞先服務】的順序爲key執行BRPOP 命令。利用這個特色,能夠來實現一個輕量級的消息隊列服務。
例如kafka、ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ等消息隊列,本就是爲異步化消息消費、應用解耦、流量消費而設計。業務根據需求加以選型便可。