1. 程式人生 > >工作小記——小秒殺活動

工作小記——小秒殺活動

累加 不用 都去 發送請求 col log 腳本 指定 動靜

公司緊急要求搞一個促銷活動:

活動到時放出banner,點進去未登錄的需先登錄,已領取過直接顯示已領取。未領取過的話,點“送祝福”,隨機彈出一條祝福語,再點“領紅包”,則發一個紅包。總共1900個,領完彈框“已領完”

需求還是比較簡單的,但是這個總共1900是個隱患啊,需求輕描淡寫的一筆,架構可要小心。就算需求方說不會大肆宣傳,小範圍宣傳,到時候直接出banner,看到的人才點。但是架構還是要做高並發的考慮。萬一到時大家都來搶,把服務器弄掛了,就是大事啊。

因為時間緊,所以作了一個簡單的方案。

1.單獨頁面:活動應打開新的單獨頁面,不在主頁進行跳轉。如果一直沒收到服務器響應,用戶也不能進行普通的投資瀏覽操作,要後退或關掉再開,體驗不好

2.文本存在前端:所有祝福語直接存在前端,隨機選取的操作由前端完成,不往後臺發送請求

3.前端判斷登錄:前端可以通過cookie判斷是否登錄,不用請求後臺。未登錄直接跳轉至登錄頁面

4.延後判斷領取狀態:進入活動頁後,不先判斷是否領取過。每一次進入頁面,都要發請求給後臺檢查登錄狀態,平白無故多出一倍請求,且無意義。等用戶點領取再判斷並返回結果就可以了

5.先插流水,後發放:沒必要在請求的時候,同步把所有的事情做完。先插流水,之後跑job根據流水表進行發放。延遲時間看業務方的接受度。

6.用緩存控制

7.用累加計數:一般來說,從0開始累加到1900,和從1900累減到0效果是一樣的。但是累加的方式,總數不用一開始寫死,整個活動過程中還能動態調整總數。而累減的方式,一開始就要初始化總數。

第五點著重聊一下:

負責開發的同事第一反應是:來新的請求時,先count(1)一下,如果<1900則插入一條流水,返回領取成功,如果>=1900則返回已領完。之後再跑job掃描流水表進行紅包的發放。

流程圖如下:

技術分享

一般單線程情況下是沒有問題,可是並發的話,比如兩個線程大家都讀到count(1)為1899,判斷下都<1900,都去insert流水表,則最終1901條數據,超賣了。

開發的同事提出了第二版方案:先插入,再count(1),如果>1900,則返回失敗,等發放的時候,根據插入的時間排序,選擇1900條

流程圖如下:

技術分享

這個確實不會超賣了,但是有兩個問題:

1.會誤報信息。1899時,兩個線程同時插入表後,再count(1)=1901,就都返回“已領完”。其實一個成功,一個失敗。發放的時候不會少發,但是會錯誤提示

2.如果不加其他判斷,這樣操作,會流水表中插入遠大於1900條的數據,除1900條以為都是廢數據。

當然,可以在達到1900之後在redis中放一個標誌位,之後如果標誌位置為是,則不再insert。數據庫操作遠比redis操作慢,不如將領取數放在redis中。

考慮流程如下:

技術分享

這個方案有幾點比較好:

1.利用redis為單線程模型,實現分布式原子增減。並且在增減後獲得最準確的結果,不存在並發情況

2.直接Insert,將檢查是否已領取與插入流水合成一件事情做。不是先檢查再插入,會存在並發問題

3.計數統計訪問redis某鍵值,比count(1)性能好

由於時間非常緊急,且活動較小,暫時做了這些優化。如果時間充裕點,且活動更大點,還可以作如下改進:

1.動靜分離將靜態資源如css,js等均推送到cdn上,由cdn來扛秒殺前,頁面刷新的訪問流量

2.領取流水用redis集合可以往redis集合中添加元素(用戶id),添加成功則說明未領取過,添加失敗則說明已領取過,會比數據庫插入一條流水性能好

3.設置已領完標誌如果擔心redis集合中元素太多,則可以在到達指定條數後,置某個標誌位,之後請求先查詢該標誌位,為true時直接返回“已領完”

4.發送一條消息入MQ:如果擔心redis突然宕機,領取的用戶id集合丟失的話,則加一個往MQ裏扔一條消息的操作。異步取消息入庫。redis和mq記了兩份數據,同時宕機的概率非常小

結合,2,3,4,流程圖如下:

技術分享

5.用lua腳:根據流程圖,可以發現前面連續操作都是redis操作,只有最後是MQ。可以將多個redis操作編寫成lua腳本,並預存在redis中,用evalsha命令一步執行。性能可以進一步提升

工作小記——小秒殺活動