问题
进行业务开发时,需要随机连接不同节点进行业务实现,所以需要取随机数。 由于没有设定随机种子就导致每次都取到一样的随机数,每次都是连接到同一节点,导致该节点业务压力骤增,进而影响后续的服务调用。
例子复现
// 没有添加随机种子
func main() {
num := 10
for j := 0; j < num; j++ {
v := rand.Intn(num)
fmt.Print(v)
}
}
output:1779185060
// 添加随机种子
func main() {
num := 10
for j := 0; j < num; j++ {
rand.Seed(time.Now().Unix())
v := rand.Intn(num)
fmt.Println(v)
}
}
output:7777777777
// 添加随机种子,增加样本大小
func main() {
num := 10
randNum := 10000
for j := 0; j < num; j++ {
rand.Seed(time.Now().Unix())
v := rand.Intn(randNum)
fmt.Printf("%d,", v)
}
}
output:3877,3877,3877,3877,3877,3877,3877,3877,3877,3877,
例子分析
后续添加了随机种子以及增加样本空间大小,但是结果就是取到的随机值在变,但是基本上都是同一值,问题在于以上对于seed的调用都是在同一个循环中进行,每次的循环间隔都是毫秒级别的,每个循环中得到的时间戳基本一样,所以time.Now().Unix()得到的值基本趋于同一个值,seed用于得到一个确定的随机序列,只要随机种子确定,得到的值就会趋于固定。 golang 官方说明
// Seed uses the provided seed value to initialize the generator to a deterministic state.
// Seed should not be called concurrently with any other Rand method.
解决方案
- 全局初始化seed,确定随机种子
- 使用纳秒级别的随机种子
- 使用crypto/rand
优化
以上的方法建议结合一二使用,如果单独进行纳秒级别的随机种子,如果是在高并发的场景下,在串行业务场景使用还是会遇到纳秒级别的时间戳一样的情况,那得到的还会是重复的随机值。
优化例子
import (
"fmt"
"math/rand"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
num := 10
randNum := 10000
for j := 0; j < num; j++ {
v := rand.Intn(randNum)
fmt.Printf("%d,", v)
}
}
output:9225,9234,1181,4004,7447,9194,3869,1534,9819,9057,
rand 的默认的 Source 为了实现并发安全使用了全局的排他锁,如果需要控制锁的粒度,同时不与其他包竞争全局的排它锁,可以自己 New 一个 Rand 来使用,但是自己 New 的 rand 不是并发安全的,一定要自行加锁。
func main() {
num := 10
randNum := 10000
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for j := 0; j < num; j++ {
v := r.Intn(randNum)
fmt.Printf("%d,", v)
}
}
output:3990,223,5656,6415,8552,3513,3902,9263,8266,1938,
crypto/rand
crypto/rand包实现了用于加解密的更安全的随机数生成器,生成的随机数并不固定,并且一定会提供更好的随机性,内部实现规则:
// Reader is a global, shared instance of a cryptographically
// secure random number generator.
//
// On Linux and FreeBSD, Reader uses getrandom(2) if available, /dev/urandom otherwise.
// On OpenBSD, Reader uses getentropy(2).
// On other Unix-like systems, Reader reads from /dev/urandom.
// On Windows systems, Reader uses the CryptGenRandom API.
// On Wasm, Reader uses the Web Crypto API.
Reader是一个全局、共享的密码用强随机数生成器
在 Linux 和 FreeBSD 系统上,Reader 会使用 getrandom(2) (如果可用的话),否则使用 /dev/urandom。
在 OpenBSD 上,Reader 会使用 getentropy(2)。
在其他的类 Unix 系统上,Reader 会读取 /dev/urandom。
在 Windows 系统上,Reader 会使用 CryptGenRandom API.
在 Wasm 上,Reader 会使用 Web Cryto API。
crypto/rand包较math/rand包有了更安全的随机生成,但性能上较math/rand包有所降低。因为crypto/rand包执行更多的操作并且不能使用预生成的序列。
import (
"crypto/rand"
"fmt"
"math/big"
)
```go
import (
"crypto/rand"
"fmt"
"math/big"
)
func main() {
num := 10
for i := 0; i < num; i++ {
n, err := rand.Int(rand.Reader, big.NewInt(10000))
if err != nil {
panic(err)
}
fmt.Printf("%d,", n)
}
}
output:3403,3120,6278,6326,2761,9847,5262,9034,9067,8411,
性能比对
package main
import (
cryrand "crypto/rand"
"math/big"
"math/rand"
"testing"
)
func BenchmarkMathRand(b *testing.B) {
for i := 0; i < b.N; i++ {
rand.Intn(10000)
}
}
func BenchmarkCryptoRand(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = cryrand.Int(cryrand.Reader, big.NewInt(10000))
}
}
BenchmarkMathRand
BenchmarkMathRand-12 82710134 13.60 ns/op
BenchmarkCryptoRand
BenchmarkCryptoRand-12 3863425 313.9 ns/op
总结
对于 math/rand 与 crypto/rand 的使用应结合实际业务进行,大部分情况下 math/rand的正确使用就可以满足随机数生成的需求。