2017年臨近年關的時候接到開發年會紅包雨的任務,時間緊任務重且之前沒有這塊經驗。這個項目看似很簡單,如果參加年會人數少,一個晚上可以搞出demo,如果是幾萬人則難度成指數級。領導說,當天現場有幾百位參會,場外全集團估計有幾萬人,公司下屬子公司從事的業務多,這次年會基本上每家子公司都有預算,如機票,飲料券,商場代金券,積分,現金,保養券,合作方贈送的禮品等全部以紅包雨的形式發放出去,當天會有多場紅包雨活動。
經過和產品,技術一個晚上的需求討論基本上把項目拆分完畢。
系統划分大致如下
1、后台管理服務,方便年會人員的工作,比如上架獎品;設置合適的活動時間,規則;開啟活動;暫停活動;拉黑企業成員;禁止某些人搶紅包;發布公告;統一推送消息;發送短信等。
2、鑒權服務,所有進來搶紅包的用戶都需要經過這個服務,統一鑒權用戶(是否登錄,合法性,平衡性,活動狀態等),如果流量太大或者中獎人數不平衡還要根據用戶標簽(司齡,中獎次數,所屬子公司)進行限流。目的是盡可能的平均化各個子公司中獎人數。舉個例子,A子公司人數有幾千人,第一場紅包雨獎品大部分被A子公司搶走,那么就要適當對A子公司人員進行管控,第二場活動可能大部分A子公司成員在鑒權時候直接踢掉,無法進入中獎服務。
3、中獎服務,用戶通過鑒權之后,開始進入真正的搶紅包業務,所點即所得,如果中獎(log4j2+disruptor)會記錄一條詳細日志(這是最原始的中獎依據)。
4、消息消費服務,統一處理用戶中獎信息,每場紅包雨活動的獎品數量和持續時間是不一致的,比如第一場活動1分鍾,100000份獎品,如果中獎紀錄直接入庫則壓力是很大的。所以這里使用消息中間件流量削峰,異步批量存入緩存,DB入庫。
5、定時服務,提前預熱活動任務,中獎核對任務,過期作廢任務,提現任務等。比如張三的中獎日志和中獎DB數據不一致,中獎核對任務需要清洗出類似這樣的異常情況,及時通知技術,運營排查,防止出現不愉快。張三中了一張有期限的購物卡,過期了就需要作廢掉不允許兌換。
6、查詢服務,活動進行中和結束后,大量用戶會查看活動詳情和自己中獎信息,這一塊采取措施是H5本地緩存和服務端緩存,CND相結合,例如整場年會我就搶了3個紅包,那么直接將這三個中獎信息寫到本地緩存。活動未開始用戶會看到公司相應的宣傳片,公司領導講話視頻,banner等統一走CND。
7、現金紅包提現服務,因為集團下屬有自己的銀行,所以本次活動涉及錢的全部走自己的銀行,用戶提現,需要綁卡,開戶。提現需要審核,確認無誤會交給定時任務處理,一般在2個小時之內會打到綁卡的銀行賬戶。
服務按職責划分出來之后優點是按需分配節約了服務器資源,避免單點故障拖垮整個服務,缺點是增加運維負擔。
中獎邏輯
首先中獎是無規律的,即用戶點擊的紅包,誰也不確定是否中獎,誰也不確定獎品是什么,為了滿足這個條件且為了增加用戶體驗,我們采取的是先預熱活動也就是初始化活動數據。提前30分鍾預熱即將開始的活動數據到緩存隊列。主要用到了redis,quartz
例如以下表格(demo數據):
紅包場次 | 開始時間 | 結束時間 | 紅包總數 | 現金總數 | 現金總金額 | xx超市代金券總數 | xx超市代金券總金額 |
第一場 | 2017/12/25 13:30:00 | 2017/12/25 13:40:00 | 4 | 3 | 100000 | 1 | 5000 |
第二場 | 2017/12/25 14:30:00 | 2017/12/25 13:35:00 | 10 | 9 | 50000 | 1 | 20000 |
在2017/12/25 13:00定時任務會預熱第一場活動數據,發送消息到釘釘群里提醒所有相關人員。
首先sql根據時間緯度查到第一場活動,開始初始化數據,整個初始化后的數據就是一個令牌桶,每個獎就是一個令牌。它們整個對應關系如下:
這種令牌桶設計一般通用於秒殺,紅包雨,紅包等場景。整個桶使用redis的list數據結構存儲,優點是拿了就走,不需要更新庫存,效率高。
代碼邏輯
1、初始化令牌的產生,結束時間戳-開始時間戳得到活動的時間區間,用開始時間戳+時間區間的隨機數生成生成四個令牌(每個令牌對應一個獎品)並將令牌按照從小到大的順序排序,rpush到redis中,並且設置key的expire為10分鍾。這里尤其注意令牌不能重復,而按照這種設計如果時間短而獎品多的情況下可能就會重復,解決思路,如果1分鍾,1000份獎品,則0-999,那就用時間戳*1000 + redis自增(incr)不夠前面補上0,真正時間對比的時候令牌/1000 才是真正的時間戳。如下
System.out.println(new Date().getTime()); System.out.println(1584866007408L*1000+999); System.out.println(1584866007408999L/1000);
2、令牌和獎品之間是k-v存儲到redis,並且設置key的expire為10分鍾
3、用戶經過前面的鑒權邏輯,進入中獎服務,lpop拿到令牌,我們設置的中獎邏輯是,拿到令牌並不代表中獎,比如令牌1時間戳對應時間是13:31,活動是13:30開始,用戶在13:30 第10秒搶到這個令牌13:30:10 < 13:31:00(令牌)所以不算中獎,這時候用戶需要把令牌lpush回去,這里需要注意的是搶令牌,對比令牌,還回令牌必須是原子操作,可以使用分布式鎖或者lua腳本來完成,目的是防止並發情況下,拿到令牌和放回去的令牌順序錯亂。如果用戶是13:31:01秒搶到令牌1 ,系統就認為中獎,整個邏輯就是當前時間小於令牌時間戳不中獎,大於令牌時間戳中獎。
4、當前活動對應的活動策略,比如最大中獎次數,司齡中獎次數等,hset到redis中,並且設置key的expire為10分鍾
5、預熱完成,更新活動狀態,並發送消息到釘釘群提醒工作人員。用戶可搶的紅包一定是已經預熱后的活動,如果預熱環節出現問題管控平台立刻啟用備用活動頂替。
6、用戶確定中獎,直接返回獎品信息,為了節省帶寬返回的獎品信息越少越好。同時發送MQ消息,后台中獎數據異步入庫