最近修改了網站的抽獎算法,使得抽獎看起來更加『公平』,爲此我整理了下,談談在抽獎系統設計中的『坑』。javascript
抽獎分爲兩種:java
知道總人數mysql
不知道總人數算法
14 個獎品分給 500 我的:sql
獎品分爲一等獎、二等獎、三等獎;數據庫
總人數 500 人。網絡
獎品 | 數量 |
---|---|
一等獎 | 1 |
二等獎 | 3 |
三等獎 | 10 |
設計思路:dom
爲 500 人設計序號,1 - 500;函數
生成中獎序列(僞代碼)網站
// 中獎序號, 大獎爲最後一個 awardIds = List() // random 出序號 while (awardIds.size() < 14) { rand = Math.ceil(random() * 500); // 取整 // 限不限制均可以,機率過低 awardIds.contains(rand) ? awardIds.push(rand); }
這種設計下,每一個人的中間機率都是 1/500 之一。
這樣就選出了中獎列表了,然而有『坑』,後面再說這個問題。
設計一個簡單的開放人數抽獎系統。
獎品分爲一等獎、二等獎、三等獎;
保證每一個人的抽獎機率一致。
獎品 | 數量 | 機率 |
---|---|---|
一等獎 | 1 | 0.001 |
二等獎 | 3 | 0.005 |
三等獎 | 10 | 0.01 |
利用程序自帶的 random
函數,很容易生成一個 0 ~ 1
之間的隨機數,能夠直接這樣設計:
獎品 | 中獎區間 |
---|---|
一等獎 | [0, 0.001) |
二等獎 | [0.001, 0.006) |
三等獎 | [0.006, 0.016) |
只要 random()
落在了哪一個區間,就中了幾等獎。
這種狀況下由於是對區間取值,因此每次抽獎的機率都是同樣的。
說完栗子,下面來講說 random
函數。
大部分語言都自帶 random 函數,然而都只是僞隨機,是由可肯定的函數(經常使用線性同餘),經過一個種子( 經常使用當前時間),產生的僞隨機數。
這意味着:若是知道了種子(seed),或者已經產生的隨機數,均可能得到接下來隨機數序列的信息。便可以預測。
因爲各個語言實現 random 的方式不同,因此栗子 一、2 會有不一樣的問題存在。
栗子 1 中可能會出現種子沒有更改的狀況,這種狀況下生成的序列能夠預測,設計沒有問題,而結果變成了『坑』。而若是種子時間相關就能夠避免這個問題。
栗子 2 中若是同一時間有多個用戶同時開始抽獎,這個時候時間相關的種子又會帶來問題,即:只要這幾個用戶有一箇中獎,就意味着這個時間點的用戶都中獎了。
程序並不能產生真正的隨機數,可是能夠產生理論上的真隨機數。
如 UNIX 中的 /dev/random
能夠當作是一個真隨機的生成器。
具體來說就是生成器有一個容納噪聲數的熵池,在讀取時,/dev/random
設備會返回小於熵池噪聲總數的隨機字節,好比任何硬件的狀態變化,IO 響應時間、磁盤讀寫速度、中斷、網絡變化等等,都會做爲熵反饋給生成器,因此理論上就產生了不可預測性。
若是程序中 random 並不能知足隨機要求,能夠爲種子設計一個熵,將不可預知的事件收集起來,做爲種子產生隨機數。
如今咱們有了真隨機函數,而後栗子 2 依然不是一個可用的抽獎系統,依然有一些問題。
重複中獎
同一個用戶重複中一種獎品?
必然要限制,否則很容易被認爲有 py 交易。
同一個用戶重複中多個獎品?
限制,虛擬獎品(積分等)無所謂,實體獎品限制。
獎品放出
抽獎開始就放出全部獎品?
因爲沒法預估人數,機率很難調整。
抽獎開始後獎品一段時間一段時間的放出。
防止大量的人都在放出的點抽取,而送光獎品。
區間清理
若是某個獎品被抽光了,是否剔除該獎品的區間?
剔除不剔除都不要緊,爲了簡化處理(偷懶),其實是能夠不用理會的。
在保持核心中獎邏輯不變(機率區間)的狀況下,爲栗子 2 添加限制。
設計了獎品(award)以後,再添加一張 具體獎品(cdkey),使用 user 表示中獎用戶,默認值 null。
字段 | 類型 | 默認值 |
---|---|---|
... | ... | ... |
user | int、UNIQUE | null |
若是使用 MySQL ,MySQL 的 UNIQUE 限制並不校驗 null 值,因此在限制惟一中獎上並不用單獨處理。
若是不作 UNIQUE 限制,那麼就須要手動查詢了:
# SQL 示例, 一次選出兩條,檢查用戶是否中獎過 SELECT * FROM ckdey WHERE award = ? AND (user = ? OR user = null) LIMIT 2;
爲了限制獎品的放出時間,能夠在 cdkey 上再添加 ready 表示放出時間:
獎品 | ready |
---|---|
一等獎 1 | 08:00:00 |
二等獎 1 | 08:00:00 |
二等獎 2 | 12:00:00 |
二等獎 3 | 16:00:00 |
三等獎 1 | 08:00:00 |
... | ... |
這種方法在獎品數少的時候特別好使,並且還能夠指定開獎時間,然而當獎品數不少的時候,就沒那麼好使了。
當不須要使用指定時間時,徹底能夠利用 random 的特性來隨機生成時間,不須要寫入數據庫。
在 award 上增長 count(總數) 和 remain (剩餘數量)
以 remain 的線性函數做爲隨機數種子,則每次隨機數的序列都是同樣的。
# 等分抽獎時間 average = Math.ceil((expirationTime - startTime) / count); # 相同的種子獲得生成器 rand = seedrand(transform(remain)) # 獲取獎品放出時間 readyTime = expirationTime + Math.ceil(average * rand()) - average * remain;
這樣對於每一個用戶,每次放出的時間都是相同的(不看源碼也不知道是何時),每次放出一個獎品,第一段時間沒送出去,就累積到第二段時間,依次後推。
PS: 這種設計下,中獎的人真的是運氣爆表,萬年非洲人表示很憂傷!!