首發於 樊浩柏科學院
需求:首先用戶經過以必定方式(好友點贊等)開啓抽獎資格,而後按照用戶 100% 中獎機率進行抽獎,且系統的發放獎品須要按照各個獎品總體的指望中獎比例來進行分佈,最後用戶抽中獎品調用第三方發放接口發放獎品並記錄保存,另有些獎品存在發放數量限制。html
整個抽獎過程是同步進行,因爲前置了開啓抽獎資格保護,會避免用戶集中進行抽獎,故系統併發量並不會過高。突出的問題主要有如下幾個:併發
1)因爲同步調用第三方接口發放獎品,獎品可能發放失敗;
2)有一些獎品存在數量限制,可能已經發放完;
3)系統要求用戶 100% 抽中獎品;
4)系統要求各個獎品總的發放狀況符合預期的比例分佈;函數
針對以上突出問題,給出針對的解決辦法。this
核心思想是採用隨機函數 mt_rand() 來模擬用戶抽獎。編碼
獎品信息以下:code
//全部獎品信息 $allPrizes = [ 'jd' => ['name' => '京東券', 'probability' => 30], 'film' => ['name' => '電影票', 'probability' => 10], 'tb' => ['name' => '淘寶券', 'probability' => 60], ]
方式一 htm
這是一個比較中規中矩的方式,主要思想 是:將全部獎品按照指望比例分佈,一段一段小區間分佈到 1~100 這個區間,而後隨機一個 1~100 的隨機數,若是這個隨機數落在某段區間,則表示抽取對應區間的獎品。排序
1 30 10 60 1|-----------|------|----------------------|100 京東券 電影票 淘寶券
代碼以下:接口
/** * 按照機率抽取一個獎品, 返回獎品 * @param array $prizes 全部獎品的probability機率總和應該爲100 * @return mixed */ private function randPrize(array $prizes) { //總機率基數 $totalProbability = array_sum(array_column(array_values($prizes), 'probability')); if (100 !== $totalProbability) { throw new Exception('invalid probability config'); } $rand = mt_rand(1, 100); $cursor = 0; $id = ''; while(list($key, $item) = each($prizes)) { if ($rand > $cursor && $rand <= $cursor + $item['probability']) { $id = $key; break; } $cursor += $item['probability']; } unset($prizes[$id]['probability']); return $prizes[$id] + ['id' => $id]; }
方式二get
該方式若是直接看代碼比較難理解。主要思想:按照給定順序(按照獎品配置順序),前後一個一個抽取獎品,直到抽中一個獎品爲止, 抽中後續獎品的機率的前提是沒有抽中當前獎品,屢次抽取機率應該相乘。
例如:
次數 獎品 機率 基數 中獎機率 未中獎機率 1 京東券 30 100 30/100 70/100 2 電影票 10 70 (70/100)*(10/70) (70/100)*(60/70) 3 淘寶券 60 60 (70/100)*(60/70)*(1) 1-(70/100)*(60/70)*(1)
/** * 按照機率抽取一個獎品, 返回獎品, * @param array $prizes 參與抽獎的獎品信息, 全部獎品的probability機率總和應該爲100 * @return array */ private function randPrize(array $prizes) { //總機率基數 $totalProbability = array_sum(array_column(array_values($prizes), 'probability')); if (100 !== $totalProbability) { throw new Exception('invalid probability config'); } //能夠考慮按照機率倒序排序 /*uasort($prizes, function(array $a, array $b) { if ($a['probability'] == $b['probability']) return 0; return $a['probability'] > $b['probability'] ? -1 : 1; });*/ //按照獎品順序依次模擬抽中獎品 $id = ''; foreach ($prizes as $key => $item) { $rand = mt_rand(1, $totalProbability); //本次抽獎的基數 if ($rand <= $item['probability']) { //表示抽中 $id = $key; break; } else { $totalProbability -= $item['probability']; //後續獎品基數減去抽過的機率, 由於抽中後一個獎品的前提是抽不中前一些獎品 } } unset($prizes[$id]['probability']); return $prizes[$id] + ['id' => $id]; }
主要包含重試機制、自動從新一輪按照機率抽獎機制、兜底機制的實現。
/** * 抽獎 * @param array $allPrizes * @return mixed */ public function draw($allPrizes) { $tryTimes = 0; $outPrize = []; $prize = []; //若是抽到有數量限制獎品且獎品也已經抽完或者抽取失敗, 最多抽獎次數 while ($tryTimes < 4) { $tryTimes++; //按照機率抽取 $prize = $this->randPrize($allPrizes); //模擬發放獎品方法 $outPrize = $this->getOnePrize($prize['id']); //抽中退出 if (!empty($outPrize)) { break; } } echo '嘗試按照機率抽取次數:' , $tryTimes, PHP_EOL; //屢次抽獎都抽中已經抽完的獎品, 則用兜底獎品兜底 $tryTimes = 0; while (!$outPrize && $tryTimes < 2) { $tryTimes++; $prize = $allPrizes['default'] + ['id' => 'default']; $outPrize = $this->getOnePrize('default'); } echo '兜底抽取次數:' , $tryTimes, PHP_EOL; if (!$outPrize) { //兜底失敗, 多是券達到上限, 或者接口down了 return false; } else { //合併獎品信息 $outPrize = $outPrize + $prize; } return $outPrize; }
抽樣方法
public function sample($all, $times) { $out = []; $count = $times; if ($times > 1000000) return; while ($times) { $times--; $prize = $this->draw($all); if (!isset($out[$prize['id']])) { $out[$prize['id']] = 0; } $out[$prize['id']]++; } array_walk($out, function(&$value, $key) use ($count) { $value = ($value / $count * 100); }); ksort($out); return $out; }
抽樣結果
//指望機率 array(3) { ["film"] => int(10) ["jd"] => int(30) ["tb"] => int(60) } //抽樣2000次 array(3) { ["film"] => string(4) "9.8" ["jd"] => string(6) "31.35" ["tb"] => string(6) "58.85" }
嘗試按照機率抽取次數: 3 兜底抽取次數: 0 抽中獎品爲:array(3) { ["name"] => string(20) "淘寶50元消費券" ["content"] => string(12) "WD84-3233-21" ["id"] => string(2) "tb" }