話很少說,直接上需求描述:redis
最近須要上一期活動,這個活動是以轉盤抽獎爲形式的抽獎活動,要求每一個用戶用積分進行抽獎,且中獎率爲100%即不可出現不中任何獎品的狀況,以後,又加了一個要求,即不能實行純隨機的抽取,若是如此會產生一個極端狀況,若是開始的時候活動極其火爆因爲隨機的不可控性頭一天用戶便將全部優質獎品所有抽走,那麼後來的用戶將只會抽到保底獎品。數據庫
那麼獎品就須要按時間分佈在從活動開始到結束的時間段,其次須要作的是,在某些特殊的時間段,咱們但願多投放一些獎品給用戶抽到。安全
需求分析:數據結構
那麼開獎策略能夠爲爲每一個獎品設置開獎時間,只有在開獎後來抽獎才能抽到該獎品,不然視爲未中獎發保底獎品,咱們只須要拿當前時間與最接近獎品開獎時間對比便可。併發
由上需求,那麼就須要一個容器來存放這些獎品,對這個容器的要求:分佈式
1. 它能夠以時間軸爲維度取出獎品;高併發
2. 它能夠以時間軸爲維度放入獎品;性能
3.它能夠以時間軸爲維度將獎品排序;spa
同時,後臺應該有地方配置每一個小時應投放的獎品數量,同時爲保證配置數據能及時生效,應當是每小時前去向獎品池投放下一個小時的獎品;debug
以下圖所示,每一個獎品都有對應開獎時間,獎品1只有10000毫秒以後的請求才能夠抽到,且只有獎品1抽走以後才能夠抽獎品2;
抽獎步驟:
性能安全考慮:
顯然,抽獎是容易引起併發問題的場景,高併發狀況每每會帶來兩個問題
1. 超發問題,例如將10個獎品發給了11我的,用鎖可解決;
2.數據庫等基礎組件負載太高致使宕機,以數據庫爲例,若是每一個用戶每抽走一個獎品都去鏈接數據庫更新庫存,數據庫頗有可能承受不住(數據庫能承受的qps遠不如redis);
方案:
使用redis的zset數據結構,這裏簡單說明下zset,它是一個基於跳錶實現的有序集合,尤爲適合排序場景比較多的場景,是一個典型的用空間換取時間的數據結構。這裏咱們用開獎時間戳做爲score,保證其按照時間排序,存入的時候能夠直接將獎品ID與時間戳存入其中便可。
同時設置定時任務,每一個小時去拿下一個小時的所需的獎品,隨機將其散列在下一個小時的各個時間上,並在此時就將各個獎品庫存扣除。
ok,需求完美解決,鎖的問題直接上代碼,鎖就是保證zset的排序操做與移除操做是原子操做,不然便會出現超發,使用了redis的setNx作分佈式鎖。
/** * 抽獎 * * @param turnTableNum 轉盤編號 * @return 獎品ID */ public long getLotteryResult(long userId, int turnTableNum, Map<Long, ActivityTurntableGoodsConfig> goodsConfigMap) { Set<String> prizeSet = null; String prizeResStr = null; try { if (RedisUtils .lock(RedisKey.TURNTABLE_PRIZE_QUEUE_LOCK, String.valueOf(turnTableNum))) { Set prizeSet = RedisClusterAccessor .zrangeByScore(RedisKey.TURNTABLE_PRIZE_QUEUE, String.valueOf(turnTableNum), 0, System.currentTimeMillis(), 0,1); if (null != prizeResStr) { //在獎池中移除獎品 log.debug("{} remove prize {} {}", XGameContextHolder.get(), turnTableNum, prizeResStr); RedisClusterAccessor .zrem(RedisKey.TURNTABLE_PRIZE_QUEUE, String.valueOf(turnTableNum), prizeResStr); } } } catch (Exception e) { throw e; } finally { RedisUtils.unlock(RedisKey.TURNTABLE_PRIZE_QUEUE_LOCK, String.valueOf(turnTableNum)); } if (null == prizeResStr) { return -1; } return CommonUtil.safeParseLong(prizeResStr.split("_")[0]); }