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