高併發紅包總體設計方案

公司前段時間根據業務方需求須要作一個搶紅包的活動,網上也搜索了不少資料。記錄下總體的設計思路以及運營過程當中的各類問題。php

產品需求:

1.紅包支持配置開始時間、結束時間、類型(隨機金額或固定金額)、單個最小紅包金額、單個最大紅包金額html

2.可領取紅包的業務條件(根據業務信息指定某些知足條件的人能夠搶)
QQ截圖20180507142703.png前端

設計思路:

難點1:紅包算法(根據紅包配置最大、最小金額、數量生成符合條件的紅包集合)java

由於紅包有配置單個紅包的最大和最小金額,因此不能徹底使用隨機分配的方式。mysql

因此要求:nginx

    * 單個紅包金額既要大於最小金額,又要小於最大金額
    * 根據紅包總金額和個數要正好將錢分完web

* 單個紅包精確到分,也就是小數點後兩位

實現代碼:ajax

/*
    * @todo 設置隨機紅包金額
    * return array
    */
    public function setRandMoney()
    {
        $result = [];
        //取小數點後兩位將金額乘100
        $this->total = $this->total * 100;//紅包總金額
        $this->min = $this->min * 100;//單個紅包最小金額
        $this->max = $this->max * 100;//單個紅包最大金額
        //獲取紅包平均金額
        $average = $this->total / $this->num;

        for ($i = 0; $i < $this->num; $i++) {
        //由於小紅包的數量一般是要比大紅包的數量要多的,由於這裏的機率要調換過來。
        //當隨機數>平均值,則產生小紅包
        //當隨機數<平均值,則產生大紅包
            if (rand($this->min, $this->max) > $average) {
        // 在平均線上減錢
                $temp = $this->min + $this->xRandom($this->min, $average);
                $result[$i] = $temp;
                $this->total -= $temp;
            } else {
        // 在平均線上加錢
                $temp = $this->max - $this->xRandom($average, $this->max);
                $result[$i] = $temp;
                $this->total -= $temp;
            }
        }
        // 若是還有餘錢,則嘗試加到小紅包裏,若是加不進去,則嘗試下一個。
        while ($this->total > 0) {
            for ($i = 0; $i < $this->num; $i++) {
                if ($this->total > 0 && $result[$i] < $this->max) {
                    $result[$i]++;
                    $this->total--;
                }
            }
        }
      // 若是錢是負數了,還得從已生成的小紅包中抽取回來
        while ($this->total < 0) {
            for ($i = 0; $i < $this->num; $i++) {
                if ($this->total < 0 && $result[$i] > $this->min) {
                    $result[$i]--;
                    $this->total++;
                }
            }
        }
        if (!empty($result)) {
            //將紅包放入隊列之中
            foreach ($result as $val) {
                $this->redis->lPush($this->redpack_money_queue . $this->act_id, $val / 100);
            }
            return ['code' => '0', 'msg' => 'success'];
        }
        return ['code' => '1', 'msg' => '建立紅包失敗,請檢查參數'];

    }


    /**
     * 生產min和max之間的隨機數,可是機率不是平均的,從min到max方向機率逐漸加大。
     * 先平方,而後產生一個平方值範圍內的隨機數,再開方,這樣就產生了一種「膨脹」再「收縮」的效果。
     */
    private function xRandom($bonus_min, $bonus_max)
    {
        $sqr = intval($this->sqr($bonus_max - $bonus_min));
        $rand_num = rand(0, ($sqr - 1));
        return intval(sqrt($rand_num));
    }

    private function sqr($n)
    {
        return $n * $n;
    }

由於取最小和最大金額之間隨機數的時候使用了intval()函數致使該算法只能處理整數,故在處理的時候將金額乘100 ,在最後入隊列的時候再將其 除100,這樣就將其精確到小數點後兩位。redis

難點2:高併發時對服務器的訪問壓力
相似搶紅包、1元搶購,秒殺等業務場景都是在同一時間大量請求堆積到服務器,從而致使服務器資源緊張,程序處理不過來。那麼咱們要作的就是將流量控制住,不讓大量的請求透過web服務器直接打到數據庫層。那麼從用戶訪問url到收到返回結果總體流程是什麼樣子呢?算法

  • 客戶端層,用戶在微信中打開URL,DNS解析域名至服務器
  • web服務器層, Apache、Nginx或Tomcat等
  • 服務器層,分配php-fpm進程,代碼接收參數進行邏輯處理
  • 數據持續化層次,將結果保存至mysql或Redis層次

客戶端層優化方案:(限流)

  1. 前端URL使用html靜態頁面顯示內容,並將頁面顯示圖片儘可能壓縮,減小服務器帶寬壓力。推薦使用base64解碼圖片
  2. 使用鏈接池控制流量,用戶點擊搶紅包時,發起ajax請求,調用後臺使用java寫的redis incr 接口,每次調用則鍵值 +1,並將自增id返回,當後臺代碼處理完後再將其鍵值減掉,由於incr自增爲原子級別,因此前端能夠根據當前有多少用戶在等待中。 根據自身服務器配置以及業務場景預估N多請求會致使服務器出現問題,若是當前等待處理的請求數大於N則前端提示用戶 "當前請求過多,請稍後再試",反之則能夠正常發起請求。

Web層優化方案(lua+nginx實現頻率控制)

  1. Nginx來處理訪問控制的方法有多種,實現的效果也有多種,訪問IP段,訪問內容限制,訪問頻率限制等。

用Nginx+Lua+Redis來作訪問限制主要是考慮到高併發環境下快速訪問控制的需求。

Nginx處理請求的過程一共劃分爲11個階段,分別是:

`post-read、server-rewrite、find-config、rewrite、post-rewrite、 preaccess、access、post-access、try-  files、content、log`.


在openresty中,能夠找到:

`set_by_lua`,`access_by_lua`,`content_by_lua`,`rewrite_by_lua`等方法。

那麼訪問控制應該是,`access`階段。

2.根據請求的ip段來控制訪問流量,每次接收到搶紅包的url後將redis鏈接池中id自增,當超過某個峯值時跳轉到等待頁。
具體配置方案參考:http://homeway.me/2015/08/11/...

php代碼層(防止出現發多、重複領取、權限等狀況)

  1. 使用redis queue 隊列功能來控制超發的狀況,將每一個算出來的小紅包lpush至隊列中,每次收到請求後消費最後一個小紅包,由於redis的的隊列爲阻塞模式,因此當隊列中爲空時是不返回數據的,也就能夠保證出現併發時不會一個紅包分配給多人。
  2. 使用 redis list集合來控制重複領取的狀況,每次接收到請求後將用戶id放置已領取的集合中(這點很重要,必定要在消費隊列前放置集合中,要不會出現由於併發致使重複領取),消費成功則跳出,反之則將其移出已領取集合。
  3. 由於業務需求處理起來很繁瑣,因此在活動建立的時候就根據活動規則將可領取的人員放置集合中,權限判斷可使用待領取集合來控制。

如下爲個人代碼實現(小菜一枚,大神勿噴)

/*
    * @todo 獲取紅包金額
    * @return array
    */
    public function doRush()
    {
        $act_info = $this->getPackInfo($this->act_id);
        if(empty($act_info)){
            return ['code'=>'1','msg'=>'活動信息錯誤,請聯繫管理員'];
        }
        if($act_info['start_time'] > now()){
            return ['code'=>'2','msg'=>"紅包還沒有開搶,請稍後再試"];
        }

        if($act_info['end_time'] <= now()){
            return ['code'=>'1','msg'=>'活動已結束'];
        }
         //將請求用戶先放置已領取的集合中
        if(!$this->redis->sAdd($this->rushed_list_key,$this->user_id)){
            return ['code'=>'1','msg'=>'每一個紅包只能領取一次哦'];
        }
        $money = $this->redis->lPop($this->redpack_money_queue);
        if(empty($money)){
            $this->redis->sRem($this->rushed_list_key,$this->user_id);
            return ['code'=>'1','msg'=>'您來完了呦,紅包已搶光'];
        }

        //將已搶的用戶和金額記錄至隊列中
        $add_res = $this->amountAdd($money);
        if($add_res['code'] != 0){
            return ['code'=>'1','msg'=>'系統繁忙,請稍後再試'];
        }
        return ['code'=>'0','msg'=>'success','data'=>$money.'元'];

    }

數據層(使用異步持續化)

  1. 用戶領取成功後,將用戶id及領取的金額存至已領取的redis queue中,異步進程根據其中的user_id和money值將其數據更新至mysql表中

--------------------------------------------------我是萬惡的分割線------------------------------------------------------------------

補充說明:
本人第一次將實際開發過程以及想法落實到書面上,對於我這種小菜來講已經很不錯了,懇求各位大神勿噴。其中紅包算法和一些處理方案也是第一次接觸,參考了網上不少資料,學到了不少。若是你有更好的方案的話多多交流~~

----PHP小菜一枚------
相關文章
相關標籤/搜索