簡單的活動抽獎算法&方案



前言

只要是有營銷的場景,抽獎可以說幾乎是必不可少的功能,如何基於一個簡單的抽獎邏輯去支撐種類繁多的抽獎方案,結合之前的經驗,總結如下。

原理

其實不論上層的抽獎方案是什么(例如,大轉盤,刮刮樂,扎氣球、砸金蛋等),都只是展示層的提現形式不一樣,底層都可以使用同一個抽獎算法。

想想,如果是線下舉辦抽獎,一般會有哪些方案?

  1. 可預估獎品數
    主動式,抽獎券500份,其中有獎品的只有10份,然后給用戶抽,抽中就是你的。例如:買汽水的瓶蓋抽獎,刮刮樂。
    被動式,帶ID的抽獎券500份,給用戶抽,然后系統隨機抽取10個ID發放獎品。例如:發布會入場券抽獎

  2. 不可預估獎品數
    用戶自己填信息的抽獎券,到時候由系統隨機生成一個數,比對一致的就即為中獎者。例如:彩票。

其實線上的抽獎算法,基本上也是基於模擬線下場景方案來模擬的。但絕大多數場景都是黑盒操作,執行抽獎,中間的過程用戶是無法獲知的。

算法

1、隨機區間法
這個方法隨機度高,根據概率論來計算,每個用戶的單次中獎概率為中獎概率=獎品數/預估抽獎用戶人數

如圖所示,上面是一個獎品的分配區間,例如預計抽獎100W人,1等級1個,2等獎3個,3等獎5個,4等獎10個,其余999981都是謝謝惠顧。用戶抽獎的時候,獲得一個隨機數,判斷是否在中獎區間即可。發放獎品,則區間內的獎品剩余數-1;回收獎品,則區間內的獎品+1。


當一個獎品被抽完之后,從獎品區間移除(謝謝惠顧一般不算獎品),其余繼續抽獎,例如上圖的10個4等獎被抽光了。而當所有獎品都抽光了,就會只剩下一個謝謝惠顧的區間,這樣用戶不論怎么抽,都只會是謝謝惠顧,直到活動日結束。如果需要限制總抽獎次數,則將謝謝惠顧的部分也納入庫存,最終庫存全部消耗完,隨即提示用戶抽獎結束即可。

需要注意的是,評估預計抽獎的人數比較重要(影響到隨即數生成區間),我們可以根據歷史數據評估,如果不是很清楚,建議評估人數大一些,這樣獎品不至於很快被抽完。

2、自增匹配法

此方法簡單至極,先設一個全局自增數,然后每個獎品我們設一個數字,有幾個獎品設幾個數,每次用戶抽獎,自增數加一返回,如果自增數此時與獎品的數字一致,則中獎。
好處是,不用記錄獎品的剩余數,只用記錄自增數。
缺點是,由於不用記錄獎品的剩余數,是因為提前進行了分布,所以獎品多的情況不適用。

庫存操作

曾經使用mysql的時候,需要使用事務、消息隊列,來保證並發導致的數據一致性問題。直到后來改為使用redis。
得益於redis的原子性操作和極高的性能,在高並發情況下也能很快的處理相應的庫存增減操作(redis同樣適用於秒殺場景)。

php實現

隨機區間法

<?php

namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
    public function index()
    {
        $people = 1000000;
        $prizes = [
            ['id' => 0, 'name' => '遺憾,您未抽中任何獎品'],//沒抽中
            ['id' => 1, 'name' => '一等獎,iPhone X', 'num' => 1],
            ['id' => 2, 'name' => '二等獎,華為Mate10', 'num' => 3],
            ['id' => 3, 'name' => '三等獎,三星note8', 'num' => 5],
            ['id' => 4, 'name' => '四等獎,一加5', 'num' => 10],
        ];
        $this->prize_draw($people, $prizes);
    }

    /**
    * 抽獎
    * @param $people 預估的抽獎人數
    * @param $prizes 讀取獎項設置,可以從數據庫讀,記得先在redis中初始化庫存
    */
    private function prize_draw($people, $prizes)
    {
        $redis = get_redis();
        foreach ($prizes as $key => $value) {
            if ($prizes[$key]['id'] != 0) {
                $count = $redis->get('prize:count:' . $value['id']);
                if ($count !== false && $count <= 0) {
                    //檢查是否有剩余,沒有則減去這部分區間
                    $people = $people - $prizes[$key]['num'];
                    unset($prizes[$key]);
                }
            }
        }

        //重新下標數組
        $prizes = array_values($prizes);
        dump($prizes);

        $rate = [];

        //計算區間
        foreach ($prizes as $key => $item) {
            if ($key == 0)
                $rate[$key] = [0, $item['num']];
            else if ($key == count($prizes) - 1)
                $rate[$key] = [$rate[$key - 1][1], $people];
            else
                $rate[$key] = [$rate[$key - 1][1], $rate[$key - 1][1] + $item['num']];
        }
        dump($rate);

        //抽獎
        $rd = mt_rand(0, $people);
        dump($rd);

        foreach ($rate as $key => $item) {
            if ($item[0] <= $rd && $rd < $item[1]) {
                if ($prizes[$key]['id'] != 0) {
                    $newcount = $redis->incrBy('prize:count:' . $prizes[$key]['id'], -1);
                    if ($newcount !== false && $newcount >= 0) {
                        return $prizes[$key];
                    }
                }
                return $prizes[0];
            }
        }
    }
}

概率測試

//模擬100W次抽獎隨機數分布
for ($i = 0; $i < 1000000; $i++) {
    $rd = mt_rand(0, $people);
    foreach ($rate as $key => $item) {
        if ($item[0] <= $rd && $rd < $item[1]) {
            if ($prizes[$key]['count']) {
                $prizes[$key]['count'] += 1;
            } else {
                $prizes[$key]['count'] = 1;
            }
        }
    }
}
dump($prizes);
//模擬100W次抽獎隨機數分布結果
array(5) {
  [0] => array(4) {
    ["id"] => int(1)
    ["name"] => string(20) "一等獎,iPhone X"
    ["num"] => int(1)
    ["count"] => int(1)
  }
  [1] => array(4) {
    ["id"] => int(2)
    ["name"] => string(24) "二等獎,華為Mate10"
    ["num"] => int(3)
    ["count"] => int(2)
  }
  [2] => array(4) {
    ["id"] => int(3)
    ["name"] => string(23) "三等獎,三星note8"
    ["num"] => int(5)
    ["count"] => int(6)
  }
  [3] => array(4) {
    ["id"] => int(4)
    ["name"] => string(19) "四等獎,一加5"
    ["num"] => int(10)
    ["count"] => int(8)
  }
  [4] => array(3) {
    ["id"] => int(0)
    ["name"] => string(33) "遺憾,您未抽中任何獎品"
    ["count"] => int(999982)
  }
}

其中,模擬100W次抽獎僅僅是計算了100W次隨機數的分布,多次運行,可以看到概率基本上是完全符合預期的。

自增匹配法

<?php

namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
    public function index()
    {
        $prizes = [
            ['id' => 0, 'name' => '遺憾,您未抽中任何獎品'],//沒抽中
            ['id' => 1, 'name' => '一等獎,iPhone X', 'num' => [54]],
            ['id' => 2, 'name' => '二等獎,華為Mate10', 'num' => [100, 386, 999]],
            ['id' => 3, 'name' => '三等獎,三星note8', 'num' => [798, 6333, 48795]],
            ['id' => 4, 'name' => '四等獎,一加5', 'num' => [159, 357, 8432, 789456, 123147, 256528, 764565, 999663, 744121, 546478]],
        ];
        dump($this->prize_draw($prizes));
    }

    /*
    * 抽獎
    * @param $prizes
    */
    private function prize_draw($prizes)
    {
        $nothing = $prizes[0];
        unset($prizes[0]);
        $redis = get_redis();
        $count = $redis->incr('prize:count');
        foreach ($prizes as $prize) {
            foreach ($prize['num'] as $num) {
                if ($num == $count) {
                    return $prize;
                }
            }
        }
        return $nothing;
    }
}

以上,簡單的營銷場景完全夠用。

如有更好的方法,歡迎討論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM