解決Go語言中高頻次和高并發(fā)下隨機(jī)數(shù)重復(fù)的問題
一、概要:
在Golang中,獲取隨機(jī)數(shù)的方法一般會(huì)介紹有兩種,一種是基于math/rand的偽隨機(jī),一種是基于crypto/rand的真隨機(jī)。其中,math/rand由于其偽隨機(jī)的原理,經(jīng)常會(huì)出現(xiàn)重復(fù)的隨機(jī)數(shù),導(dǎo)致在需要進(jìn)行隨機(jī)的業(yè)務(wù)出現(xiàn)較多的重復(fù)問題。在作者所經(jīng)歷的實(shí)際項(xiàng)目中便遇到在高頻率和高并發(fā)下獲取隨機(jī)數(shù)時(shí)的重復(fù)問題,在解決該問題的道路上,嘗試了幾種辦法,最終發(fā)現(xiàn)了較好的解決方案。
二、對(duì)math/rand隨機(jī)數(shù)的研究:
基于math/rand獲取隨機(jī)數(shù),常見的方法是:
import ( "math/rand" "time" ) r := rand.New(rand.NewSource(time.Now().UnixNano())) result := r.Int31n(100)
上述代碼原理,是通過納秒時(shí)間來生成種子,然后通過該種子獲取一個(gè)隨機(jī)數(shù)。常用的開發(fā)者應(yīng)該知道,math/rand的種子具有其固定性,同一個(gè)數(shù)作為種子時(shí),通過New獲取的實(shí)例其實(shí)是相同的,即便它們的實(shí)例地址不同,但通過同樣的隨機(jī)數(shù)方法,獲得的值會(huì)是一樣的。
這種情況是由于math/rand會(huì)根據(jù)隨機(jī)種子進(jìn)行一個(gè)較復(fù)雜的算法過程,從而獲得一個(gè)長度為607的隨機(jī)數(shù)池,由于算法過程是固定的,不存在任何的隨機(jī)情況,因此同一個(gè)種子獲取的隨機(jī)數(shù)池是完全一致的。
此時(shí)可能會(huì)有人認(rèn)為,每次取不同的納秒時(shí)間來生成實(shí)例,不就可以解決問題?但這樣的想法在實(shí)際的項(xiàng)目實(shí)踐中是存在問題的。原因主要有兩點(diǎn):
1、Golang的一大優(yōu)點(diǎn)是速度快,當(dāng)一次性多次使用time.Now().UnixNano()獲取納秒時(shí)間時(shí),會(huì)發(fā)現(xiàn)納秒時(shí)間并不是不同的,而會(huì)存在重復(fù),這使得在高頻率隨機(jī)下隨機(jī)種子是一樣的。
2、Golang的一大特點(diǎn)是高并發(fā),但是在實(shí)際的運(yùn)行過程中,是無法保證兩個(gè)并發(fā)的協(xié)程不會(huì)在同一時(shí)間獲取時(shí)間的,同樣會(huì)出現(xiàn)重復(fù)問題。
針對(duì)這兩個(gè)問題也有對(duì)應(yīng)的解決方案:
1、對(duì)于第一個(gè)原因,可以在每次獲取納秒時(shí)間前,使用time.Sleep(time.Nanosecond)保證程序過一個(gè)時(shí)間再取,如此可以保證每次獲取的時(shí)間是不重復(fù)的。
2、對(duì)于第二個(gè)原因,可以利用鎖將獲取時(shí)間的過程鎖住,以保證并發(fā)時(shí)不在同一時(shí)間執(zhí)行。
然而在實(shí)際項(xiàng)目中實(shí)踐的情況并不理想。
第一個(gè)解決方案下,由于CPU處理的時(shí)間片,跳過的時(shí)間并不是1納秒,不同的CPU會(huì)有微小差異。作者在i7-13700K的CPU下測試,跳過前后的時(shí)間差大約為0.1ms。
第二個(gè)解決方案下,倘若不結(jié)合第一個(gè)解決方案,因?yàn)闊o法保證鎖前和鎖后的時(shí)間是否完全不一的情況,因此需要保留sleep過程,然而可以計(jì)算的是,若并發(fā)量達(dá)到了10000,僅是獲取種子的最長等待時(shí)間就可能達(dá)到1s,加上一般還會(huì)有其它業(yè)務(wù)邏輯,這難以保證高效的需求。
上述方案均存在隱含的問題,那么換一個(gè)解決思路,既然隨機(jī)數(shù)池是固定長度,不能使用sleep或鎖,為什么不讓程序只保留一個(gè)隨機(jī)實(shí)例并當(dāng)獲取次數(shù)達(dá)到一定值時(shí)更新隨機(jī)實(shí)例呢?我們使用下面這個(gè)簡單例子來測試可行性:
import ( "math/rand" "time" "fmt" ) t1 := time.Now().UnixNano() fmt.Println(t1) r := rand.New(rand.NewSource(t1)) for i := 0; i < 600; i++ { r.Int31n(100) } t2 := time.Now().UnixNano() fmt.Println(t2)
上述代碼中,我們通過t1時(shí)間生成一個(gè)隨機(jī)實(shí)例,并讓該實(shí)例隨機(jī)600次,再獲取一次時(shí)間t2。執(zhí)行的結(jié)果會(huì)發(fā)現(xiàn)更大的問題,t1時(shí)間和t2時(shí)間是完全相同的!此時(shí)如果用t2作為種子,那么下一次隨機(jī)的600次會(huì)和t1時(shí)間的一模一樣。假如我們讓不同實(shí)例之間做延時(shí)或鎖,那么問題又會(huì)回到前面解決方案中同樣的問題。
因此,可以做出如下的結(jié)論。常用的math/rand在基于獲取當(dāng)前時(shí)間作為種子的隨機(jī)時(shí),無法真正地解決重復(fù)問題。
三、對(duì)crypto/rand隨機(jī)數(shù)的研究:
基于crypto/rand獲取隨機(jī)數(shù),常見的方法是:
import ( "crypto/rand" "math/big" ) r, _ := rand.Int(rand.Reader, big.NewInt(100)) result := r.Int64()
crypto/rand獲取的隨機(jī)數(shù)可以看做是真隨機(jī),其原因是它獲取種子的來源。簡單來說,在各類系統(tǒng)中,均存在一個(gè)特定的文件專門用于存儲(chǔ)真隨機(jī)值,這些值的來源是硬件在電路上產(chǎn)生的各種信息、熱噪聲等,由于它們是不可控的且無法預(yù)測的,因此可看做是產(chǎn)生了真正的隨機(jī)數(shù)。
在上述代碼中,rand.Reader就會(huì)從系統(tǒng)中存儲(chǔ)真隨機(jī)數(shù)的文件或者通過調(diào)用系統(tǒng)級(jí)別的API獲取一個(gè)真隨機(jī)數(shù)。對(duì)于系統(tǒng)而言,當(dāng)該隨機(jī)數(shù)被獲取后就會(huì)被銷毀,不會(huì)二次使用。
然而,crypto/rand有一個(gè)缺點(diǎn),它的獲取效率非常低,這是因?yàn)樗枰L問系統(tǒng)文件或者調(diào)用系統(tǒng)API,會(huì)產(chǎn)生不小的耗時(shí)。并且,每次從系統(tǒng)獲取的隨機(jī)值都會(huì)是一次性的,無法第二次使用。低效的獲取方式顯然并不能完全滿足高頻次和高并發(fā)下保持程序高效的需求。
四、最后的解決方案:
結(jié)合前面的相關(guān)內(nèi)容,可以找到一個(gè)取長補(bǔ)短的方式,得到一個(gè)解決方案:利用crypto/rand的rand.Reader獲取一個(gè)隨機(jī)數(shù),使用這個(gè)隨機(jī)數(shù)作為math/rand的種子獲得一個(gè)隨機(jī)數(shù)池進(jìn)行取值,當(dāng)取值達(dá)到一定次數(shù)后,再獲取一個(gè)新的種子生成新的實(shí)例。
通過rand.Reader獲取一個(gè)種子數(shù)的方法如下:
import ( "crypto/rand" "encoding/binary" ) var seed int64 binary.Read(rand.Reader, binary.BigEndian, &seed)
使用上述方法,即便高頻次或高并發(fā)地獲取種子數(shù),也能保證每次得到的種子都是不一樣的數(shù)值,這樣就能避免因種子相同導(dǎo)致的重復(fù)問題了。
最終的解決方案確定,結(jié)合后的代碼如下:
import ( crand "crypto/rand" "encoding/binary" mrand "math/rand" ) var seed int64 binary.Read(crand.Reader, binary.BigEndian, &seed) source := mrand.NewSource(seed) r := mrand.New(source) result := r.Int31n(100)
在上述代碼中,仍然需要注意一個(gè)重要問題,就是crand.Reader的耗時(shí)較長,在對(duì)取隨機(jī)的過程進(jìn)行封裝時(shí),需要進(jìn)行進(jìn)一步的改進(jìn):
1、增加一個(gè)統(tǒng)計(jì)閾值,每次隨機(jī)后+1,并當(dāng)次數(shù)達(dá)到一定值時(shí)更新隨機(jī)實(shí)例;
2、對(duì)更新隨機(jī)實(shí)例過程上鎖,避免并發(fā)時(shí)出現(xiàn)重復(fù)更新;
經(jīng)過改進(jìn)后,可自行測試其速度。作者參與過的項(xiàng)目實(shí)踐中,在統(tǒng)計(jì)閾值為300且較高的獲取次數(shù)下,平均每次獲取一個(gè)隨機(jī)數(shù)的耗時(shí)約25~30ns。不僅滿足了接近真隨機(jī)的需求,并且滿足了高效的需求。
以上就是解決Go語言中高頻次和高并發(fā)下的隨機(jī)數(shù)重復(fù)的問題的詳細(xì)內(nèi)容,更多關(guān)于Go高頻次和高并發(fā)隨機(jī)數(shù)重復(fù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用gorm.Scopes函數(shù)實(shí)現(xiàn)復(fù)用查詢邏輯示例
這篇文章主要為大家介紹了使用gorm.Scopes函數(shù)實(shí)現(xiàn)復(fù)用查詢邏輯示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12Go 數(shù)據(jù)結(jié)構(gòu)之堆排序示例詳解
這篇文章主要為大家介紹了Go 數(shù)據(jù)結(jié)構(gòu)之堆排序示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08